Skip to content

feat: Rooms 도메인 API 연동#117

Merged
ho0010 merged 11 commits intodevelopfrom
feat/api-rooms
Aug 15, 2025
Merged

feat: Rooms 도메인 API 연동#117
ho0010 merged 11 commits intodevelopfrom
feat/api-rooms

Conversation

@ho0010
Copy link
Collaborator

@ho0010 ho0010 commented Aug 15, 2025

#️⃣연관된 이슈

[API] Rooms API 연동 #106

📝작업 내용

  • 내 모임방 리스트 조회

  • 모집중인 방 상세보기

  • 참여하기/취소하기

  • 장르별 마감 임박한/인기 있는 독서 모임방

  • 모집 마감하기

  • 참여중인 내 모임방 조회

서버 개발이 아직 안된 아래 API를 제외하고 Rooms 도메인에 대한 API 연동 완료했습니다!

  • 방 삭제 (서버 미개발)

  • 방 검색 (서버 미개발)

💬리뷰 요구사항(선택)

snackbar는 API 연동 후 일괄 적용하도록 하겠습니다!

우선 전체적인 API 연동이 끝난 후 QA 진행하며 플로우들을 이어주는 작업을 진행이 필요할 것 같습니다!

Summary by CodeRabbit

  • 신기능
    • 그룹/상세/모달 화면이 실제 API 데이터로 동작하며 마감/인기/참여/완료 모임을 실시간으로 표시합니다.
    • 그룹 카드 클릭 시 상세 화면으로 이동합니다.
    • 상세 화면에 추천 모임과 모집 D-day 표시가 추가되었습니다.
  • 변경
    • 그룹 상세 경로가 /group/detail/:roomId 로 변경되었습니다.
    • 그룹 생성 완료 시 /group/detail/:roomId 로 이동합니다.
    • 그룹 검색 경로가 /group/search 로 변경되었습니다.
  • UI/UX
    • 로딩/에러/빈 상태를 추가하여 피드백을 강화했습니다.
    • 카드 크기와 타이포그래피 등 스타일을 개선했습니다.

@ho0010 ho0010 self-assigned this Aug 15, 2025
@ho0010 ho0010 added ✨ Feature 기능 개발 📬 API 서버 API 통신 labels Aug 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 15, 2025

Walkthrough

API 모듈을 신설해 방 목록/상세 데이터를 조회하고, 그룹 관련 UI를 더미 데이터에서 API 연동으로 전환했습니다. 클릭 시 상세 페이지 라우팅을 동적 파라미터(/group/detail/:roomId)로 변경했으며, 상세 페이지와 메인 그룹 페이지는 API 응답 기반으로 렌더링하도록 수정되었습니다.

Changes

Cohort / File(s) Summary
Rooms API 모듈 추가
src/api/rooms/getJoinedRooms.ts, src/api/rooms/getMyRooms.ts, src/api/rooms/getRoomDetail.ts, src/api/rooms/getRoomsByCategory.ts
방 참가/내 방/카테고리별/상세 조회용 타입 정의와 fetch 함수 추가. 공통 apiClient 사용, 에러 로깅 후 재throw.
그룹 컴포넌트 API 연동 및 상태 UI
src/components/group/CompletedGroupModal.tsx, src/components/group/MyGroupModal.tsx, src/components/group/MyGroupBox.tsx, src/components/group/RecruitingGroupBox.tsx, src/components/group/GroupCard.tsx
더미 제거, API 호출로 데이터 로딩/에러/빈 상태 처리 추가. 데이터 어댑터 도입. GroupCard에 onClick 지원 및 카드 사이즈 조정. 카드 클릭 시 상세 조회 후 라우팅.
그룹 메인 페이지 동적 데이터화
src/pages/group/Group.tsx
카테고리 순회하여 마감/인기 섹션 데이터 수집 및 변환. 검색 라우트 경로 수정(/group/search). MyGroupBox prop 변경 반영.
그룹 상세 페이지 API 전환
src/pages/groupDetail/GroupDetail.tsx, src/pages/groupDetail/GroupDetail.styled.ts
useParams로 roomId 수신, 상세 API 연동, D-day 계산, 추천 방 변환/렌더. 스타일 일부 조정(정렬, 폰트 크기).
라우팅/네비게이션 업데이트
src/pages/index.tsx, src/pages/group/CreateGroup.tsx
상세 경로를 /group/detail/:roomId로 변경. 생성 성공 시 해당 경로로 이동하며 state 전달 제거.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant RecruitingGroupBox
  participant API as getRoomDetail
  participant Router

  User->>RecruitingGroupBox: 클릭(GroupCard)
  RecruitingGroupBox->>API: getRoomDetail(roomId)
  alt 성공
    API-->>RecruitingGroupBox: RoomDetailResponse
    RecruitingGroupBox->>Router: navigate(/group/detail/:roomId)
  else 실패
    API-->>RecruitingGroupBox: 에러
    RecruitingGroupBox->>Router: navigate(/group/:groupId)
  end
Loading
sequenceDiagram
  participant GroupPage as Group.tsx
  participant API as getRoomsByCategory
  participant State as Sections State

  GroupPage->>GroupPage: 카테고리 목록 준비
  loop 각 카테고리
    GroupPage->>API: getRoomsByCategory(category)
    API-->>GroupPage: {deadlineRoomList, popularRoomList}
    GroupPage->>State: 변환 후 섹션 누적 업데이트
  end
  GroupPage->>GroupPage: 섹션 렌더링
