Skip to content

feat: 독서모임 생성을 위한 모임 만들기 페이지 구현#32

Merged
ljh130334 merged 19 commits into
developfrom
feat/makeroom
Jul 10, 2025
Merged

feat: 독서모임 생성을 위한 모임 만들기 페이지 구현#32
ljh130334 merged 19 commits into
developfrom
feat/makeroom

Conversation

@ljh130334

@ljh130334 ljh130334 commented Jul 8, 2025

Copy link
Copy Markdown
Member

#️⃣연관된 이슈

지라에 등록을 안해서..^^ 죄송죄송

📝작업 내용

독서모임 생성을 위한 모임 만들기 페이지를 구현했습니다.

주요 구현 기능

1. 책 선택 섹션 (BookSelectionSection)

  • 검색을 통한 책 선택 기능
  • 선택된 책 정보 표시 (표지, 제목, 저자)
  • 책 변경 버튼

2. 장르 선택 섹션 (GenreSelectionSection)

  • 5개 장르 중 1개 선택 (문학, 과학·IT, 사회과학, 인문학, 예술)
  • 선택된 장르 하이라이트 표시
  • 선택 가이드 메시지

3. 방 정보 입력 섹션 (RoomInfoSection)

  • 방 제목 입력 (최대 15자)
  • 한 줄 소개 입력 (최대 75자)
  • 실시간 글자 수 카운터

4. 활동 기간 설정 섹션 (ActivityPeriodSection)

  • 커스텀 휠 피커를 통한 시작일/종료일 선택
  • 최대 3개월(90일) 제한 검증
  • 종료일이 시작일보다 빠른 경우 에러 처리
  • 날짜 유효성 검사 및 자동 조정

5. 인원 제한 섹션 (MemberLimitSection)

  • 1-30명 범위에서 휠 피커로 선택

6. 공개 설정 섹션 (PrivacySettingSection)

  • 토글 스위치를 통한 비공개 설정

기술적 구현 사항

커스텀 날짜 휠 피커 (DateWheel)

  • 3D perspective 효과를 활용한 iOS 스타일 휠 피커
  • 터치/마우스 드래그, 휠 스크롤 지원
  • 년/월/일별 독립적인 휠 컴포넌트
  • 매끄러운 애니메이션과 선택 피드백
    (SPURT 코드를 조금 참고했음..^^)

폼 검증 로직

  • 필수 항목: 책 선택, 장르 선택
  • 날짜 범위 검증 (최대 90일, 시작일 ≤ 종료일)
  • 실시간 완료 버튼 활성화 상태 관리

스크린샷 (선택)

2025-07-09.2.39.52.mov

💬리뷰 요구사항(선택)

휠 피커의 스크린 리더 지원과 키보드 네비게이션 개선이 필요한지 검토해주세요.

Summary by CodeRabbit

  • 신규 기능

    • 그룹 생성 페이지(/group/create)가 추가되어, 사용자가 독서 모임을 직접 생성할 수 있습니다.
    • 책 검색 및 선택, 장르 선택, 방 정보 입력(제목/설명), 활동 기간 설정, 인원 제한, 공개/비공개 설정(비밀번호 입력) 등 다양한 그룹 설정 기능이 제공됩니다.
    • 날짜 및 인원 선택을 위한 3D 휠 UI, 비공개 방 비밀번호 숫자 입력, 책 검색 바텀시트 등 다양한 인터랙티브 UI가 추가되었습니다.
  • 스타일

    • 그룹 생성 및 관련 컴포넌트에 맞춘 다양한 스타일 요소가 새롭게 적용되었습니다.
  • 기타

    • react-datepicker 및 타입 정의 패키지가 추가되었습니다.

@coderabbitai

coderabbitai Bot commented Jul 8, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

## Walkthrough

이번 변경사항은 그룹 생성 페이지 및 관련 UI 컴포넌트의 신규 도입, 도서 검색 바텀시트, 장르/인원/기간/비공개 등 다양한 그룹 설정 섹션, 3D 휠 셀렉터, 스타일드 컴포넌트 도입, 라우트 추가, 그리고 필요한 라이브러리 의존성 추가로 구성되어 있습니다.

## Changes

| 파일 또는 경로 | 변경 요약 |
|---|---|
| package.json | `react-datepicker``@types/react-datepicker` 의존성 추가 |
| src/components/common/BookSearchBottomSheet/* | 도서 검색 바텀시트 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/CreateGroup.tsx, CreateGroup.styled.ts | 그룹 생성 페이지 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/CommonSection.styled.ts | 공통 섹션 스타일 컴포넌트 추가 |
| src/pages/group/components/BookSelectionSection.* | 도서 선택 섹션 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/components/GenreSelectionSection.* | 장르 선택 섹션 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/components/RoomInfoSection.* | 방 정보 입력 섹션 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/components/ActivityPeriodSection/* | 활동 기간 섹션, 3D 휠 셀렉터 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/components/MemberLimitSection.* | 인원 제한 섹션 컴포넌트 및 스타일 컴포넌트 신규 추가 |
| src/pages/group/components/PrivacySettingSection/* | 비공개 설정 토글, 비밀번호 입력 섹션 및 스타일 컴포넌트 신규 추가 |
| src/pages/index.tsx | `/group/create` 경로에 대한 라우트 추가 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant CreateGroupPage
    participant BookSearchBottomSheet
    participant SubSections

    User->>CreateGroupPage: 접속 및 폼 입력
    User->>BookSearchBottomSheet: 도서 검색 버튼 클릭
    BookSearchBottomSheet-->>User: 도서 리스트 및 검색 제공
    User->>BookSearchBottomSheet: 도서 선택
    BookSearchBottomSheet->>CreateGroupPage: 선택 도서 전달
    loop 설정 섹션
      User->>SubSections: 장르/기간/인원/비공개 등 설정
    end
    User->>CreateGroupPage: 완료 버튼 클릭
    CreateGroupPage-->>User: (추후) 그룹 생성 처리

Possibly related PRs

Suggested reviewers

  • ho0010

Poem

🐰
새로운 그룹 만들기, 토끼도 신났네!
바텀시트에서 책도 고르고,
장르와 인원, 기간도 돌려보고,
비공개 비번도 네 자리로 쏙!
스타일도 예쁘게, 코드도 반짝,
우리 모두 함께 읽는 모임,
이젠 시작만 남았어요!
📚✨


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 41bc1b155cd06fed34d606efc0c1139ee4bad542 and 989d620f63de25a98a18a1cfb7a09a74e4596a31.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/pages/index.tsx` (2 hunks)

</details>

</details>
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=THIP-TextHip/THIP-Web&utm_content=32):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@ljh130334 ljh130334 added ✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링 labels Jul 8, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (16)
src/pages/index.tsx (2)

12-12: 임포트 순서를 개선하세요.

관련 기능별로 임포트를 그룹화하는 것이 좋습니다.

 import Login from './login/Login';
 import Signup from './signup/Signup';
 import SignupGenre from './signup/SignupGenre';
 import SignupNickname from './signup/SignupNickname';
 import SignupDone from './signup/SignupDone';
+
 import CreateGroup from './group/CreateGroup';

24-24: 라우트 구조를 더 명확하게 구성하세요.

라우트 경로를 일관성 있게 구성하고, 그룹 관련 라우트들을 함께 묶는 것을 고려해보세요.

         <Route path="signupdone" element={<SignupDone />} />
-        <Route path="group/create" element={<CreateGroup />} />
+        <Route path="group">
+          <Route path="create" element={<CreateGroup />} />
+        </Route>
src/pages/group/CommonSection.styled.ts (1)

4-11: 조건부 스타일링 방식을 개선하세요.

템플릿 리터럴 대신 css 헬퍼를 사용하여 더 명확한 코드를 작성하는 것이 좋습니다.

+import styled, { css } from '@emotion/styled';
 import { colors, typography, semanticColors } from '../../styles/global/global';

 export const Section = styled.div<{ showDivider?: boolean }>`
   margin-bottom: 32px;
-  ${({ showDivider }) =>
-    showDivider &&
-    `
-    border-bottom: 1px solid ${colors.darkgrey.dark};
-  `}
+  
+  ${({ showDivider }) =>
+    showDivider &&
+    css`
+      border-bottom: 1px solid ${colors.darkgrey.dark};
+    `}
 `;
src/pages/group/components/GenreSelectionSection.styled.ts (1)

10-21: 호버 상태 및 비활성화 상태 스타일링 추가 고려

현재 활성/비활성 상태만 구현되어 있는데, 사용자 경험 향상을 위해 호버 상태와 포커스 상태 스타일링을 추가하는 것을 권장합니다:

export const GenreButton = styled.button<{ active: boolean }>`
  background-color: ${({ active }) =>
    active ? semanticColors.button.fill.primary : colors.grey[400]};
  border: none;
  border-radius: 20px;
  padding: 8px 12px;
  color: ${semanticColors.text.primary};
  font-size: ${typography.fontSize.xs};
  font-weight: ${typography.fontWeight.regular};
  cursor: pointer;
  transition: all 0.2s;
+
+  &:hover:not(:disabled) {
+    opacity: 0.8;
+  }
+
+  &:focus-visible {
+    outline: 2px solid ${semanticColors.button.fill.primary};
+    outline-offset: 2px;
+  }
+
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
`;
src/pages/group/components/MemberLimitSection.tsx (1)

23-28: DateWheel 컴포넌트 재사용이 창의적임

날짜 선택용 휠 컴포넌트를 숫자 선택에 재사용한 것이 창의적이고 효율적입니다. 하지만 컴포넌트명이 DateWheel인 점이 혼란을 야기할 수 있습니다.

향후 재사용성을 고려하여 더 범용적인 이름으로 변경하는 것을 고려해보세요:

  • NumberWheel 또는 SelectionWheel로 리네이밍
  • 또는 DateWheelWheelPicker로 일반화
src/pages/group/components/BookSelectionSection.tsx (3)

16-16: 타입 안전성을 위해 Book 인터페이스를 정의하세요.

인라인 객체 타입 대신 명시적인 Book 인터페이스를 정의하여 타입 안전성을 높이고 재사용성을 개선할 수 있습니다.

+interface Book {
+  cover: string;
+  title: string;
+  author: string;
+}
+
 interface BookSelectionSectionProps {
-  selectedBook: { cover: string; title: string; author: string } | null;
+  selectedBook: Book | null;
   onSearchClick: () => void;
   onChangeClick: () => void;
 }

37-37: 접근성을 위해 이미지 alt 텍스트를 개선하세요.

스크린 리더 사용자를 위해 더 구체적인 alt 텍스트를 제공하는 것이 좋습니다.

- <img src={selectedBook.cover} alt={selectedBook.title} />
+ <img src={selectedBook.cover} alt={`${selectedBook.title} 책 표지`} />

51-51: 일관성을 위해 인라인 스타일을 styled-components로 이동하세요.

다른 컴포넌트들과 스타일 패턴을 일관되게 유지하기 위해 인라인 스타일을 styled-components로 이동하는 것이 좋습니다.

BookSelectionSection.styled.ts 파일에 추가:

export const SearchText = styled.span`
  color: ${semanticColors.text.secondary};
`;

그리고 컴포넌트에서:

- <span style={{ color: semanticColors.text.secondary }}>검색해서 찾기</span>
+ <SearchText>검색해서 찾기</SearchText>
src/pages/group/components/GenreSelectionSection.tsx (2)

11-11: 성능 최적화를 위해 genres 배열을 컴포넌트 외부로 이동하세요.

컴포넌트가 리렌더링될 때마다 genres 배열이 재생성되는 것을 방지하기 위해 컴포넌트 외부에 정의하는 것이 좋습니다.

+ const GENRES = ['문학', '과학·IT', '사회과학', '인문학', '예술'] as const;
+
 const GenreSelectionSection = ({ selectedGenre, onGenreSelect }: GenreSelectionSectionProps) => {
-  const genres = ['문학', '과학·IT', '사회과학', '인문학', '예술'];

   return (
     <Section>
       <SectionTitle>책 장르</SectionTitle>
       <GenreButtonGroup>
-        {genres.map(genre => (
+        {GENRES.map(genre => (

27-36: 일관성을 위해 인라인 스타일을 styled-components로 이동하세요.

다른 컴포넌트들과의 스타일 패턴 일관성을 위해 인라인 스타일을 styled-components로 이동하는 것이 좋습니다.

GenreSelectionSection.styled.ts 파일에 추가:

export const GenreMessage = styled.div`
  color: ${semanticColors.text.point.green};
  font-size: ${typography.fontSize.xs};
  margin-top: 12px;
  text-align: right;
`;

그리고 컴포넌트에서:

-      <div
-        style={{
-          color: semanticColors.text.point.green,
-          fontSize: typography.fontSize.xs,
-          marginTop: '12px',
-          textAlign: 'right',
-        }}
-      >
+      <GenreMessage>
         {selectedGenre ? '1개만 선택 가능합니다.' : '책을 가장 잘 설명하는 장르를 하나 골라주세요.'}
-      </div>
+      </GenreMessage>
src/pages/group/CreateGroup.tsx (1)

22-23: 동적 기본 날짜 값을 사용하세요.

하드코딩된 날짜 값 대신 현재 날짜를 기반으로 한 동적 기본값을 사용하는 것이 좋습니다.

+ const getCurrentDate = () => {
+   const today = new Date();
+   return {
+     year: today.getFullYear(),
+     month: today.getMonth() + 1,
+     day: today.getDate()
+   };
+ };
+
- const [startDate, setStartDate] = useState({ year: 2025, month: 1, day: 1 });
- const [endDate, setEndDate] = useState({ year: 2025, month: 1, day: 1 });
+ const [startDate, setStartDate] = useState(getCurrentDate());
+ const [endDate, setEndDate] = useState(getCurrentDate());
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)

40-83: 모의 데이터의 중복 항목들을 확인하세요.

데모 목적으로 보이지만, 모의 데이터에 동일한 책들이 중복되어 있습니다 (예: '토마토 컵라면', '사슴', '호르몬 체인지'). 실제 구현 시에는 고유한 데이터를 사용해야 합니다.

src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.tsx (1)

122-154: 날짜 변경 시 재검증 로직을 개선할 수 있습니다.

각 핸들러에서 종료일 재검증 로직이 중복되고 있습니다. 이를 별도 함수로 추출하여 중복을 줄일 수 있습니다.

다음과 같이 공통 재검증 로직을 추출할 수 있습니다:

+ const revalidateEndDate = () => {
+   const adjustedEndDate = validateAndAdjustDate(endDate, true);
+   if (JSON.stringify(adjustedEndDate) !== JSON.stringify(endDate)) {
+     onEndDateChange(adjustedEndDate);
+   }
+ };

  const handleStartYearChange = (year: number) => {
    const newDate = validateAndAdjustDate({ ...startDate, year });
    onStartDateChange(newDate);
-   
-   // 종료일도 재검증
-   const adjustedEndDate = validateAndAdjustDate(endDate, true);
-   if (JSON.stringify(adjustedEndDate) !== JSON.stringify(endDate)) {
-     onEndDateChange(adjustedEndDate);
-   }
+   revalidateEndDate();
  };
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (3)

4-17: Overlay에 어두운 배경 컬러가 없어 시각적 깊이가 부족합니다
backdrop-filter 만으로는 배경이 흰 화면일 때 거의 효과가 보이지 않습니다. rgba(0,0,0,.4) 정도의 반투명 배경을 추가하면 모달 구분이 명확해집니다.

   backdrop-filter: blur(2px);
+  background-color: rgba(0, 0, 0, 0.4);

146-160: 스크롤바 커스텀 시 margin-right: -16px 트릭은 Safari 에서 문제될 수 있습니다
음수 마진은 일부 브라우저에서 클릭 영역 오프셋을 야기합니다. 가능하면 padding-inline-end 로 여유 공간을 확보하거나 scrollbar-gutter: stable 사용을 검토하세요.


63-76: SearchInput 기본 글꼴 크기 단위 혼용 주의
font-size 를 theme 값으로 지정하고 placeholder 에 다시 지정하고 있어 중복입니다. placeholder 는 상속되므로 별도 지정이 필요 없으며, 필요할 경우 inherit 로 통일해 유지보수성을 높이세요.

-  &::placeholder {
-    color: ${semanticColors.text.ghost};
-    font-size: ${typography.fontSize.base};
-  }
+  &::placeholder {
+    color: ${semanticColors.text.ghost};
+    font-size: inherit;
+  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc80a06 and 68d6c4b.

⛔ Files ignored due to path filters (8)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/assets/books/deer.svg is excluded by !**/*.svg
  • src/assets/books/hormone.svg is excluded by !**/*.svg
  • src/assets/books/life.svg is excluded by !**/*.svg
  • src/assets/books/tomato.svg is excluded by !**/*.svg
  • src/assets/group/close.svg is excluded by !**/*.svg
  • src/assets/group/search.svg is excluded by !**/*.svg
  • src/assets/group/search_white.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • package.json (1 hunks)
  • src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1 hunks)
  • src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1 hunks)
  • src/pages/group/CommonSection.styled.ts (1 hunks)
  • src/pages/group/CreateGroup.styled.ts (1 hunks)
  • src/pages/group/CreateGroup.tsx (1 hunks)
  • src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.styled.ts (1 hunks)
  • src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.tsx (1 hunks)
  • src/pages/group/components/ActivityPeriodSection/DateWheel.tsx (1 hunks)
  • src/pages/group/components/ActivityPeriodSection/Wheel.styled.ts (1 hunks)
  • src/pages/group/components/BookSelectionSection.styled.ts (1 hunks)
  • src/pages/group/components/BookSelectionSection.tsx (1 hunks)
  • src/pages/group/components/GenreSelectionSection.styled.ts (1 hunks)
  • src/pages/group/components/GenreSelectionSection.tsx (1 hunks)
  • src/pages/group/components/MemberLimitSection.styled.ts (1 hunks)
  • src/pages/group/components/MemberLimitSection.tsx (1 hunks)
  • src/pages/group/components/PrivacySettingSection.styled.ts (1 hunks)
  • src/pages/group/components/PrivacySettingSection.tsx (1 hunks)
  • src/pages/group/components/RoomInfoSection.styled.ts (1 hunks)
  • src/pages/group/components/RoomInfoSection.tsx (1 hunks)
  • src/pages/index.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (14)
src/pages/group/components/PrivacySettingSection.styled.ts (1)
src/styles/global/global.ts (2)
  • semanticColors (79-152)
  • typography (56-76)
src/pages/group/CommonSection.styled.ts (1)
src/styles/global/global.ts (3)
  • colors (4-53)
  • semanticColors (79-152)
  • typography (56-76)
src/pages/group/components/MemberLimitSection.tsx (2)
src/pages/group/CommonSection.styled.ts (2)
  • Section (4-11)
  • SectionTitle (13-18)
src/pages/group/components/MemberLimitSection.styled.ts (3)
  • MemberLimitContainer (4-9)
  • MemberWheelContainer (11-15)
  • MemberText (17-21)
src/pages/group/components/RoomInfoSection.tsx (2)
src/pages/group/CommonSection.styled.ts (2)
  • Section (4-11)
  • SectionTitle (13-18)
src/pages/group/components/RoomInfoSection.styled.ts (3)
  • TextAreaBox (4-9)
  • TextArea (11-28)
  • CharacterCount (30-35)
src/pages/group/components/RoomInfoSection.styled.ts (1)
src/styles/global/global.ts (2)
  • semanticColors (79-152)
  • typography (56-76)
src/pages/group/components/GenreSelectionSection.styled.ts (1)
src/styles/global/global.ts (3)
  • semanticColors (79-152)
  • colors (4-53)
  • typography (56-76)
src/pages/group/components/PrivacySettingSection.tsx (2)
src/pages/group/CommonSection.styled.ts (2)
  • Section (4-11)
  • SectionTitle (13-18)
src/pages/group/components/PrivacySettingSection.styled.ts (4)
  • PrivacyToggleContainer (4-8)
  • PrivacyLabel (10-14)
  • ToggleSwitch (16-25)
  • ToggleSlider (27-36)
src/pages/group/components/GenreSelectionSection.tsx (3)
src/pages/group/CommonSection.styled.ts (2)
  • Section (4-11)
  • SectionTitle (13-18)
src/pages/group/components/GenreSelectionSection.styled.ts (2)
  • GenreButtonGroup (4-8)
  • GenreButton (10-21)
src/styles/global/global.ts (2)
  • semanticColors (79-152)
  • typography (56-76)
src/pages/group/components/BookSelectionSection.styled.ts (1)
src/styles/global/global.ts (3)
  • semanticColors (79-152)
  • colors (4-53)
  • typography (56-76)
src/pages/group/CreateGroup.tsx (2)
src/pages/group/CreateGroup.styled.ts (1)
  • Container (4-14)
src/pages/group/CommonSection.styled.ts (1)
  • Section (4-11)
src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.tsx (2)
src/pages/group/CommonSection.styled.ts (2)
  • Section (4-11)
  • SectionTitle (13-18)
src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.styled.ts (7)
  • DatePickerContainer (4-6)
  • DateRangeContainer (8-15)
  • DateGroup (17-23)
  • DateUnitText (92-97)
  • SeparatorText (99-106)
  • ErrorText (116-122)
  • InfoText (108-114)
src/pages/group/components/ActivityPeriodSection/Wheel.styled.ts (2)
src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.styled.ts (1)
  • WheelContainer (25-31)
src/styles/global/global.ts (3)
  • colors (4-53)
  • typography (56-76)
  • semanticColors (79-152)
src/pages/group/components/ActivityPeriodSection/DateWheel.tsx (2)
src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.styled.ts (1)
  • WheelContainer (25-31)
src/pages/group/components/ActivityPeriodSection/Wheel.styled.ts (7)
  • WheelContainer (4-12)
  • WheelShadowTop (32-42)
  • WheelInner (14-24)
  • WheelSlides (26-30)
  • WheelSlide (67-82)
  • WheelLabel (57-65)
  • WheelShadowBottom (44-55)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
src/styles/global/global.ts (3)
  • colors (4-53)
  • semanticColors (79-152)
  • typography (56-76)
🪛 ESLint
src/pages/group/CreateGroup.tsx

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

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


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

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

src/pages/group/components/ActivityPeriodSection/DateWheel.tsx

[error] 35-35: 'slideDegree' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 36-36: 'slidesPerView' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 150-150: 'offset' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🔇 Additional comments (20)
package.json (1)

19-19: 보안 취약점 및 버전 호환성 직접 검증이 필요합니다

현재 npm audit를 위한 lockfile 생성이 실패하여 자동 검사 결과를 확인할 수 없습니다. 아래 사항을 직접 검토해주세요.

  • package-lock.json 또는 yarn.lock을 정상 생성한 뒤 다시 npm audit 또는 yarn audit를 실행
  • react-datepicker@8.4.0가 최신 버전인지 npm view react-datepicker version 또는 공식 GitHub 릴리즈 페이지에서 확인
  • 해당 버전에 보고된 보안 취약점(CVE)이나 릴리즈 노트상의 breaking change 여부 검토
src/pages/group/CommonSection.styled.ts (1)

13-18: LGTM!

SectionTitle 컴포넌트는 일관된 스타일을 적용하고 있으며, 전역 스타일 시스템을 잘 활용하고 있습니다.

src/pages/group/components/PrivacySettingSection.tsx (1)

9-12: 인터페이스 정의가 명확하고 적절함

Props 인터페이스가 명확하게 정의되어 있고, 불필요한 복잡성 없이 단순하게 구현되어 있습니다.

src/pages/group/components/GenreSelectionSection.styled.ts (1)

4-8: 잘 구조화된 버튼 그룹 레이아웃

Flexbox를 사용한 버튼 그룹 레이아웃이 적절하게 구현되어 있습니다. flex-wrap: wrapgap: 12px를 통해 반응형 레이아웃을 지원합니다.

src/pages/group/components/MemberLimitSection.styled.ts (1)

4-21: 깔끔하고 일관된 스타일링 구현

Flexbox를 활용한 레이아웃이 명확하고 의미있게 구성되어 있습니다. 각 컨테이너의 역할이 명확하게 분리되어 있고, 전역 디자인 시스템을 일관되게 사용하고 있습니다.

src/pages/group/components/RoomInfoSection.styled.ts (3)

4-9: 적절한 컨테이너 스타일링

투명한 배경과 최소한의 스타일링으로 컨테이너를 구성한 것이 적절합니다. 불필요한 스타일링을 피하고 기본 레이아웃만 제공하는 좋은 접근법입니다.


11-28: 브라우저 기본 스타일 초기화가 잘 구현됨

textarea의 기본 스타일을 완전히 제거하고 커스텀 스타일을 적용한 것이 적절합니다. 특히 resize: noneoutline: none 처리, 그리고 플레이스홀더 스타일링이 잘 되어 있습니다.


30-35: 문자 수 표시 스타일링이 직관적임

녹색 포인트 컬러와 우측 정렬을 통해 사용자가 문자 수 제한을 쉽게 인지할 수 있도록 구현되어 있습니다.

src/pages/group/components/MemberLimitSection.tsx (1)

14-16: 효율적인 배열 생성 구현

Array.from을 사용한 1-30 범위 배열 생성이 깔끔하고 직관적입니다. 성능상 문제없는 구현입니다.

src/pages/group/components/RoomInfoSection.tsx (1)

11-50: 잘 구현된 컴포넌트입니다.

텍스트 영역 입력 처리, 문자 제한, 실시간 문자 카운트 표시가 모두 올바르게 구현되어 있습니다. 컴포넌트 구조도 깔끔하고 React 모범 사례를 잘 따르고 있습니다.

src/pages/group/components/ActivityPeriodSection/Wheel.styled.ts (1)

4-82: 3D 휠 UI를 위한 스타일 컴포넌트가 잘 구현되었습니다.

3D 변환, 원근감, 그림자 효과, z-index 관리가 모두 적절하게 구현되어 있습니다. 복잡한 3D 인터페이스를 위한 스타일 구조가 체계적으로 잘 조직되어 있습니다.

src/pages/group/CreateGroup.tsx (1)

62-62: 폼 유효성 검사 로직이 올바르게 구현되었습니다.

책 선택 또는 책 제목 입력과 장르 선택을 모두 확인하는 검증 로직이 적절하게 구현되어 있습니다.

src/pages/group/components/BookSelectionSection.styled.ts (1)

1-84: 잘 구조화된 styled components 모듈입니다.

코드가 깔끔하고 조건부 스타일링이 적절하게 적용되어 있습니다. 의미론적 색상과 타이포그래피를 일관되게 사용하고 있어 좋습니다.

src/pages/group/components/ActivityPeriodSection/DateWheel.tsx (1)

61-93: 인터랙션 로직이 잘 구현되어 있습니다.

터치 및 마우스 드래그 이벤트 처리가 적절하게 구현되어 있고, 감도 조절과 경계 검사가 올바르게 수행되고 있습니다.

src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (2)

92-103: 검색 필터링 로직이 잘 구현되어 있습니다.

제목과 저자 모두에서 대소문자 구분 없이 검색하는 로직이 올바르게 구현되어 있습니다.


105-115: body 스크롤 관리가 적절하게 구현되어 있습니다.

모달이 열릴 때 body 스크롤을 비활성화하고 cleanup 함수에서 복원하는 로직이 올바르게 구현되어 있습니다.

src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.tsx (2)

43-54: 날짜 차이 계산 로직이 올바르게 구현되어 있습니다.

calculateDaysDifference 함수와 useMemo를 활용한 성능 최적화가 적절하게 적용되어 있습니다. 시작일 포함(+1) 처리도 올바릅니다.


86-120: 포괄적인 날짜 유효성 검사 로직입니다.

다양한 케이스를 고려한 날짜 유효성 검사가 잘 구현되어 있습니다. 월별 일수 조정, 과거 날짜 방지, 종료일 검증 등이 모두 처리되고 있습니다.

src/pages/group/components/ActivityPeriodSection/ActivityPeriodSection.styled.ts (2)

1-122: 잘 구조화된 styled components 모듈입니다.

날짜 선택기와 휠 선택기를 위한 스타일링이 체계적으로 구현되어 있습니다. 의미론적 색상과 타이포그래피가 일관되게 사용되고 있어 좋습니다.


33-65: 스크롤바 숨김 처리가 적절하게 구현되어 있습니다.

다양한 브라우저 호환성을 고려한 스크롤바 숨김 처리가 잘 구현되어 있고, 가상 요소를 활용한 선택 영역 표시도 적절합니다.

Comment thread package.json
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@types/react-datepicker": "^7.0.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 정의 패키지를 devDependencies로 이동해야 합니다.

@types/react-datepicker는 개발 시에만 필요한 타입 정의이므로 devDependencies로 이동하는 것이 좋습니다.

-    "@types/react-datepicker": "^7.0.0",

그리고 devDependencies에 추가:

    "@types/react": "^19.1.2",
    "@types/react-dom": "^19.1.2",
+   "@types/react-datepicker": "^7.0.0",
📝 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
"@types/react-datepicker": "^7.0.0",
{
"dependencies": {
"react-datepicker": "8.4.0"
},
"devDependencies": {
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-datepicker": "^7.0.0"
}
}
🤖 Prompt for AI Agents
In package.json at line 17, the dependency "@types/react-datepicker" is
currently listed under dependencies but should be moved to devDependencies
because it is only needed during development for type definitions. Remove it
from dependencies and add it to the devDependencies section instead.

Comment on lines +27 to +36
export const ToggleSlider = styled.div<{ active: boolean }>`
width: 20px;
height: 20px;
background-color: ${semanticColors.text.primary};
border-radius: 50%;
position: absolute;
top: 4px;
left: ${({ active }) => (active ? '24px' : '4px')};
transition: left 0.3s;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토글 슬라이더 상수화 및 계산 로직 개선이 필요합니다.

슬라이더의 위치 계산을 상수화하고 명확하게 해야 합니다.

+const SLIDER_DIMENSIONS = {
+  size: 20,
+  margin: 4,
+  transitionDuration: '0.3s',
+} as const;
+
 export const ToggleSlider = styled.div<{ active: boolean }>`
-  width: 20px;
-  height: 20px;
+  width: ${SLIDER_DIMENSIONS.size}px;
+  height: ${SLIDER_DIMENSIONS.size}px;
   background-color: ${semanticColors.text.primary};
   border-radius: 50%;
   position: absolute;
-  top: 4px;
-  left: ${({ active }) => (active ? '24px' : '4px')};
-  transition: left 0.3s;
+  top: ${SLIDER_DIMENSIONS.margin}px;
+  left: ${({ active }) => 
+    active 
+      ? `${TOGGLE_DIMENSIONS.width - SLIDER_DIMENSIONS.size - SLIDER_DIMENSIONS.margin}px`
+      : `${SLIDER_DIMENSIONS.margin}px`
+  };
+  transition: left ${SLIDER_DIMENSIONS.transitionDuration};
 `;

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

🤖 Prompt for AI Agents
In src/pages/group/components/PrivacySettingSection.styled.ts between lines 27
and 36, the left position values for the ToggleSlider are hardcoded inline.
Refactor by defining constants for the left positions (e.g., LEFT_ACTIVE and
LEFT_INACTIVE) and use these constants in the styled component. This improves
clarity and maintainability of the position calculation logic.

Comment on lines +16 to +25
export const ToggleSwitch = styled.div<{ active: boolean }>`
width: 48px;
height: 28px;
background-color: ${({ active }) =>
active ? semanticColors.button.fill.primary : semanticColors.background.card};
border-radius: 14px;
position: relative;
cursor: pointer;
transition: background-color 0.3s;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토글 스위치의 접근성과 상수화가 필요합니다.

하드코딩된 값들을 상수로 분리하고, 접근성을 위한 프로퍼티를 추가해야 합니다.

+const TOGGLE_DIMENSIONS = {
+  width: 48,
+  height: 28,
+  borderRadius: 14,
+  transitionDuration: '0.3s',
+} as const;
+
 export const ToggleSwitch = styled.div<{ active: boolean }>`
-  width: 48px;
-  height: 28px;
+  width: ${TOGGLE_DIMENSIONS.width}px;
+  height: ${TOGGLE_DIMENSIONS.height}px;
   background-color: ${({ active }) =>
     active ? semanticColors.button.fill.primary : semanticColors.background.card};
-  border-radius: 14px;
+  border-radius: ${TOGGLE_DIMENSIONS.borderRadius}px;
   position: relative;
   cursor: pointer;
-  transition: background-color 0.3s;
+  transition: background-color ${TOGGLE_DIMENSIONS.transitionDuration};
+  
+  &:focus {
+    outline: 2px solid ${semanticColors.button.fill.primary};
+    outline-offset: 2px;
+  }
 `;
🤖 Prompt for AI Agents
In src/pages/group/components/PrivacySettingSection.styled.ts around lines 16 to
25, the ToggleSwitch component uses hardcoded style values and lacks
accessibility attributes. Refactor by extracting hardcoded numeric and color
values into named constants for maintainability. Additionally, add appropriate
accessibility properties such as role="switch" and aria-checked attributes to
improve screen reader support and keyboard navigation.

Comment on lines +4 to +14
export const Container = styled.div`
display: flex;
flex-direction: column;
background-color: ${semanticColors.background.primary};
min-width: 360px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
padding: 96px 20px 100px 20px;
box-sizing: border-box;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

반응형 디자인과 상수화 개선이 필요합니다.

레이아웃 값들을 상수화하고 반응형 디자인을 더 명확하게 구성해야 합니다.

+const LAYOUT_CONSTANTS = {
+  minWidth: 360,
+  maxWidth: 767,
+  padding: {
+    top: 96,
+    horizontal: 20,
+    bottom: 100,
+  },
+} as const;
+
 export const Container = styled.div`
   display: flex;
   flex-direction: column;
   background-color: ${semanticColors.background.primary};
-  min-width: 360px;
-  max-width: 767px;
+  min-width: ${LAYOUT_CONSTANTS.minWidth}px;
+  max-width: ${LAYOUT_CONSTANTS.maxWidth}px;
   min-height: 100vh;
   margin: 0 auto;
-  padding: 96px 20px 100px 20px;
+  padding: ${LAYOUT_CONSTANTS.padding.top}px ${LAYOUT_CONSTANTS.padding.horizontal}px ${LAYOUT_CONSTANTS.padding.bottom}px;
   box-sizing: border-box;
+  
+  @media (max-width: 480px) {
+    padding-left: 16px;
+    padding-right: 16px;
+  }
 `;
📝 Committable suggestion

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

Suggested change
export const Container = styled.div`
display: flex;
flex-direction: column;
background-color: ${semanticColors.background.primary};
min-width: 360px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
padding: 96px 20px 100px 20px;
box-sizing: border-box;
`;
const LAYOUT_CONSTANTS = {
minWidth: 360,
maxWidth: 767,
padding: {
top: 96,
horizontal: 20,
bottom: 100,
},
} as const;
export const Container = styled.div`
display: flex;
flex-direction: column;
background-color: ${semanticColors.background.primary};
min-width: ${LAYOUT_CONSTANTS.minWidth}px;
max-width: ${LAYOUT_CONSTANTS.maxWidth}px;
min-height: 100vh;
margin: 0 auto;
padding: ${LAYOUT_CONSTANTS.padding.top}px ${LAYOUT_CONSTANTS.padding.horizontal}px ${LAYOUT_CONSTANTS.padding.bottom}px;
box-sizing: border-box;
@media (max-width: 480px) {
padding-left: 16px;
padding-right: 16px;
}
`;
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.styled.ts around lines 4 to 14, the layout values
such as widths, padding, and margins should be extracted into constants for
better maintainability. Additionally, refactor the styles to include clear
responsive design rules using media queries or similar techniques to adapt the
layout for different screen sizes. Define breakpoints as constants and apply
them to adjust container dimensions and padding accordingly.

Comment on lines +14 to +26
const PrivacySettingSection = ({ isPrivate, onToggle }: PrivacySettingSectionProps) => {
return (
<Section>
<SectionTitle>공개 설정</SectionTitle>
<PrivacyToggleContainer>
<PrivacyLabel>비공개로 설정하기</PrivacyLabel>
<ToggleSwitch active={isPrivate} onClick={onToggle}>
<ToggleSlider active={isPrivate} />
</ToggleSwitch>
</PrivacyToggleContainer>
</Section>
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선이 필요한 토글 스위치 구현

현재 토글 스위치가 div 요소로 구현되어 있어 키보드 네비게이션과 스크린 리더 지원이 부족합니다. 다음과 같은 접근성 개선을 권장합니다:

-        <ToggleSwitch active={isPrivate} onClick={onToggle}>
-          <ToggleSlider active={isPrivate} />
-        </ToggleSwitch>
+        <ToggleSwitch 
+          active={isPrivate} 
+          onClick={onToggle}
+          role="switch"
+          aria-checked={isPrivate}
+          aria-labelledby="privacy-label"
+          tabIndex={0}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter' || e.key === ' ') {
+              e.preventDefault();
+              onToggle();
+            }
+          }}
+        >
+          <ToggleSlider active={isPrivate} />
+        </ToggleSwitch>

그리고 라벨에 id를 추가하세요:

-        <PrivacyLabel>비공개로 설정하기</PrivacyLabel>
+        <PrivacyLabel id="privacy-label">비공개로 설정하기</PrivacyLabel>
📝 Committable suggestion

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

Suggested change
const PrivacySettingSection = ({ isPrivate, onToggle }: PrivacySettingSectionProps) => {
return (
<Section>
<SectionTitle>공개 설정</SectionTitle>
<PrivacyToggleContainer>
<PrivacyLabel>비공개로 설정하기</PrivacyLabel>
<ToggleSwitch active={isPrivate} onClick={onToggle}>
<ToggleSlider active={isPrivate} />
</ToggleSwitch>
</PrivacyToggleContainer>
</Section>
);
};
const PrivacySettingSection = ({ isPrivate, onToggle }: PrivacySettingSectionProps) => {
return (
<Section>
<SectionTitle>공개 설정</SectionTitle>
<PrivacyToggleContainer>
<PrivacyLabel id="privacy-label">비공개로 설정하기</PrivacyLabel>
<ToggleSwitch
active={isPrivate}
onClick={onToggle}
role="switch"
aria-checked={isPrivate}
aria-labelledby="privacy-label"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onToggle();
}
}}
>
<ToggleSlider active={isPrivate} />
</ToggleSwitch>
</PrivacyToggleContainer>
</Section>
);
};
🤖 Prompt for AI Agents
In src/pages/group/components/PrivacySettingSection.tsx around lines 14 to 26,
the toggle switch is implemented using div elements, which lack keyboard
navigation and screen reader support. Replace the div-based toggle with a native
accessible input element of type checkbox, ensuring it can be focused and
toggled via keyboard. Also, add an id to the label and associate it with the
input using the htmlFor attribute to improve screen reader accessibility.

Comment on lines +147 to +173
const slideValues = () => {
if (!sliderState || !sliderState.slides) return [];

const offset = 0;
const valuesArray = [];

for (let i = 0; i < slides; i++) {
const slideData = sliderState.slides[i];
if (!slideData) continue;

const distance = slideData.distance || 0;
const threshold = wheelSize / 2 + 1;
const rotate = Math.abs(distance) > threshold ? 180 : distance * (360 / wheelSize) * -1;

const isSelected = i === currentIndex;

const style = {
transform: `rotateX(${rotate}deg) translateZ(${radius}px)`,
WebkitTransform: `rotateX(${rotate}deg) translateZ(${radius}px)`,
};

const value = values[i];
valuesArray.push({ style, value, isSelected });
}

return valuesArray;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

slideValues 함수의 미사용 변수를 제거하세요.

offset 변수가 선언되었지만 사용되지 않고 있습니다.

다음 diff를 적용하여 미사용 변수를 제거하세요:

  const slideValues = () => {
    if (!sliderState || !sliderState.slides) return [];

-   const offset = 0;
    const valuesArray = [];
📝 Committable suggestion

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

Suggested change
const slideValues = () => {
if (!sliderState || !sliderState.slides) return [];
const offset = 0;
const valuesArray = [];
for (let i = 0; i < slides; i++) {
const slideData = sliderState.slides[i];
if (!slideData) continue;
const distance = slideData.distance || 0;
const threshold = wheelSize / 2 + 1;
const rotate = Math.abs(distance) > threshold ? 180 : distance * (360 / wheelSize) * -1;
const isSelected = i === currentIndex;
const style = {
transform: `rotateX(${rotate}deg) translateZ(${radius}px)`,
WebkitTransform: `rotateX(${rotate}deg) translateZ(${radius}px)`,
};
const value = values[i];
valuesArray.push({ style, value, isSelected });
}
return valuesArray;
};
const slideValues = () => {
if (!sliderState || !sliderState.slides) return [];
const valuesArray = [];
for (let i = 0; i < slides; i++) {
const slideData = sliderState.slides[i];
if (!slideData) continue;
const distance = slideData.distance || 0;
const threshold = wheelSize / 2 + 1;
const rotate = Math.abs(distance) > threshold
? 180
: distance * (360 / wheelSize) * -1;
const isSelected = i === currentIndex;
const style = {
transform: `rotateX(${rotate}deg) translateZ(${radius}px)`,
WebkitTransform: `rotateX(${rotate}deg) translateZ(${radius}px)`,
};
const value = values[i];
valuesArray.push({ style, value, isSelected });
}
return valuesArray;
};
🧰 Tools
🪛 ESLint

[error] 150-150: 'offset' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
In src/pages/group/components/ActivityPeriodSection/DateWheel.tsx between lines
147 and 173, the variable 'offset' is declared but never used in the slideValues
function. Remove the declaration of 'offset' to clean up the code and eliminate
the unused variable warning.

Comment on lines +172 to +179
export const BookItem = styled.div`
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
border-bottom: 1px solid ${colors.grey[400]};
cursor: pointer;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

클릭 가능한 항목을 div 로 표현하면 접근성이 크게 저하됩니다
BookItem 은 실제 버튼 역할이므로 시맨틱 요소(<button>/<li role="button">)로 변경하거나 최소한 role="button"tabIndex={0} 를 부여하고 키보드 Enter/Space 이벤트를 처리해야 합니다.

🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts
around lines 172 to 179, the BookItem styled component is a clickable div which
harms accessibility. To fix this, change the element to a semantic button or add
role="button" and tabIndex={0} to make it focusable, and implement keyboard
event handlers for Enter and Space keys to mimic button behavior.

Comment on lines +84 to +102
export const IconButton = styled.button`
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
padding: 0;

img {
width: 24px;
height: 24px;
}

&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

포커스 스타일이 없어 키보드 접근성이 떨어집니다
IconButtonhover 만 있고 :focus-visible 스타일이 없습니다. 시각적 포커스 표시를 추가해 스크린리더·키보드 사용자에게 명확히 알려주세요.

   &:hover {
     background-color: rgba(255, 255, 255, 0.1);
   }
+
+  &:focus-visible {
+    outline: 2px solid ${semanticColors.text.point.green};
+    outline-offset: 2px;
+  }
📝 Committable suggestion

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

Suggested change
export const IconButton = styled.button`
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
padding: 0;
img {
width: 24px;
height: 24px;
}
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`;
export const IconButton = styled.button`
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
padding: 0;
img {
width: 24px;
height: 24px;
}
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&:focus-visible {
outline: 2px solid ${semanticColors.text.point.green};
outline-offset: 2px;
}
`;
🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts
between lines 84 and 102, the IconButton styled component lacks a :focus-visible
style, reducing keyboard accessibility. Add a :focus-visible CSS rule to
IconButton that provides a clear visual focus indicator, such as an outline or
box-shadow, to improve usability for keyboard and screen reader users.

Comment on lines +111 to +134
export const Tab = styled.button<{ active: boolean }>`
background: none;
border: none;
color: ${({ active }) => (active ? semanticColors.text.primary : semanticColors.text.ghost)};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.semibold};
padding: 8px 0 8px 0;
cursor: pointer;
position: relative;

${({ active }) =>
active &&
`
&::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: ${semanticColors.text.primary};
}
`}
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

탭 버튼에도 포커스·ARIA 속성이 필요합니다
Tab 컴포넌트는 시맨틱 버튼이지만 포커스 표시 및 aria-selected 처리 없이 컬러만 변경하고 있습니다. 스크린리더 지원을 위해 prop으로 aria-selected={active} 를 전달하고 :focus-visible 스타일을 추가하세요.

-export const Tab = styled.button<{ active: boolean }>`
+export const Tab = styled.button<{ active: boolean }>`
   /* ... */
   position: relative;
+
+  &:focus-visible {
+    outline: 2px solid ${semanticColors.text.point.green};
+    outline-offset: 2px;
+  }
`;
📝 Committable suggestion

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

Suggested change
export const Tab = styled.button<{ active: boolean }>`
background: none;
border: none;
color: ${({ active }) => (active ? semanticColors.text.primary : semanticColors.text.ghost)};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.semibold};
padding: 8px 0 8px 0;
cursor: pointer;
position: relative;
${({ active }) =>
active &&
`
&::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: ${semanticColors.text.primary};
}
`}
`;
export const Tab = styled.button<{ active: boolean }>`
background: none;
border: none;
color: ${({ active }) =>
active
? semanticColors.text.primary
: semanticColors.text.ghost};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.semibold};
padding: 8px 0 8px 0;
cursor: pointer;
position: relative;
&:focus-visible {
outline: 2px solid ${semanticColors.text.point.green};
outline-offset: 2px;
}
${({ active }) =>
active &&
`
&::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: ${semanticColors.text.primary};
}
`}
`;
🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts
between lines 111 and 134, the Tab styled button lacks accessibility features.
Add the aria-selected attribute with the value of the active prop to the Tab
component and include a :focus-visible CSS style to provide a visible focus
indicator for keyboard users. This will improve screen reader support and
keyboard navigation accessibility.

Comment on lines +24 to +42
max-width: 767px;
margin: 0 auto;
background-color: ${colors.darkgrey.main};
border-radius: 20px 20px 0 0;
z-index: 1001;
transform: translateY(${({ isVisible }) => (isVisible ? '0' : '100%')});
transition: transform 0.3s ease;
max-height: 50vh;
overflow: hidden;
display: flex;
flex-direction: column;
`;

export const Content = styled.div`
padding: 20px;
display: flex;
flex-direction: column;
height: 50vh;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

max-height와 내부 height가 동일해 내용이 넘칠 수 있습니다
BottomSheetContainermax-height: 50vh, 내부 Contentheight: 50vh 로 고정돼 헤더·탭·검색바 높이를 고려하지 못합니다. 스크롤 버그가 발생할 가능성이 높으니 Contentcalc(50vh - 헤더높이) 식으로 계산하거나 flex:1 로 처리하세요.

-export const Content = styled.div`
-  padding: 20px;
-  display: flex;
-  flex-direction: column;
-  height: 50vh;
-`;
+export const Content = styled.div`
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+  flex: 1; /* 부모 높이에서 헤더 등을 제외한 영역 채우기 */
+`;
📝 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
max-width: 767px;
margin: 0 auto;
background-color: ${colors.darkgrey.main};
border-radius: 20px 20px 0 0;
z-index: 1001;
transform: translateY(${({ isVisible }) => (isVisible ? '0' : '100%')});
transition: transform 0.3s ease;
max-height: 50vh;
overflow: hidden;
display: flex;
flex-direction: column;
`;
export const Content = styled.div`
padding: 20px;
display: flex;
flex-direction: column;
height: 50vh;
`;
export const Content = styled.div`
padding: 20px;
display: flex;
flex-direction: column;
flex: 1; /* 부모 높이에서 헤더 등을 제외한 영역 채우기 */
`;
🤖 Prompt for AI Agents
In src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts
around lines 24 to 42, the Content component has a fixed height of 50vh which
matches the max-height of BottomSheetContainer, causing potential overflow and
scroll issues. To fix this, adjust Content's height to be calculated as 50vh
minus the header height using calc(), or replace the fixed height with flex: 1
to allow it to fill the remaining space flexibly within the container.

@heeeeyong heeeeyong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

전반적으로 컴포넌트화가 잘 되어있어서 보기 편했습니다. 저도 이런부분에서 공통적인 피드백을 받은것 같은데 5주차에 코드리팩토링할때 반영해보려고 합니다! 그리고 pages > group > components 폴더를 따로 하나 더 빼놓은 이유가 궁금합니다!

setIsPrivate(!isPrivate);
};

const isFormValid = (selectedBook || bookTitle.trim() !== '') && selectedGenre !== '';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

화면설계서 상으로 아래 유효성 검증 조건에서 누락된 부분만 추가해주시면 될것같아요!
image

align-items: center;
justify-content: center;

/* 스크롤바 숨기기 */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

제가 맡은 페이지에서도 스크롤바를 숨겨야하는 상황이 있는데, 전역에 기본적으로 스크롤 바를 가리는 속성을 넣고 특정 컴포넌트에서 스크롤 바가 필요하다면 넣어서 쓰는걸로 하는게 좋을 것 같습니다!

@ho0010 ho0010 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

datepicker 커스텀 하느라 고생 많이 하셨겠네요..!
styled 컴포넌트 네이밍이 잘 되어있네요... 분리도 잘 되어있어서 재사용성도 좋은 것 같아요~ 전체적으로 스타일링이 일관성이 있어서 가독성이 좋은 것 같아요!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

현재 ActivityPeriodSection은 날짜 계산과 검증 로직, 상태 관리, UI 이렇게 많은 역할을 가지고 있는 것으로 보입니다. 리팩토링할 때 날짜 계산과 검증로직을 utils/date.ts로 따로 분리하면 어떨까 싶습니다! 또한, 상태 로직도 커스텀 훅으로 분리하면 각 객체의 역할이 더 분명해질 것 같아요!

@ljh130334 ljh130334 merged commit b953795 into develop Jul 10, 2025
1 check was pending
@coderabbitai coderabbitai Bot mentioned this pull request Jul 13, 2025
5 tasks
@ljh130334 ljh130334 deleted the feat/makeroom branch July 29, 2025 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants