Skip to content

Conversation

@hongik-luke
Copy link

@hongik-luke hongik-luke commented Jan 13, 2026

💡 To Reviewers

tailwind 일부버전의 경우에
global.css 에
@theme {
--breakpoint-t: 768px;
--breakpoint-d: 1440px;
}
를 넣어야 적용이 된다는 사실을 알고 수정했습니다.

🔥 작업 내용 (가능한 구체적으로 작성해 주세요)

  • 모임 생성 UI
  • 모임 검색 UI 반응형 조절

📸 작업 결과 (스크린샷)

모임 생성 UI
테블릿 / 데스크탑

2026-01-13.151905.mp4

모바일

2026-01-13.152329.mp4

모임 검색 UI 반응형 조절

2026-01-13.152703.mp4

🔗 관련 이슈

  • [feat-21]

Summary by CodeRabbit

  • New Features

    • Added group creation wizard with multi-step form for club setup.
    • Introduced group and club search interface with filtering and application flow.
    • Added home page layout with news banners, book stories, and user recommendations.
    • Introduced user profile pages and settings interfaces.
  • UI/Style Updates

    • Updated design system with new typography tokens and color variables.
    • Added Pretendard font as default font family.
    • Introduced responsive design breakpoints for tablet and desktop layouts.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

This PR removes GitHub automation workflows and issue templates, then introduces extensive new UI components, pages, and styling infrastructure for a book club and group management platform. It adds group creation wizard and search functionality with responsive layouts, component library additions (book stories, profiles, news, search elements), TypeScript type definitions for group-related data, and updates to styling and build configuration.

Changes

Cohort / File(s) Summary
GitHub Configuration Cleanup
.github/ISSUE_TEMPLATE/custom.md, .github/PULL_REQUEST_TEMPLATE.md, .github/workflows/deploy.yml, .github/workflows/pr-merge-cleanup.yml, .github/workflows/preview.yml
Deleted GitHub issue template, PR template, and three CI/CD workflows (deploy, pr-merge-cleanup with issue closing logic, and Vercel preview deployment). Removes automation and contributor guidance.
Main App Pages & Layouts
src/app/(main)/layout.tsx, src/app/(main)/page.tsx, src/app/(main)/ui-test/page.tsx
Added main layout wrapping Header component and main element; HomePage with two-column layout featuring HomeBookclub, ListSubscribe, NewsBannerSlider, and BookStoryCard grid; UiTestClient page for UI testing.
Group Management Pages & Layouts
src/app/groups/layout.tsx, src/app/groups/page.tsx, src/app/groups/create/layout.tsx, src/app/groups/create/page.tsx
Introduced group list page (Searchpage) with search controls, club listings, and apply modal; multi-step group creation wizard (CreateClubWizardPreview) with name duplication checking, image upload, category/participant selection, and SNS link management; corresponding layout wrappers.
Book Story Components
src/components/base-ui/BookStory/bookstory_card.tsx, src/components/base-ui/BookStory/bookstory_choosebook.tsx, src/components/base-ui/BookStory/bookstory_detail.tsx, src/components/base-ui/BookStory/bookstory_text.tsx
Added reusable book story card components: card with author profile and interaction counts; choosebook with image and button; detail view with author and subscribe button; text editor with title and auto-resizing textarea.
Group Creation UI Components
src/components/base-ui/Group-Create/Chip.tsx, src/components/base-ui/Group-Create/StepDot.tsx
Introduced Chip button component for selections (togglable, disabled states) and StepDot indicator for multi-step wizard progress visualization.
Group Search Components
src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx, src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx, src/components/base-ui/Group-Search/search_club_apply_modal.tsx, src/components/base-ui/Group-Search/search_groupsearch.tsx, src/components/base-ui/Group-Search/search_mybookclub.tsx
Added group search functionality: category tag renderer with lookup mapping; club list item with apply button and mobile-expandable form; apply modal with textarea and submit; search bar with text input, category dropdown, and toggle filters; MyBookclub component with responsive grid and collapse/expand behavior.
Profile & Social Components
src/components/base-ui/Profile/mypage_profile.tsx, src/components/base-ui/Profile/others_profile.tsx, src/components/base-ui/Profile/subscribe_element.tsx, src/components/base-ui/Profile/notification_element.tsx
Introduced profile display components: MyPageProfile with edit and settings buttons; OthersProfile with subscribe toggle and report action; Subscribe element for quick subscription; NotificationElement for activity notifications (likes/comments).
News & Recommendation Components
src/components/base-ui/News/news_list.tsx, src/components/base-ui/News/recommendbook_element.tsx, src/components/base-ui/Settings/setting_news_list.tsx
Added news list items with image, title, content, and date; book cover card with like button and optional click handlers; setting-scoped news list variant.
Search Components
src/components/base-ui/Search/search_bookresult.tsx, src/components/base-ui/Search/search_recommendbook.tsx
Introduced book search result row with thumbnail, title, author, and like/pencil interaction buttons; book cover card for recommendation display with toggle like state.
Home & Layout Components
src/components/base-ui/home/NewsBannerSlider.tsx, src/components/base-ui/home/home_bookclub.tsx, src/components/base-ui/home/list_subscribe.tsx, src/components/base-ui/home/list_subscribe_element.tsx, src/components/base-ui/home/notification_element.tsx, src/components/layout/Header.tsx, src/components/layout/NavItem.tsx, src/components/base-ui/button_without_img.tsx
Added home page sections: news banner carousel; book club list panel with expand/collapse; user subscription suggestions; notification elements; site header with navigation and action icons; navigation menu item; custom button component with configurable colors and hover states.
Settings Components
src/components/base-ui/Settings/setting_report_list.tsx
Added report list item component displaying badge, reporter info, timestamp, and content.
Type Definitions & Utilities
src/types/groups/groups.ts, src/app/groups/groupSearchDummy.ts, src/utils/groupMapper.ts
Introduced ApplyType, ParticipantType, Category, and BookCategory union types; participant/category bidirectional mapping; utility converters (toParticipantType, toCategories); dummy data for group and club mock objects.
Styling & Configuration
src/app/globals.css, tailwind.config.js, tsconfig.json, src/app/layout.tsx
Updated global CSS with Pretendard font, comprehensive color system (primary, secondary, grays), new typography utility classes (headlines, subheads, body, captions), and breakpoint variables; added Tailwind breakpoints (t: 768px, d: 1440px); expanded tsconfig to include Next.js type files; changed root layout quotes to single quotes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CreateClubWizardPreview
    participant StepDot
    participant FormInputs as Form Inputs
    participant State as Local State
    participant Validation

    User->>CreateClubWizardPreview: Load wizard
    activate CreateClubWizardPreview
    CreateClubWizardPreview->>StepDot: Render step indicators
    CreateClubWizardPreview->>FormInputs: Render step 1 (basic info)
    deactivate CreateClubWizardPreview

    User->>FormInputs: Enter club name
    activate FormInputs
    FormInputs->>State: Update name
    State->>Validation: Check name duplication
    Validation-->>State: duplicate/available status
    FormInputs->>CreateClubWizardPreview: Enable/disable next button
    deactivate FormInputs

    User->>CreateClubWizardPreview: Click next
    activate CreateClubWizardPreview
    CreateClubWizardPreview->>State: Move to step 2
    CreateClubWizardPreview->>FormInputs: Render image upload
    deactivate CreateClubWizardPreview

    User->>FormInputs: Upload profile image
    activate FormInputs
    FormInputs->>State: Store image preview
    FormInputs->>CreateClubWizardPreview: Show preview
    deactivate FormInputs

    loop Steps 3-4
        User->>CreateClubWizardPreview: Navigate steps
        activate CreateClubWizardPreview
        CreateClubWizardPreview->>State: Update form data
        CreateClubWizardPreview->>StepDot: Highlight current step
        deactivate CreateClubWizardPreview
    end

    User->>CreateClubWizardPreview: Submit final step
    activate CreateClubWizardPreview
    CreateClubWizardPreview->>State: Collect all form data
    State-->>CreateClubWizardPreview: Complete club object
    deactivate CreateClubWizardPreview
Loading
sequenceDiagram
    participant User
    participant Searchpage
    participant SearchGroupSearch
    participant SearchClubListItem
    participant SearchClubApplyModal
    participant State

    User->>Searchpage: Load group search page
    activate Searchpage
    Searchpage->>SearchGroupSearch: Render search bar
    Searchpage->>SearchClubListItem: Render club list
    deactivate Searchpage

    User->>SearchGroupSearch: Enter search query/filter
    activate SearchGroupSearch
    SearchGroupSearch->>State: Update search/filter state
    State->>Searchpage: Trigger re-render
    deactivate SearchGroupSearch
    
    Searchpage->>SearchClubListItem: Update filtered clubs
    activate SearchClubListItem
    SearchClubListItem->>User: Display filtered results
    deactivate SearchClubListItem

    User->>SearchClubListItem: Click apply button
    activate SearchClubListItem
    SearchClubListItem->>State: Set applyClubId
    State->>SearchClubApplyModal: Open modal with club
    deactivate SearchClubListItem

    activate SearchClubApplyModal
    SearchClubApplyModal->>User: Show club details & reason form
    deactivate SearchClubApplyModal

    User->>SearchClubApplyModal: Enter reason & submit
    activate SearchClubApplyModal
    SearchClubApplyModal->>Searchpage: Call onSubmitApply callback
    Searchpage->>State: Log apply (API TODO)
    State->>SearchClubApplyModal: Close modal
    deactivate SearchClubApplyModal
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • fix: 폴더 이동 #4: Both PRs modify tsconfig.json's .next/ type file inclusion paths, indicating related build configuration changes.
  • chore: 이슈 닫기 워크플로우 추가 #7: PR#7 introduces the pr-merge-cleanup.yml workflow that this PR deletes, creating a direct conflict or supersession relationship.
  • feat: header UI 추가 #9: Both PRs add identical header and layout components (src/components/layout/Header.tsx, src/components/layout/NavItem.tsx, src/app/(main)/layout.tsx, src/app/(main)/page.tsx), indicating overlapping feature work.

Suggested reviewers

  • shinwokkang
  • bini0918
  • psm1st

Poem

🐰 A wizard's dance through four swift steps,
With clubs to birth and searches kept,
New colors bloom, new fonts align,
While old workflows fade—the future's fine! ✨
Book clubs sing as profiles bloom,
Magic fills this growing room! 📚💫

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly addresses the main changes: it describes the implementation of group/community creation UI and responsive adjustments to group search UI.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 @hongik-luke, 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은 모임 생성 및 검색 기능의 반응형 사용자 인터페이스를 구현하고, Tailwind CSS 브레이크포인트 적용 문제를 해결하는 데 중점을 둡니다. 전반적인 사용자 경험을 향상시키고 향후 기능을 위한 기반을 마련하기 위해 여러 새로운 UI 컴포넌트와 스타일 정의가 도입되었습니다.

Highlights

  • 반응형 UI 조정: 모임 생성 및 모임 검색 UI에 대한 반응형 디자인이 태블릿 및 데스크톱 뷰포트에 맞춰 조정되었습니다.
  • Tailwind CSS 설정 수정: Tailwind CSS의 일부 버전에서 전역 CSS에 @theme 블록을 추가하여 브레이크포인트가 올바르게 적용되도록 수정되었습니다.
  • 새로운 UI 컴포넌트 및 에셋 추가: 책 이야기 카드, 모임 생성 마법사, 모임 검색 목록, 뉴스 배너 슬라이더, 프로필 및 설정 관련 컴포넌트 등 다양한 새로운 UI 컴포넌트와 SVG 에셋이 추가되었습니다.
  • 전역 스타일 및 타이포그래피 개선: Pretendard 폰트, 새로운 색상 변수, 그리고 헤드라인, 서브헤드, 바디, 캡션 등 다양한 타이포그래피 유틸리티 클래스를 포함하도록 globals.css 파일이 업데이트되었습니다.

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

Ignored Files
  • Ignored by pattern: .github/workflows/** (3)
    • .github/workflows/deploy.yml
    • .github/workflows/pr-merge-cleanup.yml
    • .github/workflows/preview.yml
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

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

Footnotes

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Note

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

🤖 Fix all issues with AI agents
In @src/app/groups/page.tsx:
- Around line 19-29: Remove the erroneous method declaration from the
ClubSummary interface: delete the line declaring reason(clubId: number, reason:
string): void so ClubSummary only contains data properties (clubId, name,
profileImageUrl, category, public, applytype, region, participantTypes). Verify
there are no consumers expecting a reason method and, if any exist, refactor
them to accept the DTO as data (or move that method to the appropriate
service/class) and update groupSearchDummy.ts to remain valid with the cleaned
interface.

In @src/components/base-ui/BookStory/bookstory_choosebook.tsx:
- Around line 1-2: This component (bookstory_choosebook.tsx) is interactive (has
an onClick handler) but lacks the required 'use client' directive; fix it by
adding the exact line 'use client' as the very first line of the file (before
any imports) so the component becomes a client component, and verify that the
exported component/function with the onClick handler continues to only use
client-safe APIs (no server-only modules).

In @src/components/base-ui/Group-Search/search_club_apply_modal.tsx:
- Line 169: onSubmit is typed as (club: number, reason: string) => void but is
being called with only reason; change the call site to pass the club id first
(e.g., onSubmit(club, reason)) so the parent receives (club: number, reason:
string). Locate the component prop/type that declares the club id (the parameter
named "club" in the prop type) and use that identifier when invoking onSubmit in
the onClick handler.

In @src/components/base-ui/Group-Search/search_groupsearch.tsx:
- Around line 49-60: The outside-click handler never fires because the ref
created with useRef (wrapRef) is never attached to the DOM; update the component
JSX to pass wrapRef to the dropdown/container element (e.g., add ref={wrapRef}
on the element that wraps the searchable dropdown), ensuring the useEffect's
onDown can access wrapRef.current and call setOpen(false) correctly; keep the
existing useEffect, onDown, useRef, and setOpen logic unchanged.

In @src/components/base-ui/home/list_subscribe_element.tsx:
- Around line 1-2: This component uses an onClick handler but is missing the
Next.js client component directive; add the string "use client" as the very
first line of src/components/base-ui/home/list_subscribe_element.tsx (before any
imports) so the component becomes a client component and its event handler (the
onClick in this file) will work correctly.

In @src/components/base-ui/Profile/mypage_profile.tsx:
- Around line 96-99: The span uses a non-existent utility class subhead_4 so its
styles won't apply; update the class to one of the defined utilities (e.g.,
subhead_4_1 or one of subhead_1/2/3/3_1/3_2 depending on intended size) by
replacing subhead_4 in the span's className, and verify the visual matches
design; ensure no other occurrences of subhead_4 remain (search for "subhead_4")
and run the app to confirm styling.

In @src/components/base-ui/Profile/notification_element.tsx:
- Around line 1-50: Duplicate NotificationElement component exists in two
places; consolidate by extracting the NotificationElement function (and its
props type NotificationElementProps) into a single shared/common component
module, export it, update all imports that currently reference the duplicated
copies to import the shared NotificationElement, and then remove the redundant
duplicate file so only the shared implementation remains.

In @src/components/base-ui/Settings/setting_report_list.tsx:
- Around line 22-28: The fixed-width class w-[1000px] in the SettingsReportList
container causes overflow on small screens; replace it with responsive sizing
(e.g., use w-full or max-w-[1000px] plus breakpoint aliases) so the component
expands to full width on mobile and caps at 1000px on larger screens—update the
class string in setting_report_list.tsx (the div with className containing
w-[1000px]) to something like w-full sm:w-[1000px] or w-full max-w-[1000px]
(preserving existing padding, gap, border and ${className}).
🟡 Minor comments (16)
src/app/(main)/ui-test/page.tsx-3-4 (1)

3-4: Remove unused imports.

Subscribe_elements is imported but never used. The useState import is also unnecessary if the unused state is removed.

Suggested cleanup
 "use client";
-import React from "react";
-import { useState } from "react";
-import Subscribe_elements from "@/components/base-ui/Profile/subscribe_element";
 import Setting_ReportList from "@/components/base-ui/Settings/setting_report_list";
src/app/(main)/ui-test/page.tsx-7-9 (1)

7-9: Remove unused state.

The subs state variable and its setter setSubs are defined but never used in the component.

Suggested cleanup
 export default function UiTestClient() {
-  const [subs, setSubs] = useState(false);
-
   return (

Note: If the unused imports and state are removed, the 'use client' directive can also be removed since the component would become purely presentational.

src/app/globals.css-116-118 (1)

116-118: Unexpected truncate in body_1 utility.

The body_1 utility includes truncate which forces single-line text with ellipsis. This is likely unintentional for a general-purpose body text utility and will cause unexpected text truncation wherever body_1 is applied. Consider removing it or creating a separate body_1_truncate variant.

🔧 Suggested fix
 @utility body_1 {
-  @apply text-[14px] font-semibold leading-[145%] tracking-[-0.014px] truncate;
+  @apply text-[14px] font-semibold leading-[145%] tracking-[-0.014px];
 }
src/components/base-ui/Profile/others_profile.tsx-6-21 (1)

6-21: Unused onSettings prop declared in type.

The onSettings prop is defined in OthersProfileProps (line 17) but is never destructured or used in the component. Either remove it from the type definition or implement the functionality.

🔧 Suggested fix - remove unused prop
 type OthersProfileProps = {
   profileImgSrc?: string; // default: /profile.svg
   name: string;

   followingCount: number; // 구독중
   followerCount: number; // 구독자
   isSubscribed: boolean;
   onToggleSubscribe: (newState: boolean) => void;

   intro: string;

-  onSettings?: () => void;
   onReportClick?: () => void;

   className?: string;
 };
src/app/layout.tsx-14-17 (1)

14-17: Update placeholder metadata.

The metadata still contains default Next.js scaffold values. Consider updating title and description to reflect the actual application (e.g., the 모임/group management platform).

📝 Suggested update
 export const metadata: Metadata = {
-  title: 'Create Next App',
-  description: 'Generated by create next app',
+  title: '체크모', // or appropriate app name
+  description: '모임 관리 플랫폼', // or appropriate description
 };
src/components/base-ui/News/news_list.tsx-48-51 (1)

48-51: Comment-code mismatch: comment says 120px but code uses 180px.

The comment states "최소 120px 확보" but the actual class uses min-w-[180px]. Update either the comment or the value to be consistent.

tsconfig.json-35-39 (1)

35-39: Remove stale checkmo/ prefixed paths from include array.

The checkmo directory does not exist in the repository. The paths checkmo/.next/types/**/*.ts and checkmo/.next/dev/types/**/*.ts appear to be leftover from a previous configuration and should be removed, keeping only the root-level .next/types/**/*.ts and .next/dev/types/**/*.ts patterns.

src/components/base-ui/Search/search_bookresult.tsx-66-68 (1)

66-68: Typo: flex1 should be flex-1.

This appears to be a typo in the Tailwind class name.

Proposed fix
-<p className="flex1 h-full text-[color:var(--Gray_4,#8D8D8D)] body_1_2 line-clamp-6">
+<p className="flex-1 h-full text-[color:var(--Gray_4,#8D8D8D)] body_1_2 line-clamp-6">
   {clippedDetail}
 </p>
src/app/groups/create/page.tsx-411-416 (1)

411-416: Missing maxLength enforcement on activity area input.

The placeholder mentions a 40 character limit, but there's no maxLength attribute to enforce it.

Proposed fix
 <input
   value={activityArea}
   onChange={(e) => setActivityArea(e.target.value)}
   placeholder="활동 지역을 입력해주세요 (40자 제한)"
+  maxLength={40}
   className="..."
 />
src/app/groups/create/page.tsx-195-216 (1)

195-216: Missing maxLength enforcement on textarea.

The placeholder mentions a 500 character limit, but there's no maxLength attribute to enforce it.

Proposed fix
 <textarea
   value={clubDescription}
   onChange={(e) => {
     setClubDescription(e.target.value);
     autoResize(e.currentTarget);
   }}
   onInput={(e) => autoResize(e.currentTarget)}
   placeholder="자유롭게 입력해주세요! (500자 제한)"
+  maxLength={500}
   className="..."
 />
src/components/base-ui/Group-Search/search_mybookclub.tsx-62-65 (1)

62-65: Invalid/empty Tailwind class h-[].

h-[] is an incomplete arbitrary value class and will have no effect. Either remove it or specify an actual height value.

Proposed fix
-<div className="h-[] flex items-center justify-center py-4 t:py-10 d:py-20">
+<div className="flex items-center justify-center py-4 t:py-10 d:py-20">
   <Image src="/logo2.svg" alt="로고" height={300} width={300} />
 </div>
src/app/groups/create/page.tsx-572-584 (1)

572-584: Button text should indicate final step.

On step 4 (the last step), the button still says "다음" (Next). It should say something like "완료" (Complete) or "생성" (Create) to indicate this is the final action.

Proposed fix
 <button
   type="button"
   onClick={onNext}
   disabled={!canNext}
   className={cx(...)}
 >
-  다음
+  완료
 </button>
src/app/groups/create/page.tsx-57-65 (1)

57-65: Memory leak risk: setTimeout is not cleaned up on unmount.

If the component unmounts during the 500ms delay, the callback will still fire and attempt to update state on an unmounted component.

Proposed fix
 const fakeCheckName = () => {
   if (!clubName.trim()) return;
   setNameCheck("checking");
-  setTimeout(() => {
-    // 그냥 프리뷰용: 이름에 "중복" 들어가면 duplicate 처리
-    if (clubName.includes("중복")) setNameCheck("duplicate");
-    else setNameCheck("available");
-  }, 500);
+  const timer = setTimeout(() => {
+    if (clubName.includes("중복")) setNameCheck("duplicate");
+    else setNameCheck("available");
+  }, 500);
+  return timer;
 };

Then store and clear the timer in a useEffect cleanup or use a ref to track it.

src/app/groups/groupSearchDummy.ts-7-17 (1)

7-17: Duplicate IDs will cause React key warnings.

IDs '1', '2', '3', '4' appear twice in mydummyGroup. Even for mock data, duplicate keys cause React warnings and can lead to rendering bugs.

Suggested fix
 export const mydummyGroup: GroupSummary[] = [
   { id: '1', name: '모임1' },
   { id: '2', name: '모임2' },
   { id: '3', name: '모임3' },
   { id: '4', name: '모임4' },
-  { id: '1', name: '모임11241' },
-  { id: '2', name: '모임51212' },
-  { id: '3', name: '모임125153' },
-  { id: '4', name: '모임12512514' },
+  { id: '5', name: '모임11241' },
+  { id: '6', name: '모임51212' },
+  { id: '7', name: '모임125153' },
+  { id: '8', name: '모임12512514' },
 ];
src/components/base-ui/Search/search_recommendbook.tsx-44-58 (1)

44-58: Add accessible label to the like button.

The button contains only an image with alt="". Screen reader users won't understand the button's purpose. Add an aria-label for accessibility.

Suggested fix
         <button
           type="button"
+          aria-label={liked ? '좋아요 취소' : '좋아요'}
           onClick={(e) => {
             e.stopPropagation();
             onLikeChange(!liked);
           }}
           className="w-[24px] h-[24px] shrink-0"
         >
src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx-51-51 (1)

51-51: Typo: item-center should be items-center.

The Tailwind CSS class item-center is invalid. The correct class is items-center for flexbox vertical alignment.

🐛 Proposed fix
-             'h-[21px] my-auto py-[1px] inline-flex item-center justify-center body_1_2',
+             'h-[21px] my-auto py-[1px] inline-flex items-center justify-center body_1_2',
🧹 Nitpick comments (43)
src/components/base-ui/home/notification_element.tsx (2)

32-32: Fixed width may break responsive layout.

The component uses a hardcoded w-[364px] width, which conflicts with the PR's responsive design goals. Consider using w-full or responsive width utilities to allow the parent container to control sizing.

♻️ Suggested fix
-    <div className="flex w-[364px] px-[16px] py-[20px] justify-between items-center border-b border-b-[color:var(--Subbrown_4,#EAE5E2)] bg-[color:var(--White,#FFF)]">
+    <div className="flex w-full px-[16px] py-[20px] justify-between items-center border-b border-b-[color:var(--Subbrown_4,#EAE5E2)] bg-[color:var(--White,#FFF)]">

35-39: Consider adding accessible label for the status indicator.

The red dot indicator for isLatest conveys meaning visually but is not accessible to screen readers. Consider adding an aria-label or sr-only text.

♻️ Suggested fix
        {isLatest ? (
-          <span className="w-[12px] h-[12px] rounded-full bg-red-500 shrink-0" />
+          <span className="w-[12px] h-[12px] rounded-full bg-red-500 shrink-0" aria-label="새 알림" role="img" />
        ) : (
          <span className="w-[12px] h-[12px] shrink-0" />
        )}
src/components/base-ui/Settings/setting_report_list.tsx (1)

1-1: Consider removing 'use client' directive.

This component is purely presentational with no client-side interactivity (no state, effects, or event handlers). It can be rendered as a Server Component, reducing the client bundle size.

src/app/globals.css (3)

128-130: Duplicate typography definitions: body_2_1 and caption_1_1 are identical.

Both utilities apply the exact same styles (text-[12px] font-semibold leading-[145%] tracking-[-0.012px]). Consider whether both are needed or if one should be an alias/removed to reduce maintenance overhead.

Also applies to: 139-141


147-150: Consider consolidating @theme blocks.

There are two separate @theme blocks (lines 33-64 and 147-150). While functionally valid, consolidating them into a single block improves maintainability and makes the design system easier to understand at a glance.


1-1: Pin the font CDN version for production stability.

Using @latest in the CDN URL means builds may receive different font versions over time, potentially causing visual inconsistencies or breaking changes. Pin to a specific version (e.g., @v1.3.9) for reproducible builds.

🔧 Suggested fix
-@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@latest/dist/web/static/pretendard.css");
+@import url("https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/static/pretendard.css");
src/components/base-ui/Profile/mypage_profile.tsx (2)

109-116: Remove empty className attribute.

The className="" on line 114 serves no purpose and adds noise. Remove it for cleaner code.

🔧 Suggested fix
               <Image
                 src="/setting_icon.svg"
                 alt=""
                 width={24}
                 height={24}
-                className=""
               />

36-51: Consider extracting shared profile header logic.

The profile header section (avatar, name, follow counts, intro) is nearly identical between MypageProfile and OthersProfile. Consider extracting a shared ProfileHeader component to reduce duplication and ensure consistent styling across both variants.

Also applies to: 53-82

src/components/base-ui/Profile/subscribe_element.tsx (1)

46-60: Consider using the caption_1_1 utility instead of inline font styles.

The button's font styles (text-[12px] font-semibold leading-[100%] tracking-[-0.012px]) closely match the caption_1_1 utility defined in globals.css. Using the utility would improve consistency and reduce duplication.

Note: The leading-[100%] differs from caption_1_1's leading-[145%], so verify if the tighter line-height is intentional for this button context.

src/components/base-ui/button_without_img.tsx (1)

52-62: Consider CSS-based hover for better performance.

Using useState for hover state causes React re-renders on every mouse enter/leave. For a button that may appear many times on a page, this can impact performance. Consider using CSS custom properties with :hover pseudo-class, or Tailwind's hover: variants.

💡 Alternative approach using CSS variables and :hover

You could set CSS variables as inline styles and use a :hover selector in the className:

// Simplified approach - define base and hover colors as CSS vars
<button
  style={{
    '--btn-bg': toCssColor(bgColorVar),
    '--btn-bg-hover': toCssColor(hoverBgColorVar ?? bgColorVar),
    // ... other colors
  } as React.CSSProperties}
  className={`
    bg-[var(--btn-bg)] hover:bg-[var(--btn-bg-hover)]
    ...
  `}
>

This eliminates re-renders while preserving the dynamic color capability.

Also applies to: 69-70

tailwind.config.js (1)

3-10: Consider more descriptive breakpoint names for clarity.

Single-letter abbreviations (t, d) are cryptic. More descriptive names like tablet and desktop would improve readability across the codebase without adding significant verbosity.

Also, there's inconsistent indentation—lines 3–10 use 4-space indentation while nested properties use 2-space.

♻️ Suggested improvement
 module.exports = {
     content: ['./src/**/*.{js,ts,jsx,tsx}', './app/**/*.{js,ts,jsx,tsx}'],
-    theme: {
-    extend: {
-      screens: {
-        t: "768px",
-        d: "1440px",
-      },
-    },
-  },
+    theme: {
+        extend: {
+            screens: {
+                tablet: "768px",
+                desktop: "1440px",
+            },
+        },
+    },
     plugins: [],
-  };
+};
src/app/groups/create/layout.tsx (1)

4-11: Unnecessary fragment wrapper.

The React fragment (<>...</>) serves no purpose here since it wraps only a single <main> element. Returning <main> directly simplifies the code.

♻️ Suggested simplification
 export default function MainLayout({ children }: { children: ReactNode }) {
-  return (
-    <>
-
-      <main>{children}</main>
-    </>
-  );
+  return <main>{children}</main>;
 }
src/components/base-ui/Group-Create/StepDot.tsx (2)

1-1: Unused React import.

In modern React (17+) with the new JSX transform, explicit import React from "react" is unnecessary unless you're using React APIs directly (e.g., React.useState).

♻️ Remove unused import
-import React from "react";
-

3-5: Consider using clsx or classnames library.

Rolling a custom cx helper is fine for simple cases, but using a well-established library like clsx provides better edge-case handling and is a common convention in React/Next.js projects.

If clsx is already a project dependency or you'd like to add it:

-function cx(...classes: (string | false | null | undefined)[]) {
-  return classes.filter(Boolean).join(" ");
-}
+import clsx from "clsx";

Then replace cx(...) calls with clsx(...).

src/app/layout.tsx (1)

24-26: Consider setting lang attribute to match target audience.

The application appears to target Korean users (based on the PR objectives mentioning 모임 생성/검색), but lang="en" is set. This affects accessibility tools, search engines, and browser behavior.

♻️ Suggested fix
-    <html lang="en">
+    <html lang="ko">
src/components/base-ui/News/news_list.tsx (1)

1-1: Consider removing 'use client' directive.

This component doesn't use any client-side hooks (useState, useEffect, event handlers, etc.). It could be rendered as a Server Component, which would reduce the client-side JavaScript bundle.

src/components/layout/Header.tsx (1)

55-91: Reconsider priority on icon images.

All three icon images have priority={true}, which preloads them. The priority prop should typically be reserved for above-the-fold LCP (Largest Contentful Paint) elements like the logo. For small navigation icons, this may unnecessarily delay more important resources.

♻️ Suggested change
             <Link href="/search" aria-label="검색" className="relative h-6 w-6">
               <Image
                 src="/search_light.svg"
                 alt="검색"
                 fill
                 className="object-contain"
-                priority
               />
             </Link>

             <Link
               href="/notification"
               aria-label="알림"
               className="relative h-6 w-6"
             >
               <Image
                 src="/notification.svg"
                 alt="알림"
                 fill
                 className="object-contain"
-                priority
               />
             </Link>

             <Link
               href="/profile"
               aria-label="프로필"
               className="relative h-6 w-6"
             >
               <Image
                 src="/profile.svg"
                 alt="프로필"
                 fill
                 className="object-contain"
-                priority
               />
             </Link>
src/app/(main)/layout.tsx (1)

4-10: Layout structure is clean, but duplicated with groups/layout.tsx.

This layout is identical to src/app/groups/layout.tsx. If both sections should share the same layout, consider using a shared layout higher in the route hierarchy or extracting a common layout component to avoid duplication.

However, if the layouts are expected to diverge (e.g., groups section will have additional UI), keeping them separate is acceptable.

src/app/groups/layout.tsx (1)

4-10: Consider renaming to GroupsLayout for clarity.

Both this file and src/app/(main)/layout.tsx export a function named MainLayout. While Next.js uses file-based routing and the export name doesn't affect routing, using a more specific name like GroupsLayout improves code clarity and debugging experience.

♻️ Suggested change
-export default function MainLayout({ children }: { children: ReactNode }) {
+export default function GroupsLayout({ children }: { children: ReactNode }) {
src/components/base-ui/Group-Create/Chip.tsx (1)

3-5: Consider extracting the cx utility to a shared module.

This helper is duplicated in src/app/groups/create/page.tsx. Extract it to a shared utility (e.g., src/utils/cx.ts) or use a library like clsx/classnames to avoid duplication.

src/app/groups/create/page.tsx (2)

7-7: Unused imports detected.

PARTICIPANT_LABEL_TO_TYPE and ParticipantType are imported but not used in this file. Remove them to keep imports clean.

Proposed fix
-import { BOOK_CATEGORIES, BookCategory, PARTICIPANT_LABEL_TO_TYPE, ParticipantLabel, PARTICIPANTS, ParticipantType } from "@/types/groups/groups";
+import { BOOK_CATEGORIES, BookCategory, ParticipantLabel, PARTICIPANTS } from "@/types/groups/groups";

73-77: Indentation issue: function appears outside component scope.

The toggleWithLimit helper has inconsistent indentation which makes it look like it's outside the component. While syntactically this still works (it's inside the function), the formatting should be corrected for clarity.

Proposed fix
-  const toggleWithLimit = <T,>(arr: T[], item: T, limit: number) => {
-  if (arr.includes(item)) return arr.filter((x) => x !== item);
-  if (arr.length >= limit) return arr;
-  return [...arr, item];
-};
+  const toggleWithLimit = <T,>(arr: T[], item: T, limit: number) => {
+    if (arr.includes(item)) return arr.filter((x) => x !== item);
+    if (arr.length >= limit) return arr;
+    return [...arr, item];
+  };
src/components/base-ui/Search/search_bookresult.tsx (1)

88-101: Pencil button renders even when onPencilClick is undefined.

Since onPencilClick is optional, consider conditionally rendering the pencil button to avoid showing a non-functional button.

Proposed fix
-<button
-  type="button"
-  onClick={(e) => {
-    e.stopPropagation();
-    onPencilClick?.();
-  }}
-  className="..."
->
-  <Image src="/pencil_icon.svg" alt="" width={20} height={20} />
-</button>
+{onPencilClick && (
+  <button
+    type="button"
+    onClick={(e) => {
+      e.stopPropagation();
+      onPencilClick();
+    }}
+    className="..."
+  >
+    <Image src="/pencil_icon.svg" alt="" width={20} height={20} />
+  </button>
+)}
src/components/base-ui/Group-Search/search_mybookclub.tsx (1)

69-74: Redundant flex-col class with grid layout.

When using grid, the flex-col class has no effect since grid layout takes precedence. Remove it for clarity.

Proposed fix
-"grid grid-cols-1 t:grid-cols-2 d:grid-cols-1 flex-col gap-2",
+"grid grid-cols-1 t:grid-cols-2 d:grid-cols-1 gap-2",
src/components/base-ui/Group-Search/search_groupsearch.tsx (1)

6-13: Duplicate Category type definition.

The Category type is already defined in src/types/groups/groups.ts. Import it from there to maintain a single source of truth.

Proposed fix
+'use client';
+
+import Image from 'next/image';
+import { useEffect, useRef, useState } from 'react';
+import { Category } from '@/types/groups/groups';
-'use client';
-
-import Image from 'next/image';
-import { useEffect, useRef, useState } from 'react';
-
-export type Category =
-  | '전체'
-  | '대학생'
-  | '직장인'
-  | '온라인'
-  | '동아리'
-  | '모임'
-  | '대면';
src/utils/groupMapper.ts (1)

1-1: Import types from the dedicated types module instead of the page component.

The Category and ParticipantType types are defined in src/types/groups/groups.ts (as shown in the relevant code snippets). Importing from a page component couples this utility to presentation code and may cause circular dependency issues.

Suggested fix
-import { Category, ParticipantType } from "@/app/groups/page";
+import { Category, ParticipantType } from "@/types/groups/groups";
src/components/base-ui/home/list_subscribe.tsx (2)

25-25: Remove console.log before production.

Development logging should be replaced with actual subscription logic or a no-op placeholder.

Suggested fix
-            onSubscribeClick={() => console.log('subscribe', u.id)}
+            onSubscribeClick={() => {
+              // TODO: implement subscription logic
+            }}

5-11: Consider extracting mock data or accepting users as a prop.

The hardcoded users array limits reusability. For a more flexible component, consider accepting users as a prop or importing from a dedicated mock data file.

src/components/base-ui/home/NewsBannerSlider.tsx (2)

17-17: Fixed width may break responsiveness.

The w-[1040px] constraint won't adapt to smaller viewports. Consider using responsive widths or max-width with percentage-based sizing given the PR's responsive focus.

Suggested approach
-    <div className="relative h-[424px] w-[1040px] overflow-hidden rounded-[10px]">
+    <div className="relative h-[424px] w-full max-w-[1040px] overflow-hidden rounded-[10px]">

6-11: Duplicate banner entry.

"/news_sample.svg" appears at both index 0 and 3. If intentional as placeholder data, consider adding a comment; otherwise, replace with unique assets.

src/app/(main)/page.tsx (2)

11-11: Fixed width limits responsiveness.

w-[1400px] creates a rigid layout that won't adapt to smaller screens. Given the PR focuses on responsive adjustments, consider using max-width or responsive breakpoints.

Suggested approach
-    <div className="mx-auto w-[1400px]">
+    <div className="mx-auto w-full max-w-[1400px] px-4 d:px-0">

29-60: Hardcoded BookStoryCard data—consider extracting to mock data or TODO comment.

Three identical cards with hardcoded props are fine for initial scaffolding. Adding a TODO or extracting to a mock data array (like groupSearchDummy.ts) would clarify intent.

src/components/base-ui/News/recommendbook_element.tsx (2)

29-34: Consider accessibility for the clickable card.

The clickable <div> is not keyboard-accessible. Users navigating with keyboard won't be able to focus or activate the card click handler.

♿ Suggested fix for keyboard accessibility
     <div
       onClick={onCardClick}
+      role={onCardClick ? 'button' : undefined}
+      tabIndex={onCardClick ? 0 : undefined}
+      onKeyDown={onCardClick ? (e) => { if (e.key === 'Enter' || e.key === ' ') onCardClick(); } : undefined}
       className={`relative flex w-[244px] h-[320px] p-[12px] flex-col justify-end items-start gap-[10px] overflow-hidden ${
         onCardClick ? 'cursor-pointer' : ''
       } ${className}`}
     >

44-58: Add accessible label to the like button.

The button has no accessible name for screen reader users. Consider adding aria-label or aria-pressed to convey state.

♿ Proposed fix
         <button
           type="button"
+          aria-label={liked ? '좋아요 취소' : '좋아요'}
+          aria-pressed={liked}
           onClick={(e) => {
             e.stopPropagation();
             onLikeChange(!liked);
           }}
           className="w-[24px] h-[24px] shrink-0"
         >
src/components/base-ui/BookStory/bookstory_choosebook.tsx (1)

29-36: Add fallback for potentially empty bookUrl.

bookUrl is typed as required string, but if an empty string is passed, Next.js Image will fail. Consider adding a fallback similar to other components in this PR.

🛡️ Proposed fix
+        const coverSrc = bookUrl && bookUrl.length > 0 ? bookUrl : '/booksample.svg';
+
         <div className="relative w-[100px] h-[145px] shrink-0">
           <Image
-            src={bookUrl}
+            src={coverSrc}
             alt={bookName}
             fill
             className="object-cover rounded-[4px]"
             sizes="100px"
           />
         </div>
src/components/base-ui/BookStory/bookstory_card.tsx (2)

18-30: Handle invalid date input in timeAgo.

If iso is an invalid date string, new Date(iso).getTime() returns NaN, causing the function to return "NaN일 전". Consider adding validation.

🛡️ Proposed defensive fix
 function timeAgo(iso: string) {
-  const diff = Date.now() - new Date(iso).getTime();
+  const date = new Date(iso);
+  if (isNaN(date.getTime())) return '';
+  const diff = Date.now() - date.getTime();
   const minutes = Math.floor(diff / 60000);

79-88: Add sizes prop to the cover Image.

The Image component uses fill but lacks a sizes prop. This can negatively impact loading performance. Other images in this file correctly specify sizes.

⚡ Proposed fix
           <Image
             src={coverImgSrc}
             alt="bookstory cover"
             fill
             className="object-cover"
+            sizes="336px"
           />
src/components/base-ui/home/list_subscribe_element.tsx (2)

24-32: Fix sizes prop mismatch.

The image container is 32x32 but sizes="42px" is specified. This should match the actual rendered size for optimal image loading.

⚡ Proposed fix
           sizes="42px"
+          sizes="32px"

44-50: Remove redundant color classes.

text-white and text-[color:var(--White,#FFF)] are redundant—both set the same white color.

🧹 Proposed cleanup
-        className="flex px-[17px] py-[8px] justify-center items-center gap-[10px] rounded-[8px] bg-[#9A7A6B] text-white text-[color:var(--White,#FFF)] text-[12px] font-semibold leading-[100%] tracking-[-0.012px] whitespace-nowrap"
+        className="flex px-[17px] py-[8px] justify-center items-center gap-[10px] rounded-[8px] bg-[#9A7A6B] text-white text-[12px] font-semibold leading-[100%] tracking-[-0.012px] whitespace-nowrap"
src/components/base-ui/home/home_bookclub.tsx (1)

32-33: Use Next.js Image component consistently.

This uses a native <img> tag while the rest of the component and codebase uses next/image. For consistency and optimization benefits, consider using Image.

🔄 Proposed fix
-          <img src="logo2.svg" alt="로고" className="mx-auto mb-4 mt-[118px]" />
+          <Image src="/logo2.svg" alt="로고" width={100} height={100} className="mx-auto mb-4 mt-[118px]" />

Note: Adjust width/height values to match the actual logo dimensions.

src/components/base-ui/BookStory/bookstory_text.tsx (1)

21-27: Consider useEffect instead of useLayoutEffect for SSR compatibility.

useLayoutEffect can cause warnings in Next.js SSR environments. Since this is a client component and the height adjustment is non-critical for initial paint, useEffect would work similarly without SSR warnings.

♻️ Optional alternative
-import React, { useCallback, useLayoutEffect, useRef } from 'react';
+import React, { useCallback, useEffect, useRef } from 'react';
...
-  useLayoutEffect(() => {
+  useEffect(() => {
src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx (2)

36-36: onCloseApply prop is unused.

The onCloseApply prop is declared as required but never used within this component. Either remove it from the props or implement the close behavior where needed (e.g., when the apply form is dismissed).

♻️ Option 1: Remove unused prop
 type Props = {
   club: ClubSummary;
   onClickVisit?: (clubId: number) => void;
   onClickApply?: (clubId: number) => void;
 
   onSubmitApply: (clubId: number, reason: string) => void;
-  onCloseApply: () => void;
   applyOpenId: number | null;
 };
 
 export default function SearchClubListItem({
   club,
   onClickVisit,
   onClickApply,
   applyOpenId,
-  onCloseApply,
   onSubmitApply,
 }: Props) {

12-19: Consider extracting PARTICIPANT_KO to a shared constants file.

This mapping is duplicated in search_club_apply_modal.tsx. Extracting it to a shared location (e.g., @/constants/groups.ts or alongside the types in @/types/groups/groups.ts) would reduce duplication and make future updates easier.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad62f6f and fed9583.

⛔ Files ignored due to path filters (42)
  • package-lock.json is excluded by !**/package-lock.json
  • public/ArrowDown.svg is excluded by !**/*.svg
  • public/ArrowTop.svg is excluded by !**/*.svg
  • public/BrownCheck.svg is excluded by !**/*.svg
  • public/CheckBox_No.svg is excluded by !**/*.svg
  • public/CheckBox_Yes.svg is excluded by !**/*.svg
  • public/ClubDefaultImg.svg is excluded by !**/*.svg
  • public/Edit_icon.svg is excluded by !**/*.svg
  • public/GreenCheck.svg is excluded by !**/*.svg
  • public/Lock.svg is excluded by !**/*.svg
  • public/Polygon.svg is excluded by !**/*.svg
  • public/RadioOff.svg is excluded by !**/*.svg
  • public/RadioOn.svg is excluded by !**/*.svg
  • public/Setting_icon.svg is excluded by !**/*.svg
  • public/Unlock.svg is excluded by !**/*.svg
  • public/blank_heart.svg is excluded by !**/*.svg
  • public/booksample.svg is excluded by !**/*.svg
  • public/bookstorycard.svg is excluded by !**/*.svg
  • public/cancle_button.svg is excluded by !**/*.svg
  • public/comment.svg is excluded by !**/*.svg
  • public/default_profile_1.svg is excluded by !**/*.svg
  • public/default_profile_2.svg is excluded by !**/*.svg
  • public/gray_heart.svg is excluded by !**/*.svg
  • public/icon_minus_1.svg is excluded by !**/*.svg
  • public/icon_plus.svg is excluded by !**/*.svg
  • public/icon_plus_1.svg is excluded by !**/*.svg
  • public/logo.svg is excluded by !**/*.svg
  • public/logo2.svg is excluded by !**/*.svg
  • public/news_sample.svg is excluded by !**/*.svg
  • public/news_sample2.png is excluded by !**/*.png
  • public/news_sample3.png is excluded by !**/*.png
  • public/notification.svg is excluded by !**/*.svg
  • public/pencil_icon.svg is excluded by !**/*.svg
  • public/plus.svg is excluded by !**/*.svg
  • public/profile.svg is excluded by !**/*.svg
  • public/profile2.svg is excluded by !**/*.svg
  • public/profile3.svg is excluded by !**/*.svg
  • public/profile4.svg is excluded by !**/*.svg
  • public/profile5.svg is excluded by !**/*.svg
  • public/red_heart.svg is excluded by !**/*.svg
  • public/search.svg is excluded by !**/*.svg
  • public/search_light.svg is excluded by !**/*.svg
📒 Files selected for processing (50)
  • .github/ISSUE_TEMPLATE/custom.md
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/workflows/deploy.yml
  • .github/workflows/pr-merge-cleanup.yml
  • .github/workflows/preview.yml
  • src/app/(main)/layout.tsx
  • src/app/(main)/page.tsx
  • src/app/(main)/ui-test/page.tsx
  • src/app/globals.css
  • src/app/groups/create/layout.tsx
  • src/app/groups/create/page.tsx
  • src/app/groups/groupSearchDummy.ts
  • src/app/groups/layout.tsx
  • src/app/groups/page.tsx
  • src/app/layout.tsx
  • src/app/page.backup.tsx
  • src/components/base-ui/BookStory/bookstory_card.tsx
  • src/components/base-ui/BookStory/bookstory_choosebook.tsx
  • src/components/base-ui/BookStory/bookstory_detail.tsx
  • src/components/base-ui/BookStory/bookstory_text.tsx
  • src/components/base-ui/Group-Create/Chip.tsx
  • src/components/base-ui/Group-Create/StepDot.tsx
  • src/components/base-ui/Group-Search/search_club_apply_modal.tsx
  • src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx
  • src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx
  • src/components/base-ui/Group-Search/search_groupsearch.tsx
  • src/components/base-ui/Group-Search/search_mybookclub.tsx
  • src/components/base-ui/News/news_list.tsx
  • src/components/base-ui/News/recommendbook_element.tsx
  • src/components/base-ui/Profile/mypage_profile.tsx
  • src/components/base-ui/Profile/notification_element.tsx
  • src/components/base-ui/Profile/others_profile.tsx
  • src/components/base-ui/Profile/subscribe_element.tsx
  • src/components/base-ui/Search/search_bookresult.tsx
  • src/components/base-ui/Search/search_recommendbook.tsx
  • src/components/base-ui/Settings/setting_news_list.tsx
  • src/components/base-ui/Settings/setting_report_list.tsx
  • src/components/base-ui/button_without_img.tsx
  • src/components/base-ui/home/NewsBannerSlider.tsx
  • src/components/base-ui/home/home_bookclub.tsx
  • src/components/base-ui/home/list_subscribe.tsx
  • src/components/base-ui/home/list_subscribe_element.tsx
  • src/components/base-ui/home/notification_element.tsx
  • src/components/layout/Header.tsx
  • src/components/layout/NavItem.tsx
  • src/types/groups/groups.ts
  • src/utils/.keep
  • src/utils/groupMapper.ts
  • tailwind.config.js
  • tsconfig.json
💤 Files with no reviewable changes (5)
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/ISSUE_TEMPLATE/custom.md
  • .github/workflows/preview.yml
  • .github/workflows/pr-merge-cleanup.yml
  • .github/workflows/deploy.yml
🧰 Additional context used
🧬 Code graph analysis (14)
src/components/layout/Header.tsx (1)
src/components/layout/NavItem.tsx (1)
  • NavItem (9-28)
src/app/(main)/layout.tsx (1)
src/components/layout/Header.tsx (1)
  • Header (15-97)
src/app/groups/groupSearchDummy.ts (1)
src/app/groups/page.tsx (1)
  • ClubSummary (19-29)
src/components/base-ui/home/list_subscribe.tsx (1)
src/components/base-ui/home/list_subscribe_element.tsx (1)
  • ListSubscribeElement (13-53)
src/components/base-ui/home/notification_element.tsx (1)
src/components/base-ui/Profile/notification_element.tsx (1)
  • NotificationElement (13-50)
src/utils/groupMapper.ts (1)
src/types/groups/groups.ts (2)
  • Category (3-10)
  • ParticipantType (12-18)
src/components/base-ui/Group-Search/search_club_apply_modal.tsx (2)
src/app/groups/page.tsx (1)
  • ClubSummary (19-29)
src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx (1)
  • ClubCategoryTags (36-64)
src/app/groups/page.tsx (2)
src/types/groups/groups.ts (2)
  • ApplyType (1-1)
  • ParticipantType (12-18)
src/app/groups/groupSearchDummy.ts (2)
  • dummyClubs (19-170)
  • mydummyGroup (7-17)
src/app/(main)/ui-test/page.tsx (1)
src/components/base-ui/Settings/setting_report_list.tsx (1)
  • Setting_ReportList (14-65)
src/components/base-ui/Profile/notification_element.tsx (1)
src/components/base-ui/home/notification_element.tsx (1)
  • NotificationElement (13-50)
src/app/(main)/page.tsx (4)
src/components/base-ui/home/home_bookclub.tsx (1)
  • HomeBookclub (12-111)
src/components/base-ui/home/list_subscribe.tsx (1)
  • ListSubscribe (5-31)
src/components/base-ui/home/NewsBannerSlider.tsx (1)
  • NewsBannerSlider (13-46)
src/components/base-ui/BookStory/bookstory_card.tsx (1)
  • BookStoryCard (32-120)
src/app/groups/layout.tsx (1)
src/components/layout/Header.tsx (1)
  • Header (15-97)
src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx (2)
src/app/groups/page.tsx (1)
  • ClubSummary (19-29)
src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx (1)
  • ClubCategoryTags (36-64)
src/app/groups/create/page.tsx (3)
src/types/groups/groups.ts (3)
  • BookCategory (39-39)
  • BOOK_CATEGORIES (21-37)
  • PARTICIPANTS (41-48)
src/components/base-ui/Group-Create/StepDot.tsx (1)
  • StepDot (7-22)
src/components/base-ui/Group-Create/Chip.tsx (1)
  • Chip (14-36)
🔇 Additional comments (23)
src/components/base-ui/home/notification_element.tsx (1)

13-50: Component logic looks good.

The message construction, conditional styling, and layout structure are implemented correctly. The truncation for long messages is a good touch.

src/components/base-ui/Profile/others_profile.tsx (2)

35-38: Fixed pixel widths may break responsive layouts.

The container (w-[734px]) and subscribe button (w-[532px]) use fixed pixel widths. Given the PR objective is to implement responsive adjustments, consider using responsive utilities (e.g., w-full max-w-[734px]) or breakpoint-based classes (t:w-[734px]) to ensure proper rendering on mobile/tablet.

Is this component intended only for desktop views, or should it adapt to mobile/tablet screens?

Also applies to: 88-100


41-48: LGTM on Image implementation.

Good use of Next.js Image with fill, object-cover, and explicit sizes prop for optimal image loading.

src/components/base-ui/Profile/subscribe_element.tsx (1)

15-61: LGTM on the component structure.

Good use of optional chaining for onToggleSubscribe, sensible defaults, and clean conditional styling for the subscribe state toggle.

src/components/base-ui/button_without_img.tsx (2)

25-31: LGTM on toCssColor utility.

Clean helper that handles multiple input formats (var() wrapped, bare CSS variable names, and literal colors). Good defensive coding with the early return for undefined values.


64-87: Button implementation is functional and well-structured.

The disabled state handling, type safety, and composable className approach are all solid. The component provides good flexibility for styling variations.

src/components/base-ui/Group-Create/StepDot.tsx (1)

7-21: LGTM!

The component logic correctly distinguishes between active, done, and pending states. The conditional styling approach is clear and functional.

src/components/layout/NavItem.tsx (1)

9-27: LGTM!

The component is well-structured with clear conditional styling for the active state. The interface is properly typed.

One minor consideration: the fixed w-32 width may cause layout issues if nav labels vary significantly in length. If this becomes a problem, consider w-auto or min-w-[...] for flexibility.

src/components/layout/Header.tsx (1)

34-50: Navigation implementation looks good.

The active state detection logic correctly handles the root path / with an exact match while using startsWith for nested routes. This prevents / from being marked active on all pages.

src/components/base-ui/Settings/setting_news_list.tsx (1)

1-55: LGTM!

The component is well-structured with proper use of Next.js Image component, appropriate accessibility attributes (alt={title}), and clean flexbox layout for responsive content handling.

src/components/base-ui/Group-Create/Chip.tsx (1)

14-35: LGTM!

The Chip component is well-implemented with proper button semantics, disabled state handling, and responsive styling using the t: breakpoint prefix.

src/types/groups/groups.ts (1)

1-70: LGTM!

Well-structured type definitions with proper use of as const assertions, derived types from constant arrays, and bidirectional mappings for label/type translations. This provides a solid foundation for type-safe group-related operations.

src/utils/groupMapper.ts (1)

30-36: LGTM on the mapping functions.

The bidirectional mapping logic is well-structured. Handling "전체" as null for API filtering and the reverse mapping via toCategories are both correct.

src/components/base-ui/BookStory/bookstory_detail.tsx (1)

26-98: LGTM!

The component is well-structured with proper TypeScript typing, sensible defaults, and clean layout logic. The author link derivation and button handling are implemented correctly.

src/components/base-ui/Search/search_recommendbook.tsx (1)

17-71: LGTM on component structure.

Good use of stopPropagation on the like button to prevent card click, proper fallback for missing images, and clean prop handling.

src/app/groups/groupSearchDummy.ts (1)

4-4: No action needed.

ClubSummary is defined only in src/app/groups/page.tsx and does not exist in src/types/groups/groups.ts. The suggested import path is not feasible. Additionally, the referenced pattern (groupMapper.ts) also imports types from page.tsx rather than a types module, so the coupling concern noted applies equally to the reference example.

src/components/base-ui/home/home_bookclub.tsx (1)

77-106: Action buttons lack click handlers.

The "모임 검색하기" and "모임 생성하기" buttons have no onClick handlers and won't navigate or trigger any action. If this is intentional for a future implementation, consider adding TODO comments or passing handler props similar to onSubscribeClick pattern used elsewhere.

src/components/base-ui/Group-Search/search_club_apply_modal.tsx (1)

72-183: LGTM on the modal UI structure.

The modal correctly handles overlay clicks for closing, uses proper accessibility patterns with ESC key support, and implements auto-resizing textarea. The layout is well-structured with proper responsive considerations.

src/components/base-ui/BookStory/bookstory_text.tsx (1)

30-50: LGTM on Tab key handling.

The Tab indentation logic correctly prevents default behavior, calculates the new string with inserted spaces, and uses requestAnimationFrame to properly restore cursor position after the state update.

src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx (1)

36-63: LGTM on the category tag rendering logic.

The deduplication, filtering, sorting, and dynamic styling based on category ranges are well implemented.

src/app/groups/page.tsx (2)

133-140: Verify modal integration after fixing the onSubmit signature mismatch.

The onSubmitApply handler expects (clubId: number, reason: string), but the modal currently calls onSubmit(reason). Once the modal is fixed (see review comment on search_club_apply_modal.tsx), this integration will work correctly.


31-59: LGTM on state management and handlers.

The state setup for search filters and apply modal control is well-structured. The onSubmitApply handler correctly validates input and includes a TODO for API integration.

src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx (1)

191-211: LGTM on the mobile apply form.

The inline form correctly handles the apply flow: character limit enforcement, disabled state when empty, and properly passes both club.clubId and reason to onSubmitApply.

Comment on lines +19 to +29
export interface ClubSummary {
reason(clubId: number, reason: string): void;
clubId: number;
name: string;
profileImageUrl?: string | null; // 없으면 기본 이미지 쓰면 됨
category: number[]; // 복수 가능
public: boolean;
applytype: ApplyType;
region: string;
participantTypes: ParticipantType[];
}
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

Remove the reason method from the ClubSummary interface.

ClubSummary appears to be a data transfer object representing club information. The reason(clubId: number, reason: string): void method declaration on line 20 doesn't belong here—it looks like it was accidentally included. The dummy data in groupSearchDummy.ts confirms that clubs are pure data objects.

🐛 Proposed fix
 export interface ClubSummary {
-  reason(clubId: number, reason: string): void;
   clubId: number;
   name: string;
   profileImageUrl?: string | null;
   category: number[];
   public: boolean;
   applytype: ApplyType;
   region: string;
   participantTypes: ParticipantType[];
 }
📝 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 interface ClubSummary {
reason(clubId: number, reason: string): void;
clubId: number;
name: string;
profileImageUrl?: string | null; // 없으면 기본 이미지 쓰면 됨
category: number[]; // 복수 가능
public: boolean;
applytype: ApplyType;
region: string;
participantTypes: ParticipantType[];
}
export interface ClubSummary {
clubId: number;
name: string;
profileImageUrl?: string | null; // 없으면 기본 이미지 쓰면 됨
category: number[]; // 복수 가능
public: boolean;
applytype: ApplyType;
region: string;
participantTypes: ParticipantType[];
}
🤖 Prompt for AI Agents
In @src/app/groups/page.tsx around lines 19 - 29, Remove the erroneous method
declaration from the ClubSummary interface: delete the line declaring
reason(clubId: number, reason: string): void so ClubSummary only contains data
properties (clubId, name, profileImageUrl, category, public, applytype, region,
participantTypes). Verify there are no consumers expecting a reason method and,
if any exist, refactor them to accept the DTO as data (or move that method to
the appropriate service/class) and update groupSearchDummy.ts to remain valid
with the cleaned interface.

Comment on lines +1 to +2
import React from 'react';
import Image from 'next/image';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing 'use client' directive for interactive component.

This component has an onClick handler (line 57), but lacks the 'use client' directive. In Next.js App Router, event handlers require client components.

🐛 Proposed fix
+'use client';
+
 import React from 'react';
 import Image from 'next/image';
📝 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
import React from 'react';
import Image from 'next/image';
'use client';
import React from 'react';
import Image from 'next/image';
🤖 Prompt for AI Agents
In @src/components/base-ui/BookStory/bookstory_choosebook.tsx around lines 1 -
2, This component (bookstory_choosebook.tsx) is interactive (has an onClick
handler) but lacks the required 'use client' directive; fix it by adding the
exact line 'use client' as the very first line of the file (before any imports)
so the component becomes a client component, and verify that the exported
component/function with the onClick handler continues to only use client-safe
APIs (no server-only modules).

<div className="w-full flex justify-end">
<button
type="button"
onClick={() => onSubmit(reason)}
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

Critical: onSubmit called with wrong arguments.

The onSubmit prop is typed as (club: number, reason: string) => void (line 24), but here it's called with only reason. This will cause the parent handler to receive incorrect arguments.

🐛 Proposed fix
-            onClick={() => onSubmit(reason)}
+            onClick={() => onSubmit(club.clubId, reason)}
📝 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
onClick={() => onSubmit(reason)}
onClick={() => onSubmit(club.clubId, reason)}
🤖 Prompt for AI Agents
In @src/components/base-ui/Group-Search/search_club_apply_modal.tsx at line 169,
onSubmit is typed as (club: number, reason: string) => void but is being called
with only reason; change the call site to pass the club id first (e.g.,
onSubmit(club, reason)) so the parent receives (club: number, reason: string).
Locate the component prop/type that declares the club id (the parameter named
"club" in the prop type) and use that identifier when invoking onSubmit in the
onClick handler.

Comment on lines +49 to +60
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement | null>(null);
// 바깥 클릭하면 닫기
useEffect(() => {
const onDown = (e: MouseEvent) => {
const el = wrapRef.current;
if (!el) return;
if (!el.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', onDown);
return () => document.removeEventListener('mousedown', onDown);
}, []);
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

Bug: wrapRef is never assigned to an element.

The ref wrapRef is created but never attached to any DOM element, so the outside-click detection in the useEffect will never work (el will always be null).

Proposed fix

Attach the ref to the dropdown container:

-<div className="relative">
+<div className="relative" ref={wrapRef}>
   <button
     type="button"
     onClick={() => setOpen((v) => !v)}
     ...
   >
🤖 Prompt for AI Agents
In @src/components/base-ui/Group-Search/search_groupsearch.tsx around lines 49 -
60, The outside-click handler never fires because the ref created with useRef
(wrapRef) is never attached to the DOM; update the component JSX to pass wrapRef
to the dropdown/container element (e.g., add ref={wrapRef} on the element that
wraps the searchable dropdown), ensuring the useEffect's onDown can access
wrapRef.current and call setOpen(false) correctly; keep the existing useEffect,
onDown, useRef, and setOpen logic unchanged.

Comment on lines +1 to +2
import React from 'react';
import Image from 'next/image';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing 'use client' directive for interactive component.

This component has an onClick handler (line 46), but lacks the 'use client' directive. Event handlers require client components in Next.js App Router.

🐛 Proposed fix
+'use client';
+
 import React from 'react';
 import Image from 'next/image';
📝 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
import React from 'react';
import Image from 'next/image';
'use client';
import React from 'react';
import Image from 'next/image';
🤖 Prompt for AI Agents
In @src/components/base-ui/home/list_subscribe_element.tsx around lines 1 - 2,
This component uses an onClick handler but is missing the Next.js client
component directive; add the string "use client" as the very first line of
src/components/base-ui/home/list_subscribe_element.tsx (before any imports) so
the component becomes a client component and its event handler (the onClick in
this file) will work correctly.

Comment on lines +96 to +99
<Image src="/Edit_icon.svg" alt="" width={24} height={24} />
<span className="text-[color:var(--Gray_5,#5C5C5C)] subhead_4 whitespace-nowrap">
프로필 편집
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Undefined utility class subhead_4.

The class subhead_4 (line 97) is not defined in globals.css. The available subhead utilities are: subhead_1, subhead_2, subhead_3, subhead_3_1, subhead_3_2, and subhead_4_1. This will result in the styling not being applied.

🔧 Suggested fix
               <Image src="/Edit_icon.svg" alt="" width={24} height={24} />
-                <span className="text-[color:var(--Gray_5,#5C5C5C)] subhead_4 whitespace-nowrap">
+                <span className="text-[color:var(--Gray_5,#5C5C5C)] subhead_4_1 whitespace-nowrap">
                   프로필 편집
                 </span>
📝 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
<Image src="/Edit_icon.svg" alt="" width={24} height={24} />
<span className="text-[color:var(--Gray_5,#5C5C5C)] subhead_4 whitespace-nowrap">
프로필 편집
</span>
<Image src="/Edit_icon.svg" alt="" width={24} height={24} />
<span className="text-[color:var(--Gray_5,#5C5C5C)] subhead_4_1 whitespace-nowrap">
프로필 편집
</span>
🤖 Prompt for AI Agents
In @src/components/base-ui/Profile/mypage_profile.tsx around lines 96 - 99, The
span uses a non-existent utility class subhead_4 so its styles won't apply;
update the class to one of the defined utilities (e.g., subhead_4_1 or one of
subhead_1/2/3/3_1/3_2 depending on intended size) by replacing subhead_4 in the
span's className, and verify the visual matches design; ensure no other
occurrences of subhead_4 remain (search for "subhead_4") and run the app to
confirm styling.

Comment on lines +1 to +50
import React from 'react';

type NotificationType = 'like' | 'comment';

type NotificationElementProps = {
nickname: string;
type: NotificationType;
commentContent?: string;
timeLabel: string; // "지금", "2분 전"
isLatest?: boolean;
};

export default function NotificationElement({
nickname,
type,
commentContent,
timeLabel,
isLatest = false,
}: NotificationElementProps) {
const textColorClass = isLatest
? 'text-[color:var(--Gray_7,#2C2C2C)]'
: 'text-[color:var(--Gray_3,#BBB)]';

const message =
type === 'like'
? `${nickname}님이 좋아요를 눌렀습니다.`
: `${nickname}님이 댓글을 남겼습니다.${
commentContent ? `: ${commentContent}` : ''
}`;

return (
<div className="flex w-[364px] px-[16px] py-[20px] justify-between items-center border-b border-b-[color:var(--Subbrown_4,#EAE5E2)] bg-[color:var(--White,#FFF)]">
{/* left */}
<div className="flex items-center gap-[12px] min-w-0">
{isLatest ? (
<span className="w-[12px] h-[12px] rounded-full bg-red-500 shrink-0" />
) : (
<span className="w-[12px] h-[12px] shrink-0" />
)}

<p className={`${textColorClass} body_1_2 truncate`}>{message}</p>
</div>

{/* right */}
<span className="shrink-0 text-[color:var(--Gray_3,#BBB)] body_1_2">
{timeLabel}
</span>
</div>
);
}
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

Duplicate component — consolidate into a shared location.

This file is identical to src/components/base-ui/home/notification_element.tsx. Having two copies of the same component violates DRY and creates maintenance burden — any bug fix or enhancement must be applied in both places.

Consider moving this component to a shared location (e.g., src/components/shared/ or src/components/common/) and importing it where needed.

#!/bin/bash
# Verify the files are identical
diff src/components/base-ui/home/notification_element.tsx src/components/base-ui/Profile/notification_element.tsx
echo "Exit code: $? (0 means files are identical)"
🤖 Prompt for AI Agents
In @src/components/base-ui/Profile/notification_element.tsx around lines 1 - 50,
Duplicate NotificationElement component exists in two places; consolidate by
extracting the NotificationElement function (and its props type
NotificationElementProps) into a single shared/common component module, export
it, update all imports that currently reference the duplicated copies to import
the shared NotificationElement, and then remove the redundant duplicate file so
only the shared implementation remains.

Comment on lines +22 to +28
<div
className={`
flex w-[1000px] p-[20px] items-start gap-[40px]
rounded-[8px] border border-[color:var(--Subbrown_4,#EAE5E2)]
bg-[color:var(--White,#FFF)]
${className}
`}
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

Fixed width breaks responsiveness on smaller screens.

The hardcoded w-[1000px] will cause horizontal overflow on mobile and tablet devices. Given the PR objective is to adjust UI for responsive behavior, consider using responsive utilities.

Suggested responsive fix
     <div
       className={`
-        flex w-[1000px] p-[20px] items-start gap-[40px]
+        flex w-full max-w-[1000px] p-[20px] items-start gap-[20px] t:gap-[40px]
         rounded-[8px] border border-[color:var(--Subbrown_4,#EAE5E2)]
         bg-[color:var(--White,#FFF)]
         ${className}
       `}
     >
🤖 Prompt for AI Agents
In @src/components/base-ui/Settings/setting_report_list.tsx around lines 22 -
28, The fixed-width class w-[1000px] in the SettingsReportList container causes
overflow on small screens; replace it with responsive sizing (e.g., use w-full
or max-w-[1000px] plus breakpoint aliases) so the component expands to full
width on mobile and caps at 1000px on larger screens—update the class string in
setting_report_list.tsx (the div with className containing w-[1000px]) to
something like w-full sm:w-[1000px] or w-full max-w-[1000px] (preserving
existing padding, gap, border and ${className}).

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와 모임 검색 UI를 구현하고 반응형 디자인을 적용하는 등 많은 기능이 추가되었네요. 전반적으로 새로운 기능들이 잘 구현되었고, 특히 Tailwind CSS를 활용한 디자인 시스템 구축과 반응형 처리가 인상적입니다. 몇 가지 코드 구조 개선 및 잠재적 버그 수정에 대한 제안을 드립니다. 이 피드백들이 코드의 유지보수성과 안정성을 높이는 데 도움이 되기를 바랍니다. 수고하셨습니다!

<div className="w-full flex justify-end">
<button
type="button"
onClick={() => onSubmit(reason)}

Choose a reason for hiding this comment

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

critical

가입 신청 버튼의 onClick 핸들러에서 onSubmit 함수를 호출할 때 club.clubId가 누락되었습니다. onSubmit 함수의 시그니처는 (clubId: number, reason: string) => void 이므로, 첫 번째 인자로 club.clubId를 전달해야 합니다. 이 부분이 수정되지 않으면 가입 신청 기능이 정상적으로 동작하지 않을 수 있습니다.

Suggested change
onClick={() => onSubmit(reason)}
onClick={() => onSubmit(club.clubId, reason)}

Comment on lines +23 to +593
export default function CreateClubWizardPreview() {
const [step, setStep] = useState(1);

// Step 1
const [clubName, setClubName] = useState("");
const [clubDescription, setClubDescription] = useState("");
const [nameCheck, setNameCheck] = useState<NameCheckState>("idle");
const DuplicationCheckisConfirmed = nameCheck === "available";
const DuplicationCheckisDisabled = !clubName.trim() || nameCheck === "checking" || DuplicationCheckisConfirmed ;

// Step 2
const [profileMode, setProfileMode] = useState<"default" | "upload">("default");
const [selectedImageUrl, setSelectedImageUrl] = useState<string | null>(null);
const [visibility, setVisibility] = useState<"공개" | "비공개" | null>(null);
const fileRef = useRef<HTMLInputElement | null>(null);
// Step 3
const [selectedCategories, setSelectedCategories] = useState<BookCategory[]>([]);
const [selectedParticipants, setSelectedParticipants] = useState<ParticipantLabel[]>([]);
const [activityArea, setActivityArea] = useState("");

// Step 4
const [links, setLinks] = useState<SnsLink[]>([{ label: "", url: "" }]);

const canNext = useMemo(() => {
if (step === 1) return Boolean(clubName.trim() && clubDescription.trim() && nameCheck === "available");
if (step === 2) return Boolean(visibility);
if (step === 3) return selectedCategories.length > 0 && selectedParticipants.length > 0 && activityArea.trim();
if (step === 4) return true; // 선택
return false;
}, [step, clubName, clubDescription,nameCheck ,visibility, selectedCategories, selectedParticipants, activityArea]);

const onPrev = () => setStep((v) => Math.max(1, v - 1));
const onNext = () => setStep((v) => Math.min(4, v + 1));

const fakeCheckName = () => {
if (!clubName.trim()) return;
setNameCheck("checking");
setTimeout(() => {
// 그냥 프리뷰용: 이름에 "중복" 들어가면 duplicate 처리
if (clubName.includes("중복")) setNameCheck("duplicate");
else setNameCheck("available");
}, 500);
};

const pickImage = (file: File) => {
const reader = new FileReader();
reader.onloadend = () => setSelectedImageUrl(reader.result as string);
reader.readAsDataURL(file);
};

const toggleWithLimit = <T,>(arr: T[], item: T, limit: number) => {
if (arr.includes(item)) return arr.filter((x) => x !== item);
if (arr.length >= limit) return arr;
return [...arr, item];
};

const updateLink = (idx: number, patch: Partial<SnsLink>) => {
setLinks((prev) => prev.map((it, i) => (i === idx ? { ...it, ...patch } : it)));
};

const addLinkRow = () => {
setLinks((prev) => [...prev, { label: "", url: "" }]);
};

const removeLinkRow = (idx: number) => {
setLinks((prev) => prev.filter((_, i) => i !== idx));
};

return (
<div className="min-h-screen max-w-[1440px] mx-auto ">


{/* breadcrumb */}
<div className="flex gap-5 px-[10px] py-3 border-b border-Gray-2 t:mx-4 ">
<Link href="/groups" className="shrink-0">
<p className="body_1 t:subhead_4_1 cursor-pointer">모임</p>
</Link>

<Image
className="block t:hidden shrink-0"
src="/Polygon.svg"
alt=""
width={9}
height={9}
/>

<Image
className="hidden t:block shrink-0"
src="/Polygon.svg"
alt=""
width={16}
height={16}
/>

<p className="body_1 t:subhead_4_1">새 모임 생성</p>
</div>


<div className="w-full max-w-[1040px] mx-auto px-6 t:px-10 pt-6 pb-16 ">
{/* step dots */}
<div className="mb-7">
<div className="flex items-center gap-6">
<StepDot n={1} current={step} />
<StepDot n={2} current={step} />
<StepDot n={3} current={step} />
<StepDot n={4} current={step} />
</div>
</div>

{/* 본문 박스 */}
<main className="">

{/* STEP 1 */}
{step === 1 && (
<section>
<h2 className="subhead_4_1 t:subhead_1">
독서 모임 이름을 입력해주세요!
</h2>

<div className="mt-4 flex items-center gap-5">
<input
value={clubName}
onChange={(e) => {
setClubName(e.target.value);
setNameCheck("idle");
}}
placeholder="독서 모임 이름을 입력해주세요."
className="w-full h-[44px] t:h-[56px] rounded-[8px] border border-[#EAE5E2] p-4 outline-none bg-white body_1_3 t:subhead_4_1"
/>

<button
type="button"
onClick={fakeCheckName}
disabled={DuplicationCheckisDisabled}
aria-busy={nameCheck === "checking"}
className={cx(
`
flex justify-center items-center
w-[100px] h-[48px] t:w-[128px] t:h-[56px]
px-1 py-3 rounded-[8px]
body_1_2
hover:opacity-90 active:opacity-80
disabled:opacity-50 disabled:cursor-not-allowed
`,
DuplicationCheckisConfirmed
? "bg-primary-1 text-White border border-primary-1 disabled:opacity-100 disabled:cursor-default"
: "border border-Subbrown-3 bg-Subbrown-4 text-primary-3"
)}
>
{nameCheck === "checking" ? "확인중" : "중복확인"}
</button>

</div>

<div className="body_2_3 t:body_1_3 text-Gray-3 mt-3">
다른 이름을 입력하거나, 기수 또는 지역명을 추가해 구분해주세요.
<br />
예) 독서재량 2기, 독서재량 서울, 북적북적 인문학팀
</div>

<div className="mt-1 text-[12px]">
{nameCheck === "available" && (
<p className="text-[#367216] body_1_4">사용 가능한 모임 이름입니다.</p>
)}
{nameCheck === "duplicate" && (
<p className="text-[#FF8045] body_1_4">이미 존재하는 모임 이름입니다.</p>
)}
</div>

<h2 className="mt-[56px] subhead_1 subhead_4_1 t:subhead_1">
모임의 소개글을 입력해주세요!
</h2>
<textarea
value={clubDescription}
onChange={(e) => {
setClubDescription(e.target.value);
autoResize(e.currentTarget);
}}
onInput={(e) => autoResize(e.currentTarget)} // 초기/붙여넣기 대응
placeholder="자유롭게 입력해주세요! (500자 제한)"
className="
w-full
min-h-[200px] t:min-h-[260px]
rounded-[8px]
border border-Subbrown-4
bg-White
p-5 mt-5
no-scrollbar
resize-none
outline-none
Body_1_3
placeholder:text-Gray-3
"
/>
<div className="mt-6 flex justify-end">
<button
type="button"
onClick={onNext}
disabled={!canNext}
className={cx(
"flex justify-center items-center gap-[10px] h-[48px] px-4 py-3 rounded-[8px]",
"w-full t:w-[148px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
다음
</button>

</div>
</section>
)}
{/* STEP 2 */}
{step === 2 && (
<section>
<h2 className="subhead_4_1 t:subhead_1 text-Gray-7">
모임의 프로필 사진을 업로드 해주세요!
</h2>

<div className="mt-4 flex items-start gap-6">
{/* profile preview */}
<div className="relative w-[96px] h-[80px] t:w-[194px] t:h-[162px] rounded-[10px] overflow-hidden bg-Subbrown-4 flex items-center justify-center">
{selectedImageUrl ? (
<img
src={selectedImageUrl}
alt="preview"
className="w-full h-full object-cover"
/>
) : (
<>
{/* mobile default */}
<Image
src="/default_profile_2.svg"
alt="기본 프로필"
fill
sizes="96px"
className="object-contain t:hidden"
priority
/>
{/* t~ desktop default */}
<Image
src="/default_profile_1.svg"
alt="기본 프로필"
fill
sizes="194px"
className="object-contain hidden t:block"
priority
/>
</>
)}
</div>

{/* action buttons */}
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => {
setSelectedImageUrl(null);
setProfileMode("default");
}}
className={cx(
"flex justify-center items-center gap-[10px] w-[200px] h-[36px] px-4 py-3 rounded-[8px] border body_1_3",
"hover:opacity-90 active:opacity-80",
profileMode === "default"
? "bg-primary-1 border-primary-1 text-White"
: "bg-Subbrown-4 border-Subbrown-3 text-primary-3"
)}
>
기본 프로필 사용하기
</button>

<button
type="button"
onClick={() => {
setProfileMode("upload");
fileRef.current?.click();
}}
className={cx(
"flex justify-center items-center gap-[10px] w-[200px] h-[36px] px-4 py-3 rounded-[8px] border body_1_3",
"hover:opacity-90 active:opacity-80",
profileMode === "upload"
? "bg-primary-1 border-primary-1 text-White"
: "bg-Subbrown-4 border-Subbrown-3 text-primary-3"
)}
>
사진 업로드하기
</button>

<input
ref={fileRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) {
setProfileMode("upload");
pickImage(f);
}
}}
/>
</div>
</div>

<h2 className="mt-7 subhead_4_1 t:subhead_1 text-Gray-7">
모임의 공개여부를 알려주세요!
</h2>

<div className="mt-4 flex flex-col gap-3 text-Gray-7">
<button
type="button"
onClick={() => setVisibility("공개")}
className="flex items-center gap-3 cursor-pointer select-none text-left"
>
<Image
src={visibility === "공개" ? "/CheckBox_Yes.svg" : "/CheckBox_No.svg"}
alt=""
width={24}
height={24}
/>
<span className="subhead_4_1">공개</span>
</button>

<button
type="button"
onClick={() => setVisibility("비공개")}
className="flex items-center gap-3 cursor-pointer select-none text-left"
>
<Image
src={visibility === "비공개" ? "/CheckBox_Yes.svg" : "/CheckBox_No.svg"}
alt=""
width={24}
height={24}
/>
<span className="subhead_4_1">비공개</span>
</button>
</div>

{/* bottom buttons: mobile = 다음 풀폭, t~ = 이전/다음 오른쪽 정렬 */}
<div className="mt-7 flex justify-end t:gap-3">
<button
type="button"
onClick={onPrev}
className={cx(
"hidden t:flex justify-center items-center gap-[10px] w-[148px] h-[48px] px-4 py-3 rounded-[8px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
이전
</button>

<button
type="button"
onClick={onNext}
disabled={!canNext}
className={cx(
"flex justify-center items-center gap-[10px] h-[48px] px-4 py-3 rounded-[8px]",
"w-full t:w-[148px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
다음
</button>
</div>
</section>
)}
{/* STEP 3 */}
{step === 3 && (
<section>
<h2 className="subhead_4_1 t:subhead_1">
선호하는 독서 카테고리를 선택해주세요!{" "}
<span className="hidden t:box Gray_4 caption_1_1">(최대 6개)</span>
</h2>

<div className="mt-4 flex flex-wrap gap-2.5">
{BOOK_CATEGORIES.map((c) => (
<Chip
key={c}
label={c}
selected={selectedCategories.includes(c)}
onClick={() => setSelectedCategories((prev) => toggleWithLimit(prev, c, 6))}
/>
))}
</div>

<h2 className="mt-10 text-[18px] font-semibold text-[#2C2C2C]">활동 지역을 입력해주세요!</h2>
<input
value={activityArea}
onChange={(e) => setActivityArea(e.target.value)}
placeholder="활동 지역을 입력해주세요 (40자 제한)"
className="mt-4 w-full h-[44px] t:h-[56px] rounded-[8px] border border-[#EAE5E2] body_1_3 bg-white px-4 outline-none"
/>

<h2 className="mt-10 text-[18px] font-semibold text-[#2C2C2C]">
모임의 대상을 선택해주세요! <span className="text-[12px] text-[#8D8D8D]">(최대 6개)</span>
</h2>

<div className="mt-4 flex flex-wrap gap-2.5">
{PARTICIPANTS.map((p) => (
<Chip
key={p}
label={p}
selected={selectedParticipants.includes(p)}
onClick={() => setSelectedParticipants((prev) => toggleWithLimit(prev, p, 6))}
/>
))}
</div>


{/* bottom buttons: mobile = 다음 풀폭, t~ = 이전/다음 오른쪽 정렬 */}
<div className="mt-7 flex justify-end t:gap-3">
<button
type="button"
onClick={onPrev}
className={cx(
"hidden t:flex justify-center items-center gap-[10px] w-[148px] h-[48px] px-4 py-3 rounded-[8px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
이전
</button>

<button
type="button"
onClick={onNext}
disabled={!canNext}
className={cx(
"flex justify-center items-center gap-[10px] h-[48px] px-4 py-3 rounded-[8px]",
"w-full t:w-[148px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
다음
</button>
</div>
</section>
)}

{/* STEP 4 */}
{step === 4 && (
<section>
<h2 className="subhead_4_1 t:subhead_1 mb-4">
SNS나 링크 연동이 있다면 해주세요! (선택)
</h2>

<div className="">
{links.map((it, idx) => (
<div
key={idx}
className="
flex flex-col gap-4 py-3
t:flex-row t:items-center
"
>
<input
value={it.label}
onChange={(e) => updateLink(idx, { label: e.target.value })}
placeholder="링크 대체 텍스트 입력(최대 20자)"
maxLength={20}
className="
w-full t:w-[35%] h-[44px] t:h-[56px]
rounded-[8px]
border border-Subbrown-4
bg-White
px-4
outline-none
body_1_3
placeholder:text-Gray-3
"
/>

<div className="flex gap-3 w-full t:flex-1">
<input
value={it.url}
onChange={(e) => updateLink(idx, { url: e.target.value })}
placeholder="링크 입력(최대 100자)"
maxLength={100}
className="
flex-1
h-[44px] t:h-[56px]
rounded-[8px]
border border-Subbrown-4 bg-White
px-4
outline-none
body_1_3
placeholder:text-Gray-3
"
/>

<button
type="button"
onClick={() => removeLinkRow(idx)}
disabled={links.length <= 1}
className="
w-[44px] h-[44px] t:w-[56px] t:h-[56px]
rounded-[8px]
bg-Gray-1
flex items-center justify-center
hover:bg-Gray-2
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-Gray-1
"
title="삭제"
>
<Image
src={"/icon_minus_1.svg"}
alt=""
width={24}
height={24}
/>
</button>
</div>
</div>
))}

<button
type="button"
onClick={addLinkRow}
className="
w-full h-[56px] rounded-[8px]
bg-Gray-1
flex items-center justify-center
hover:bg-Gray-2
"
title="추가"
>
<Image src={"/icon_plus_1.svg"} alt="" width={24} height={24} />
</button>
</div>




<div className="mt-10 flex justify-between">
<button
type="button"
onClick={onPrev}
className={cx(
"hidden t:flex justify-center items-center gap-[10px] w-[148px] h-[48px] px-4 py-3 rounded-[8px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
이전
</button>

<button
type="button"
onClick={onNext}
disabled={!canNext}
className={cx(
"flex justify-center items-center gap-[10px] h-[48px] px-4 py-3 rounded-[8px]",
"w-full t:w-[148px]",
"bg-primary-1 hover:bg-primary-3 text-White",
"disabled:bg-Gray-2 disabled:hover:bg-Gray-2 disabled:cursor-not-allowed"
)}
>
다음
</button>

</div>
</section>
)}
</main>
</div>
</div>
);
}

Choose a reason for hiding this comment

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

high

CreateClubWizardPreview 컴포넌트가 4단계의 모든 상태와 UI를 관리하고 있어 매우 큽니다(약 600줄). 컴포넌트가 비대해지면 가독성이 떨어지고 유지보수가 어려워질 수 있습니다. 각 단계를 별도의 컴포넌트로 분리하는 리팩터링을 제안합니다. 부모 컴포넌트에서 현재 단계를 관리하고, 각 단계 컴포넌트는 필요한 데이터와 상태 변경 함수를 props로 전달받는 구조로 만들면 코드의 모듈성이 향상되고 테스트하기도 용이해집니다.

@@ -0,0 +1,36 @@
import { Category, ParticipantType } from "@/app/groups/page";

Choose a reason for hiding this comment

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

high

CategoryParticipantTypepage.tsx 파일에서 가져오고 있습니다. 타입 정의는 src/types/groups/groups.ts와 같은 중앙 관리 파일에서 가져오는 것이 올바른 의존성 관리 방식입니다. 페이지 컴포넌트에서 타입을 가져오면 불필요한 의존성이 생기고 코드 구조가 복잡해질 수 있습니다.

Suggested change
import { Category, ParticipantType } from "@/app/groups/page";
import { Category, ParticipantType } from '@/types/groups/groups';

Comment on lines +30 to +59
<BookStoryCard
authorName="hy_0716"
createdAt="2026.01.03"
viewCount={302}
title="나는 나이든 왕자다"
content="나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸..."
likeCount={1}
commentCount={1}
subscribeText={"구독"}
/>
<BookStoryCard
authorName="hy_0716"
createdAt="2026.01.03"
viewCount={302}
title="나는 나이든 왕자다"
content="나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸..."
likeCount={1}
commentCount={1}
subscribeText={"구독"}
/>
<BookStoryCard
authorName="hy_0716"
createdAt="2026.01.03"
viewCount={302}
title="나는 나이든 왕자다"
content="나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸..."
likeCount={1}
commentCount={1}
subscribeText={"구독"}
/>

Choose a reason for hiding this comment

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

medium

현재 BookStoryCard 컴포넌트가 세 번 반복되어 사용되고 있습니다. 이는 코드 중복을 유발하고 향후 데이터가 동적으로 변경될 때 수정이 번거로워질 수 있습니다. 데이터를 배열로 관리하고 .map() 함수를 사용하여 렌더링하는 방식으로 리팩터링하는 것을 추천합니다. 이렇게 하면 코드가 더 간결해지고 유지보수하기 쉬워집니다.

const bookStories = [
  {
    authorName: "hy_0716",
    createdAt: "2026.01.03",
    viewCount: 302,
    title: "나는 나이든 왕자다",
    content: "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...",
    likeCount: 1,
    commentCount: 1,
    subscribeText: "구독",
  },
  {
    authorName: "hy_0716",
    createdAt: "2026.01.03",
    viewCount: 302,
    title: "나는 나이든 왕자다",
    content: "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...",
    likeCount: 1,
    commentCount: 1,
    subscribeText: "구독",
  },
  {
    authorName: "hy_0716",
    createdAt: "2026.01.03",
    viewCount: 302,
    title: "나는 나이든 왕자다",
    content: "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...",
    likeCount: 1,
    commentCount: 1,
    subscribeText: "구독",
  },
];

// ...

<div className="mt-6 grid grid-cols-3 gap-5">
  {bookStories.map((story, index) => (
    <BookStoryCard key={index} {...story} />
  ))}
</div>

Comment on lines +73 to +77
const toggleWithLimit = <T,>(arr: T[], item: T, limit: number) => {
if (arr.includes(item)) return arr.filter((x) => x !== item);
if (arr.length >= limit) return arr;
return [...arr, item];
};

Choose a reason for hiding this comment

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

medium

toggleWithLimit 함수는 컴포넌트의 상태에 의존하지 않는 순수 함수입니다. 이런 유틸리티 함수는 컴포넌트 외부에 정의하거나, 여러 곳에서 사용될 가능성이 있다면 별도의 유틸리티 파일(src/utils/array.ts 등)로 분리하는 것이 좋습니다. 이렇게 하면 컴포넌트의 핵심 로직에 더 집중할 수 있고, 함수를 다른 곳에서도 재사용하기 용이해집니다.

Comment on lines +19 to +29
export interface ClubSummary {
reason(clubId: number, reason: string): void;
clubId: number;
name: string;
profileImageUrl?: string | null; // 없으면 기본 이미지 쓰면 됨
category: number[]; // 복수 가능
public: boolean;
applytype: ApplyType;
region: string;
participantTypes: ParticipantType[];
}

Choose a reason for hiding this comment

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

medium

ClubSummary 인터페이스가 페이지 컴포넌트 파일 내에 정의되어 있습니다. 여러 컴포넌트에서 사용될 수 있는 타입 정의는 src/types/groups/groups.ts와 같은 중앙의 타입 정의 파일로 옮기는 것이 좋습니다. 이렇게 하면 타입의 재사용성이 높아지고, 프로젝트 전체의 타입 관리가 용이해집니다.

@shinwokkang shinwokkang merged commit 7642973 into main Jan 14, 2026
1 check 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.

5 participants