Loading
sequenceDiagram
  participant MyGroupBox
  participant API as getJoinedRooms
  participant UI as Carousel/Empty

  MyGroupBox->>API: getJoinedRooms(page=1)
  API-->>MyGroupBox: {roomList, ...}
  MyGroupBox->>UI: 변환 후 렌더(캐러셀 or 빈 상태)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • heeeeyong
  • ljh130334

Poem

폴짝, 폴짝, 새 길 위로
더미를 넘고 API로 go!
방 찾고, 방 보고, 방으로 hop—
상세는 path에 실어 pop.
오늘도 토끼는 코드밭 누벼,
길잃은 state는 없도록요. 🥕✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-rooms

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link

vercel bot commented Aug 15, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Preview Comment Aug 15, 2025 8:17am

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: 5

🔭 Outside diff range comments (2)
src/pages/group/CreateGroup.tsx (2)

37-39: 기본 종료일(oneMonthLater) 계산 버그: 내일(+1일)로 설정되고 있습니다.

변수명이 가리키는 의미(한 달 뒤)와 다르게 현재는 +1일만 더하고 있어 기본 종료일이 시작일과 동일하게 내일로 설정됩니다. 한 달 뒤로 맞춰주세요.

아래처럼 내일을 기준으로 한 달 뒤로 설정하는 것이 의도에 부합해 보입니다.

-    const oneMonthLater = new Date(today);
-    oneMonthLater.setDate(today.getDate() + 1);
+    // 내일을 기준으로 한 달 뒤 종료일 설정
+    const oneMonthLater = new Date(tomorrow);
+    oneMonthLater.setMonth(oneMonthLater.getMonth() + 1);

135-137: 성공 여부 판단 로직 오타: 동일 속성을 두 번 체크하고 있습니다.

주석과 달리 response.isSuccess || response.isSuccess는 항상 isSuccess만 검사합니다. 백엔드 응답이 success로 올 가능성까지 고려하려면 아래처럼 보완이 필요합니다.

-      // 두 가지 응답 형식 모두 확인
-      const isSuccessful = response.isSuccess || response.isSuccess;
+      // 두 가지 응답 형식 모두 확인 (isSuccess 또는 success)
+      const isSuccessful = Boolean(
+        // @ts-expect-error: 백엔드 응답의 성공 필드명이 환경별로 다를 수 있어 유연하게 처리
+        (response?.isSuccess ?? (response as any)?.success)
+      );

추가로 roomId가 존재하는지까지 가드하면 네비게이션 안정성이 올라갑니다.

-      if (isSuccessful) {
-        // 성공 시 모집 중인 방 상세 페이지로 이동
-        navigate(`/group/detail/${response.data.roomId}`, {
-          replace: true,
-        });
+      if (isSuccessful && response?.data?.roomId) {
+        // 성공 시 모집 중인 방 상세 페이지로 이동
+        navigate(`/group/detail/${response.data.roomId}`, { replace: true });
+      } else if (isSuccessful) {
+        alert('방 생성은 성공했지만 roomId를 확인할 수 없습니다.');
       } else {
         alert(`방 생성에 실패했습니다: ${response.message} (코드: ${response.code})`);
       }
🧹 Nitpick comments (11)
src/pages/group/CreateGroup.tsx (1)

83-83: ISBN 기본값 하드코딩은 제거 권장(폼 유효성으로 이미 보장됨).

selectedBook !== null이 폼 유효성 조건에 포함되어 있어 기본 ISBN이 실제로 쓰일 일이 없습니다. 오히려 잘못된 책으로 생성될 리스크가 있으므로 기본값은 제거하는 편이 안전합니다.

-        isbn: selectedBook?.isbn || '9788936434632', // 선택된 책의 ISBN 또는 기본값
+        isbn: selectedBook!.isbn, // 폼 유효성 상 selectedBook은 null이 아님
src/components/group/GroupCard.tsx (2)

52-53: border-radius에 'none'은 유효한 값이 아닙니다.

CSS에서 border-radius: none은 무시됩니다. 0으로 명시하세요.

-  border-radius: ${({ cardType }) => (cardType === 'search' ? `none` : '12px')};
+  border-radius: ${({ cardType }) => (cardType === 'search' ? '0' : '12px')};

12-19: 클릭 가능한 카드의 접근성 보완(키보드/포커스/커서).

onClick을 전달받는 경우 키보드 접근성과 사용자 피드백이 필요합니다.

다음과 같이 보완을 제안합니다.

  • 카드에 role="button", tabIndex={0} 추가
  • Enter/Space로 onClick 트리거
  • 시각적 피드백: cursor: pointer
-      <Card ref={ref} cardType={type} onClick={onClick}>
+      <Card
+        ref={ref}
+        cardType={type}
+        onClick={onClick}
+        role={onClick ? 'button' : undefined}
+        tabIndex={onClick ? 0 : undefined}
+        onKeyDown={
+          onClick
+            ? (e) => {
+                if (e.key === 'Enter' || e.key === ' ') {
+                  e.preventDefault();
+                  onClick();
+                }
+              }
+            : undefined
+        }
+      >

Card 스타일에 커서 추가:

 const Card = styled.div<{ cardType: 'main' | 'search' | 'modal' }>`
   display: flex;
   align-items: center;
+  cursor: pointer;

필요 시 cursoronClick이 있을 때만 적용되도록 prop을 추가해 분기할 수도 있습니다.

src/api/rooms/getRoomDetail.ts (1)

41-49: API 래퍼/타입 정의 전반적으로 깔끔합니다.

타입 명세와 에러 로깅이 명확합니다. 아래 두 가지는 선택적 보완 포인트입니다.

  • 404/410 등 방이 없거나 마감된 케이스를 구분해 호출부가 적절히 분기할 수 있도록, 상태 코드에 따라 커스텀 에러를 던지는 것도 고려해보세요.
  • roomId는 라우트 파라미터에서 문자열로 들어오므로 호출부에서 Number() 변환 시 NaN 가능성을 가드하는 유틸을 두면 안정적입니다.
src/pages/groupDetail/GroupDetail.tsx (4)

79-79: 불필요한 console.log 제거

프로덕션 코드에 디버깅용 console.log가 남아있습니다.

-    console.log(endDate);

94-94: 불필요한 console.log 제거

프로덕션 코드에 디버깅용 console.log가 남아있습니다.

-        console.log(response);

73-83: 날짜 계산 로직 개선 및 타임존 고려

calculateDday 함수에서 타임존을 고려하지 않고 있으며, 시간 단위가 아닌 날짜 단위로 계산하는 것이 더 정확할 수 있습니다.

   const calculateDday = (recruitEndDate: string): string => {
-    const today = new Date();
-    const endDate = new Date(recruitEndDate);
-    const diffTime = endDate.getTime() - today.getTime();
-    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);  // 오늘 날짜의 시작 시간으로 설정
+    const endDate = new Date(recruitEndDate);
+    endDate.setHours(0, 0, 0, 0);  // 마감일의 시작 시간으로 설정
+    const diffTime = endDate.getTime() - today.getTime();
+    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

-    console.log(endDate);
     if (diffDays < 0) return '모집 종료';
     if (diffDays === 0) return '오늘 마감';
     return `${diffDays}일 남음`;
   };

59-71: 하드코딩된 기본값 개선

convertRecommendRoomToGroup 함수에서 deadLinegenre 필드에 하드코딩된 값을 사용하고 있습니다. 이러한 값들을 계산하거나 적절한 기본값을 설정하는 것이 좋습니다.

   const convertRecommendRoomToGroup = (room: RecommendRoom): Group => {
+    const deadLine = Math.max(0, Math.ceil(
+      (new Date(room.recruitEndDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
+    ));
+    
     return {
       id: room.roomId.toString(),
       title: room.roomName,
       userName: '',
       participants: room.memberCount,
       maximumParticipants: room.recruitCount,
       coverUrl: room.roomImageUrl,
-      deadLine: 0,
-      genre: '',
+      deadLine,
+      genre: '추천',  // 또는 적절한 기본값
       isOnGoing: true,
     };
   };
src/components/group/MyGroupBox.tsx (1)

22-28: 타입 안전성 개선

convertJoinedRoomToGroup 함수에서 필수 필드인 userName이 누락되어 있습니다. 기본값을 설정하거나 타입을 수정해야 합니다.

 const convertJoinedRoomToGroup = (room: JoinedRoomItem): Group => ({
   id: room.roomId,
   title: room.roomTitle,
   participants: room.memberCount,
   coverUrl: room.bookImageUrl,
   progress: room.userPercentage,
+  userName: '사용자',  // 또는 API에서 제공되는 경우 해당 값 사용
 });
src/components/group/MyGroupModal.tsx (2)

21-33: 하드코딩된 기본값 개선

convertRoomToGroup 함수에서 deadLinegenre 필드에 하드코딩된 값을 사용하고 있습니다. API 응답에서 제공되는 endDate를 활용하여 계산하는 것이 좋습니다.

   const convertRoomToGroup = (room: Room): Group => {
+    const deadLine = Math.max(0, Math.ceil(
+      (new Date(room.endDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
+    ));
+    
     return {
       id: room.roomId.toString(),
       title: room.roomName,
       userName: '',
       participants: room.memberCount,
       maximumParticipants: room.recruitCount,
       coverUrl: room.bookImageUrl,
-      deadLine: 0,
-      genre: '',
+      deadLine,
+      genre: '내 모임',  // 또는 적절한 기본값
       isOnGoing: room.type === 'playing' || room.type === 'playingAndRecruiting',
     };
   };

41-46: 타입 안전성 개선

roomType 계산 로직에서 타입 추론을 명확하게 할 수 있습니다.

-        const roomType: RoomType =
-          selected === '진행중'
-            ? 'playing'
-            : selected === '모집중'
-              ? 'recruiting'
-              : 'playingAndRecruiting';
+        let roomType: RoomType;
+        switch (selected) {
+          case '진행중':
+            roomType = 'playing';
+            break;
+          case '모집중':
+            roomType = 'recruiting';
+            break;
+          default:
+            roomType = 'playingAndRecruiting';
+        }
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9f2ace7 and c2bd7ba.

📒 Files selected for processing (14)
  • src/api/rooms/getJoinedRooms.ts (1 hunks)
  • src/api/rooms/getMyRooms.ts (1 hunks)
  • src/api/rooms/getRoomDetail.ts (1 hunks)
  • src/api/rooms/getRoomsByCategory.ts (1 hunks)
  • src/components/group/CompletedGroupModal.tsx (3 hunks)
  • src/components/group/GroupCard.tsx (2 hunks)
  • src/components/group/MyGroupBox.tsx (5 hunks)
  • src/components/group/MyGroupModal.tsx (4 hunks)
  • src/components/group/RecruitingGroupBox.tsx (4 hunks)
  • src/pages/group/CreateGroup.tsx (1 hunks)
  • src/pages/group/Group.tsx (2 hunks)
  • src/pages/groupDetail/GroupDetail.styled.ts (1 hunks)
  • src/pages/groupDetail/GroupDetail.tsx (2 hunks)
  • src/pages/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (11)
src/pages/groupDetail/GroupDetail.styled.ts (1)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
src/api/rooms/getJoinedRooms.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/api/rooms/getRoomsByCategory.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/api/rooms/getRoomDetail.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/components/group/MyGroupBox.tsx (3)
src/api/rooms/getJoinedRooms.ts (2)
  • JoinedRoomItem (4-10)
  • getJoinedRooms (26-34)
src/components/group/MyGroupCard.tsx (1)
  • MyGroupCard (10-33)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
src/components/group/MyGroupModal.tsx (5)
src/api/rooms/getMyRooms.ts (3)
  • Room (6-14)
  • RoomType (3-3)
  • getMyRooms (28-44)
src/components/group/MyGroupBox.tsx (1)
  • Group (9-20)
src/components/group/GroupCard.tsx (1)
  • GroupCard (15-42)
src/pages/searchBook/SearchBook.styled.ts (3)
  • EmptyState (192-201)
  • EmptyTitle (203-208)
  • EmptySubText (210-214)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
src/api/rooms/getMyRooms.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/components/group/RecruitingGroupBox.tsx (4)
src/api/rooms/getRoomDetail.ts (1)
  • getRoomDetail (41-49)
src/components/group/GroupCard.tsx (1)
  • GroupCard (15-42)
src/pages/searchBook/SearchBook.styled.ts (1)
  • EmptySubText (210-214)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
src/components/group/CompletedGroupModal.tsx (4)
src/components/group/MyGroupBox.tsx (1)
  • Group (9-20)
src/api/rooms/getMyRooms.ts (1)
  • getMyRooms (28-44)
src/components/group/GroupCard.tsx (1)
  • GroupCard (15-42)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
src/pages/groupDetail/GroupDetail.tsx (4)
src/api/rooms/getRoomDetail.ts (3)
  • RoomDetailResponse (4-30)
  • RecommendRoom (32-39)
  • getRoomDetail (41-49)
src/components/group/MyGroupBox.tsx (1)
  • Group (9-20)
src/pages/groupDetail/GroupDetail.styled.ts (1)
  • TopBackground (19-28)
src/components/group/GroupCard.tsx (1)
  • GroupCard (15-42)
src/pages/group/Group.tsx (3)
src/api/rooms/getRoomsByCategory.ts (2)
  • RoomItem (4-11)
  • getRoomsByCategory (23-33)
src/components/group/MyGroupBox.tsx (2)
  • Group (9-20)
  • MyGroupBox (34-106)
src/components/group/RecruitingGroupCarousel.tsx (1)
  • Section (6-9)
🔇 Additional comments (22)
src/pages/index.tsx (1)

55-55: 경로 파라미터 기반 GroupDetail 라우팅 확인 — 승인 (LGTM)

검사 결과, 상수 경로 /group/detail는 남아있지 않고 모든 호출이 파라미터 또는 하위 경로를 사용하고 있습니다. 확인된 위치:

  • src/pages/index.tsx:55 — <Route path="group/detail/:roomId" element={} />
  • src/pages/index.tsx:56 — <Route path="group/detail/joined" element={} />
  • src/pages/group/CreateGroup.tsx:140 — navigate(/group/detail/${response.data.roomId}, { ... })
  • src/components/group/RecruitingGroupBox.tsx:29 — navigate(/group/detail/${roomId});

승인합니다.

src/pages/groupDetail/GroupDetail.styled.ts (2)

172-180: BookDetails 여백 조정 LGTM.

상단 마진으로 시각적 균형이 좋아졌습니다. 타이포 스케일과의 조합도 자연스럽습니다.


182-187: 본문 크기(2xs) 적용 적절.

BookIntro > p2xs를 적용해 서브카피 위계가 명확해졌습니다.

src/components/group/RecruitingGroupBox.tsx (4)

5-7: 필요한 import를 추가해 API 통합을 위한 준비 완료

API 통합과 내비게이션, 스타일링을 위한 필수 import들이 올바르게 추가되었습니다.


22-35: getRoomDetail API 호출 후 적절한 네비게이션 처리

방 상세 정보를 먼저 확인한 후 성공 시 동적 라우팅으로 이동하는 로직이 잘 구현되었습니다. 실패 시 기존 경로로 fallback하는 것도 적절합니다.


48-62: 장르별 빈 상태에 대한 UX 개선

선택한 장르에 방이 없을 때 사용자에게 명확한 피드백을 제공하는 UI가 잘 구현되었습니다. 메시지도 친근하고 이해하기 쉽습니다.


130-154: 빈 상태 UI 컴포넌트 스타일링

전역 스타일 토큰을 사용하여 일관성 있는 빈 상태 UI를 구현했습니다. 중앙 정렬과 적절한 간격 설정이 잘 되어 있습니다.

src/api/rooms/getRoomsByCategory.ts (3)

4-11: RoomItem 인터페이스 설계 검토

RoomItem 인터페이스의 필드명들이 명확하고 적절합니다. API 응답 구조와 잘 매칭됩니다.


13-21: API 응답 구조 타입 정의

표준적인 API 응답 구조(isSuccess, code, message, data)를 따르고 있어 일관성이 좋습니다. deadlineRoomList와 popularRoomList로 구분된 데이터 구조도 적절합니다.


23-33: 카테고리 파라미터 URL 인코딩 처리

encodeURIComponent를 사용하여 카테고리 파라미터를 안전하게 처리한 것이 좋습니다. 한글 카테고리명이 포함될 가능성을 고려한 적절한 처리입니다.

src/api/rooms/getMyRooms.ts (3)

3-3: RoomType 유니온 타입 정의

방의 다양한 상태를 명확하게 표현하는 타입 정의가 잘 되어 있습니다. 비즈니스 로직을 잘 반영합니다.


6-14: Room 인터페이스 필드 검토

Room 인터페이스의 필드들이 UI 컴포넌트에서 필요한 정보들을 포함하고 있어 적절합니다.


28-44: 쿼리 파라미터 처리와 API 호출

URLSearchParams를 사용한 쿼리 파라미터 구성이 깔끔하고, cursor 파라미터의 조건부 추가 로직도 적절합니다. 페이지네이션을 고려한 좋은 설계입니다.

src/components/group/CompletedGroupModal.tsx (6)

15-18: 상태 관리를 위한 적절한 useState 활용

로딩, 에러, 데이터 상태를 분리해서 관리하는 것이 UI 상태 표현에 적합합니다.


20-31: Room을 Group으로 변환하는 어댑터 함수

API 응답 데이터를 UI 컴포넌트가 기대하는 형태로 변환하는 어댑터 패턴이 잘 적용되었습니다. 필드 매핑도 적절합니다.


33-53: useEffect에서 API 호출과 에러 처리

컴포넌트 마운트 시 API를 호출하고 로딩/에러 상태를 적절히 관리하는 구조가 잘 구현되었습니다.


65-77: 다양한 UI 상태에 대한 조건부 렌더링

로딩, 에러, 빈 상태, 성공 상태를 모두 고려한 조건부 렌더링이 사용자 경험을 향상시킵니다.


93-104: isEmpty prop을 활용한 동적 스타일링

빈 상태일 때 overflow-y를 visible로 설정하여 스크롤바가 나타나지 않도록 한 것이 세심한 UI 고려사항입니다.


106-146: 상태별 메시지 컴포넌트 스타일링

로딩, 에러, 빈 상태에 대한 각각의 스타일 컴포넌트가 전역 디자인 토큰을 활용하여 일관성 있게 구현되었습니다.

src/api/rooms/getJoinedRooms.ts (3)

4-10: JoinedRoomItem 인터페이스 필드 명명

userPercentage 필드가 포함되어 있어 사용자의 진행률 정보를 제공할 수 있는 좋은 설계입니다.


12-24: 페이지네이션 정보를 포함한 응답 타입

page, size, last, first 등 페이지네이션에 필요한 메타데이터가 포함되어 있어 향후 무한 스크롤이나 페이지네이션 구현에 유용합니다.


26-34: 페이지 파라미터를 사용한 API 호출

기본값으로 page=1을 설정하여 사용하기 편리하게 구현되었습니다.

Comment on lines 54 to 59
padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')};
gap: 12px;
width: 100%;
min-width: 208px;
min-height: 80px;
padding: 12px;
`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

padding 중복 선언으로 조건부 패딩이 무시됩니다.

padding이 두 번 선언되어 마지막 padding: 12px;이 항상 적용됩니다. 이로 인해 cardType === 'search'일 때의 여백 의도가 깨집니다.

아래 중 하나로 정리해주세요.

-  padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')};
+  padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')};
   gap: 12px;
   min-width: 208px;
   min-height: 80px;
-  padding: 12px;

또는 search 외 타입의 경우에만 별도 선언이 필요하다면 다음과 같이 분기하세요.

   gap: 12px;
   min-width: 208px;
   min-height: 80px;
+  ${({ cardType }) => cardType !== 'search' && 'padding: 12px;'}
📝 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
padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')};
gap: 12px;
width: 100%;
min-width: 208px;
min-height: 80px;
padding: 12px;
`;
padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')};
gap: 12px;
min-width: 208px;
min-height: 80px;
`;
🤖 Prompt for AI Agents
In src/components/group/GroupCard.tsx around lines 54 to 59, padding is declared
twice so the conditional padding for cardType === 'search' is being overridden
by the final `padding: 12px;`; remove the duplicate unconditional padding and
keep a single conditional padding expression (or invert the condition to apply
12px only when cardType !== 'search') so that the intended search padding is
respected.

Comment on lines +39 to +56
const fetchJoinedRooms = async () => {
try {
setLoading(true);
setError(null);
const response = await getJoinedRooms(1);

if (response.isSuccess) {
const convertedGroups = response.data.roomList.map(convertJoinedRoomToGroup);
setGroups(convertedGroups);
}
} catch (error) {
console.error('가입한 방 목록 조회 오류:', error);
setError('방 목록을 불러오는데 실패했습니다.');
setGroups([]);
} finally {
setLoading(false);
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 개선

API 호출 실패 시 에러 메시지를 설정하지만, 응답이 성공이 아닌 경우(response.isSuccess가 false)에 대한 처리가 없습니다.

   const fetchJoinedRooms = async () => {
     try {
       setLoading(true);
       setError(null);
       const response = await getJoinedRooms(1);

       if (response.isSuccess) {
         const convertedGroups = response.data.roomList.map(convertJoinedRoomToGroup);
         setGroups(convertedGroups);
+      } else {
+        setError(response.message || '방 목록을 불러오는데 실패했습니다.');
+        setGroups([]);
       }
     } catch (error) {
       console.error('가입한 방 목록 조회 오류:', error);
       setError('방 목록을 불러오는데 실패했습니다.');
       setGroups([]);
     } finally {
       setLoading(false);
     }
   };
📝 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 fetchJoinedRooms = async () => {
try {
setLoading(true);
setError(null);
const response = await getJoinedRooms(1);
if (response.isSuccess) {
const convertedGroups = response.data.roomList.map(convertJoinedRoomToGroup);
setGroups(convertedGroups);
}
} catch (error) {
console.error('가입한 방 목록 조회 오류:', error);
setError('방 목록을 불러오는데 실패했습니다.');
setGroups([]);
} finally {
setLoading(false);
}
};
const fetchJoinedRooms = async () => {
try {
setLoading(true);
setError(null);
const response = await getJoinedRooms(1);
if (response.isSuccess) {
const convertedGroups = response.data.roomList.map(convertJoinedRoomToGroup);
setGroups(convertedGroups);
} else {
setError(response.message || '방 목록을 불러오는데 실패했습니다.');
setGroups([]);
}
} catch (error) {
console.error('가입한 방 목록 조회 오류:', error);
setError('방 목록을 불러오는데 실패했습니다.');
setGroups([]);
} finally {
setLoading(false);
}
};
🤖 Prompt for AI Agents
In src/components/group/MyGroupBox.tsx around lines 39 to 56, the
fetchJoinedRooms function only handles exceptions but does nothing when
response.isSuccess is false; update the logic after awaiting getJoinedRooms(1)
to handle non-success responses by setting an appropriate error message (use
response.message or a default string), clearing or setting groups to an empty
array, and logging the response for debugging; keep the existing
try/catch/finally structure but add an else branch for response.isSuccess to
call setError(...), setGroups([]) and console.error with the response details so
failures without thrown exceptions are surfaced.

Comment on lines +140 to 142
navigate(`/group/detail/${response.data.roomId}`, {
replace: true,
state: { roomId: response.data.roomId },
});
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

경로 기반 상세 이동으로 전환된 점 LGTM. roomId 존재성만 한 번 더 확인해주세요.

/group/detail/:roomId로의 네비게이션 변경은 라우팅 개편과 일치합니다. 위 코멘트대로 roomId null 가드를 추가하면 더 안전합니다.

아래 스크립트로 프로젝트 전반에서 과거 정적 경로(/group/detail) 사용 흔적이 남아 있지 않은지 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# 프로젝트 내 '/group/detail' 정적 경로 사용 여부 점검 (예: Link, navigate 등)
rg -n -C2 --type-add 'tsx:*.tsx' --type-add 'ts:*.ts' --type tsx,ts \
  -e $'["\']\\/group\\/detail["\']' \
  -e $'to=\\{?[\'"`]\\/group\\/detail[\'"`]\\}?'

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# 프로젝트 내 '/group/detail' 정적 경로 사용 여부 점검 (ts, tsx, js, jsx 파일 대상)
echo "Searching repository for '/group/detail' occurrences..."
rg -n -S --fixed-strings '/group/detail' \
  --type-add 'tsx:*.tsx' --type-add 'ts:*.ts' \
  --type-add 'jsx:*.jsx' --type-add 'js:*.js' \
  --type ts,tsx,js,jsx || true

Length of output: 341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for literal '/group/detail' in repository..."
rg -n -S --fixed-strings '/group/detail' \
  --hidden --glob '!**/node_modules/**' \
  --glob '**/*.ts' --glob '**/*.tsx' --glob '**/*.js' --glob '**/*.jsx' --glob '**/*.json' --glob '**/*.md' || true

echo
echo "Searching for 'response.data.roomId' occurrences..."
rg -n -S 'response\.data\.roomId' --hidden --glob '!**/node_modules/**' --glob '**/*.{ts,tsx,js,jsx}' || true

echo
echo "Showing src/pages/group/CreateGroup.tsx snippet (lines 1-240) if file exists..."
if [ -f src/pages/group/CreateGroup.tsx ]; then
  sed -n '1,240p' src/pages/group/CreateGroup.tsx | nl -ba -w3 -s': '
else
  echo "File not found: src/pages/group/CreateGroup.tsx"
fi

Length of output: 1228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== CreateGroup.tsx 파일 내용 확인 ==="
if [ -f src/pages/group/CreateGroup.tsx ]; then
  cat src/pages/group/CreateGroup.tsx | head -n 150 | tail -n +130
else
  echo "파일을 찾을 수 없습니다: src/pages/group/CreateGroup.tsx"
fi

echo
echo "=== RecruitingGroupBox.tsx 파일 내용 확인 ==="
if [ -f src/components/group/RecruitingGroupBox.tsx ]; then
  cat src/components/group/RecruitingGroupBox.tsx | head -n 40 | tail -n +20
else
  echo "파일을 찾을 수 없습니다: src/components/group/RecruitingGroupBox.tsx"
fi

echo
echo "=== response.data.roomId 사용 패턴 분석 ==="
rg -n -A5 -B5 'response\.data\.roomId' src/pages/group/CreateGroup.tsx || true

Length of output: 2198


경로 기반 네비게이션은 OK — CreateGroup.tsx에서 roomId null 가드 및 isSuccess 중복 수정 필요

간단 요약: 레포 전체 검색 결과 '/group/detail'은 아래 두 파일에서만 사용됩니다. CreateGroup.tsx에서 response.data.roomId를 바로 사용하고 있고, 같은 파일에 isSuccess 검사(중복)도 확인되어 수정이 필요합니다.

수정 대상(요점)

  • src/pages/group/CreateGroup.tsx (약 135–142)

    • 문제: const isSuccessful = response.isSuccess || response.isSuccess; (중복)
    • 문제: navigate(\/group/detail/${response.data.roomId}`)` — response.data.roomId가 없을 수 있음.
    • 제안 변경:
      // 기존
      const isSuccessful = response.isSuccess || response.isSuccess;
      if (isSuccessful) {
        navigate(`/group/detail/${response.data.roomId}`, { replace: true });
      }
      
      // 제안
      const isSuccessful = !!response?.isSuccess; // 혹은 API 스펙에 맞게 조정
      const roomId = response?.data?.roomId;
      if (isSuccessful && roomId != null) {
        navigate(`/group/detail/${roomId}`, { replace: true });
      } else {
        alert(`방 생성에 실패했습니다: ${response?.message} (코드: ${response?.code})`);
      }
  • src/components/group/RecruitingGroupBox.tsx (약 29)

    • 상태: navigate(\/group/detail/${roomId}`)사용. roomId는 인자/파싱으로 생성되어 있고response.isSuccess` 확인 후 호출되어 현재는 추가 변경 불필요. 다만 API 응답 포맷이 바뀔 가능성이 있다면 일관성 차원에서 검토 권장.

태그:

🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 135 to 142, fix the duplicated
isSuccess assignment and guard against a missing roomId before navigating:
replace the redundant "const isSuccessful = response.isSuccess ||
response.isSuccess;" with a proper boolean check (e.g. const isSuccessful =
!!response?.isSuccess), read roomId with a safe optional chain (const roomId =
response?.data?.roomId), and only call navigate(`/group/detail/${roomId}`, {
replace: true }) when isSuccessful && roomId != null; otherwise surface an error
(e.g. alert or set error state) using response?.message and response?.code.

Comment on lines +26 to +28
deadLine: Math.ceil(
(new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

날짜 계산 로직 개선 필요

deadLine 계산 시 음수 값이 발생할 수 있습니다. 마감일이 이미 지난 경우를 처리해야 합니다.

   deadLine: Math.ceil(
     (new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
-  ),
+  ) || 0,  // 음수인 경우 0으로 처리

더 나은 방법은 Math.max를 사용하는 것입니다:

-  deadLine: Math.ceil(
-    (new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
-  ),
+  deadLine: Math.max(0, Math.ceil(
+    (new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
+  )),
📝 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
deadLine: Math.ceil(
(new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
),
deadLine: Math.max(0, Math.ceil(
(new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24),
)),
🤖 Prompt for AI Agents
In src/pages/group/Group.tsx around lines 26 to 28, the deadLine calculation can
produce negative values when the deadline has passed; clamp the computed days to
a minimum of 0 (e.g., wrap the current Math.ceil result with Math.max(0, ...))
and optionally use Date.now() for the current timestamp to simplify the
expression.

Comment on lines +42 to +75
const fetchAllRoomsData = async () => {
try {
const categories = ['문학', '인문학', '사회과학', '과학·IT', '예술'];
const deadlineRoomsData: GroupType[] = [];
const popularRoomsData: GroupType[] = [];

for (const category of categories) {
const response = await getRoomsByCategory(category);
if (response.isSuccess) {
const deadlineGroups = response.data.deadlineRoomList.map(room =>
convertRoomItemToGroup(room, category, 'deadline'),
);
const popularGroups = response.data.popularRoomList.map(room =>
convertRoomItemToGroup(room, category, 'popular'),
);
deadlineRoomsData.push(...deadlineGroups);
popularRoomsData.push(...popularGroups);
}
}

setSections([
{ title: '마감 임박한 독서 모임방', groups: deadlineRoomsData },
{ title: '인기 있는 독서 모임방', groups: popularRoomsData },
{ title: '인플루언서·작가 독서 모임방', groups: [] },
]);
} catch (error) {
console.error('방 목록 조회 오류:', error);
setSections([
{ title: '마감 임박한 독서 모임방', groups: [] },
{ title: '인기 있는 독서 모임방', groups: [] },
{ title: '인플루언서·작가 독서 모임방', groups: [] },
]);
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 요청 최적화 필요

현재 5개의 카테고리에 대해 순차적으로 API를 호출하고 있어 성능이 저하될 수 있습니다. Promise.all을 사용하여 병렬 처리하는 것이 좋습니다.

   const fetchAllRoomsData = async () => {
     try {
       const categories = ['문학', '인문학', '사회과학', '과학·IT', '예술'];
-      const deadlineRoomsData: GroupType[] = [];
-      const popularRoomsData: GroupType[] = [];
-
-      for (const category of categories) {
-        const response = await getRoomsByCategory(category);
-        if (response.isSuccess) {
-          const deadlineGroups = response.data.deadlineRoomList.map(room =>
-            convertRoomItemToGroup(room, category, 'deadline'),
-          );
-          const popularGroups = response.data.popularRoomList.map(room =>
-            convertRoomItemToGroup(room, category, 'popular'),
-          );
-          deadlineRoomsData.push(...deadlineGroups);
-          popularRoomsData.push(...popularGroups);
-        }
-      }
+      
+      const responses = await Promise.all(
+        categories.map(category => getRoomsByCategory(category))
+      );
+      
+      const deadlineRoomsData: GroupType[] = [];
+      const popularRoomsData: GroupType[] = [];
+      
+      responses.forEach((response, index) => {
+        if (response.isSuccess) {
+          const category = categories[index];
+          const deadlineGroups = response.data.deadlineRoomList.map(room =>
+            convertRoomItemToGroup(room, category, 'deadline'),
+          );
+          const popularGroups = response.data.popularRoomList.map(room =>
+            convertRoomItemToGroup(room, category, 'popular'),
+          );
+          deadlineRoomsData.push(...deadlineGroups);
+          popularRoomsData.push(...popularGroups);
+        }
+      });

       setSections([
         { title: '마감 임박한 독서 모임방', groups: deadlineRoomsData },
         { title: '인기 있는 독서 모임방', groups: popularRoomsData },
         { title: '인플루언서·작가 독서 모임방', groups: [] },
       ]);
     } catch (error) {
       console.error('방 목록 조회 오류:', error);
       setSections([
         { title: '마감 임박한 독서 모임방', groups: [] },
         { title: '인기 있는 독서 모임방', groups: [] },
         { title: '인플루언서·작가 독서 모임방', groups: [] },
       ]);
     }
   };
📝 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 fetchAllRoomsData = async () => {
try {
const categories = ['문학', '인문학', '사회과학', '과학·IT', '예술'];
const deadlineRoomsData: GroupType[] = [];
const popularRoomsData: GroupType[] = [];
for (const category of categories) {
const response = await getRoomsByCategory(category);
if (response.isSuccess) {
const deadlineGroups = response.data.deadlineRoomList.map(room =>
convertRoomItemToGroup(room, category, 'deadline'),
);
const popularGroups = response.data.popularRoomList.map(room =>
convertRoomItemToGroup(room, category, 'popular'),
);
deadlineRoomsData.push(...deadlineGroups);
popularRoomsData.push(...popularGroups);
}
}
setSections([
{ title: '마감 임박한 독서 모임방', groups: deadlineRoomsData },
{ title: '인기 있는 독서 모임방', groups: popularRoomsData },
{ title: '인플루언서·작가 독서 모임방', groups: [] },
]);
} catch (error) {
console.error('방 목록 조회 오류:', error);
setSections([
{ title: '마감 임박한 독서 모임방', groups: [] },
{ title: '인기 있는 독서 모임방', groups: [] },
{ title: '인플루언서·작가 독서 모임방', groups: [] },
]);
}
};
const fetchAllRoomsData = async () => {
try {
const categories = ['문학', '인문학', '사회과학', '과학·IT', '예술'];
const responses = await Promise.all(
categories.map(category => getRoomsByCategory(category))
);
const deadlineRoomsData: GroupType[] = [];
const popularRoomsData: GroupType[] = [];
responses.forEach((response, index) => {
if (response.isSuccess) {
const category = categories[index];
const deadlineGroups = response.data.deadlineRoomList.map(room =>
convertRoomItemToGroup(room, category, 'deadline'),
);
const popularGroups = response.data.popularRoomList.map(room =>
convertRoomItemToGroup(room, category, 'popular'),
);
deadlineRoomsData.push(...deadlineGroups);
popularRoomsData.push(...popularGroups);
}
});
setSections([
{ title: '마감 임박한 독서 모임방', groups: deadlineRoomsData },
{ title: '인기 있는 독서 모임방', groups: popularRoomsData },
{ title: '인플루언서·작가 독서 모임방', groups: [] },
]);
} catch (error) {
console.error('방 목록 조회 오류:', error);
setSections([
{ title: '마감 임박한 독서 모임방', groups: [] },
{ title: '인기 있는 독서 모임방', groups: [] },
{ title: '인플루언서·작가 독서 모임방', groups: [] },
]);
}
};
🤖 Prompt for AI Agents
In src/pages/group/Group.tsx around lines 42 to 75, the current implementation
calls getRoomsByCategory sequentially for each category which is slow; change it
to kick off all requests in parallel (e.g., map categories to getRoomsByCategory
promises and await Promise.all), then iterate the returned responses to build
deadlineRoomsData and popularRoomsData (skip or handle non-success responses),
and finally call setSections as before; preserve the try/catch error handling
and ensure any individual API failures are handled (filter or check isSuccess)
so one failed category doesn't break all results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant