Skip to content

feat: 진행중인 방 상세보기 및 독서메이트 조회 API 연동 구현#122

Merged
ljh130334 merged 8 commits intodevelopfrom
feat/api-rooms-detail
Aug 15, 2025
Merged

feat: 진행중인 방 상세보기 및 독서메이트 조회 API 연동 구현#122
ljh130334 merged 8 commits intodevelopfrom
feat/api-rooms-detail

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Aug 15, 2025

#️⃣ 연관된 이슈

#106

📝 작업 내용

진행중인 방 상세보기 페이지와 독서메이트 조회 기능을 API와 연동하여 구현했습니다.

🕸️ 주요 구현 사항

1. 진행중인 방 상세보기 API 연동

  • API 파일 생성: /rooms/{roomId}/playing 엔드포인트를 호출하는 getRoomPlaying.ts API 함수를 구현했습니다.
  • 타입 정의: 스웨거 명세에 맞는 RoomPlayingResponse, CurrentVote, VoteItem 등의 타입을 정의했습니다.
  • 데이터 변환 함수: API 응답의 currentVotes 데이터를 기존 HotTopicSection 컴포넌트에서 사용하는 Poll 형태로 변환하는 convertVotesToPolls 함수를 구현했습니다.
  • ParticipatedGroupDetail 컴포넌트 개선: 기존 목킹 데이터를 제거하고 실제 API 데이터를 사용하도록 수정했습니다. useParams를 통해 roomId를 받아 API를 호출하며, 로딩/에러 상태도 적절히 처리합니다.

2. 독서메이트 조회 API 연동

  • API 파일 생성: /rooms/{roomId}/users 엔드포인트를 호출하는 getRoomMembers.ts API 함수를 구현했습니다.
  • 타입 정의: RoomMember, RoomMembersResponse 타입을 스웨거 명세에 맞게 정의했습니다.
  • 데이터 변환: API 응답의 userList 데이터를 기존 MemberList 컴포넌트에서 사용하는 Member 형태로 변환하는 함수를 구현했습니다.
  • GroupMembers 컴포넌트 개선: 기존 목킹 데이터를 제거하고 실제 API 연동을 구현했습니다. roomId 파라미터 처리를 위해 localStorage 백업 로직도 추가했습니다.

3. 데이터 매핑 및 변환

  • 진행중인 방 데이터: roomName, roomDescription, progressStartDate/progressEndDate, memberCount, category, bookTitle/authorName, currentPage/userPercentage, currentVotes 등을 UI 컴포넌트에 맞게 매핑했습니다.
  • 독서메이트 데이터: userId→id, nickname→nickname, alias→role, subscriberCount→followersCount, imageUrl→profileImageUrl 등의 필드 매핑을 구현했습니다.
  • 날짜 포맷팅: API 응답의 YYYY-MM-DD 형식을 UI에서 사용하는 YYYY.MM.DD 형식으로 변환했습니다.

Summary by CodeRabbit

  • New Features
    • 그룹 상세(참여중) 화면이 실데이터로 동작하며 투표(핫토픽)를 UI용 폼으로 변환해 표시합니다. 로딩·에러 상태 UI가 추가되었습니다.
    • 그룹 멤버 목록이 실데이터로 표시되고, 원격 프로필 이미지를 표시하며 비어있음/로딩/에러 상태 UI를 제공합니다. 멤버 클릭 시 다른 사용자 피드로 이동합니다.
  • Refactor
    • 라우트가 roomId 파라미터 기반으로 변경되었습니다.
  • Style
    • 프로필 이미지 크기 조정 및 대체 텍스트 추가로 접근성·레이아웃 개선.

@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 Ready Preview Comment Aug 15, 2025 5:24pm

@coderabbitai
Copy link

coderabbitai bot commented Aug 15, 2025

Walkthrough

새 API 모듈 두 개(getRoomMembers, getRoomPlaying)를 추가하고, 그룹 상세 및 멤버 페이지를 목(mock) 데이터에서 API 기반으로 전환했다. 라우트를 roomId 파라미터화하고 로딩/에러/빈 상태 UI와 프로필 이미지 조건부 렌더링, 투표→Poll 변환 유틸을 도입했다.

Changes

Cohort / File(s) Change Summary
Rooms API: Members & Playing
src/api/rooms/getRoomMembers.ts, src/api/rooms/getRoomPlaying.ts
새 API 모듈 추가. 멤버/플레잉 응답 타입 정의, GET 호출 구현, 오류 로그 후 재throw. 변환 유틸 제공: convertRoomMembersToMembers, convertVotesToPolls. 다수의 타입 및 함수가 공개(export)됨.
Group Detail: Participated view
src/pages/groupDetail/ParticipatedGroupDetail.tsx
roomId 파라미터로 getRoomPlaying 호출, 로딩/에러 상태 관리, UI를 API 데이터로 교체. convertVotesToPolls 적용, 네비게이션 경로에 roomId 반영, 날짜 포맷팅 추가. GroupActionBottomSheet prop에 onReportGroup 추가.
Members UI & Styling
src/pages/groupMembers/GroupMembers.tsx, src/pages/groupMembers/GroupMembers.styled.ts, src/components/members/MemberList.tsx, src/components/members/MemberList.styled.ts
멤버 페이지를 API 연동으로 전환(데이터 fetch/변환/상태). 로딩·에러·빈 상태용 컨테이너 추가(export). MemberListprofileImageUrl 있을 때 실제 이미지(ProfileImageWithSrc) 렌더, 없으면 기본 아바타; 프로필 이미지 크기 48→36px로 조정. 멤버 클릭 시 경로를 /otherfeed/{id}로 변경.
Routing
src/pages/index.tsx
라우트 파라미터화: group/detail/joined/:roomId, group/:roomId/members로 변경(기존 group/detail/:roomId 유지).

Sequence Diagram(s)

sequenceDiagram
  participant UI as GroupMembers.tsx
  participant API as getRoomMembers
  participant HTTP as apiClient
  UI->>API: getRoomMembers(roomId)
  API->>HTTP: GET /rooms/{roomId}/users
  HTTP-->>API: { data: RoomMembersResponse }
  API-->>UI: RoomMembersResponse.data
  UI->>UI: convertRoomMembersToMembers()
  UI-->>UI: 상태 갱신 및 렌더링
Loading
sequenceDiagram
  participant UI as ParticipatedGroupDetail
  participant API as getRoomPlaying
  participant HTTP as apiClient
  UI->>API: getRoomPlaying(roomId)
  API->>HTTP: GET /rooms/{roomId}/playing
  HTTP-->>API: { data: RoomPlayingResponse }
  API-->>UI: RoomPlayingResponse
  UI->>UI: convertVotesToPolls(currentVotes)
  UI-->>UI: 로딩/에러 처리 후 화면 업데이트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related PRs

Suggested labels

✨ Feature, 📬 API

Suggested reviewers

  • heeeeyong

Poem

토끼가 부르르 — 방 문을 열면 🐇
사진이 반짝, 멤버가 쫙— 나타나요 📸
투표는 휙— Poll로 둥글게 변하고
로딩은 살며시, 에러는 “조금만요”
roomId 따라 달리는 코드, 즐겁게 껑충✨

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

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.

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

🧹 Nitpick comments (9)
src/pages/groupMembers/GroupMembers.styled.ts (1)

15-42: 중복 스타일 추출 + 색상 토큰 일관성 유지 제안

세 개 컨테이너(Loading/Error/Empty)가 거의 동일한 레이아웃 속성을 반복합니다. 중복 제거를 위해 Base 컨테이너를 추출하고, 색상은 기존 Wrapper가 사용하는 theme 토큰(colors)과 일관되게 맞추는 것을 권장합니다. 유지보수성과 디자인 일관성이 향상됩니다.

예시(참고용, 새 컴포넌트 추가가 필요하므로 일반 코드 블록으로 제안):

import styled from '@emotion/styled';
import { colors } from '../../styles/global/global';

const StateContainer = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  height: 200px;
  font-size: var(--string-size-base, 16px);
`;

export const LoadingContainer = styled(StateContainer)`
  color: ${colors.grey['200']};
`;

export const ErrorContainer = styled(StateContainer)`
  color: ${colors.red.main};
  text-align: center;
  padding: 20px;
`;

export const EmptyContainer = styled(StateContainer)`
  color: ${colors.grey['200']};
`;
src/api/rooms/getRoomPlaying.ts (1)

52-62: Poll/옵션 ID를 index 기반으로 생성 → 보다 안정적인 키로 개선 제안

리스트 리렌더/정렬 변경 시 index 기반 ID는 키 불안정 문제를 유발할 수 있습니다. 페이지번호+내용 조합 등 보다 안정적인 키로 만드는 것을 권장합니다. 또한 voteItems가 가끔 빈/누락될 경우를 방어하면 좋습니다.

 export const convertVotesToPolls = (currentVotes: CurrentVote[]): Poll[] => {
-  return currentVotes.map((vote, index) => ({
-    id: index.toString(),
-    question: vote.content,
-    options: vote.voteItems.map((item, itemIndex) => ({
-      id: itemIndex.toString(),
-      text: item.itemName,
-    })),
-    pageNumber: vote.page,
-  }));
+  return currentVotes.map((vote, voteIndex) => {
+    const pollId = `${vote.page}:${vote.content}`; // 안정적 키(필요시 slugify 고려)
+    const items = (vote.voteItems ?? []).map((item, itemIndex) => ({
+      id: `${vote.page}:${item.itemName}:${itemIndex}`,
+      text: item.itemName,
+    }));
+    return {
+      id: pollId,
+      question: vote.content,
+      options: items,
+      pageNumber: vote.page,
+    };
+  });
 };

필요 시 slugify 유틸을 도입해 공백/특수문자 처리도 가능하게 할 수 있습니다.

src/pages/groupMembers/GroupMembers.tsx (3)

26-38: roomId 기본값 '1' 사용은 오작동 가능성 — 유효성 검사로 안전하게 처리하세요

현재 '|| "1"' 기본값 때문에 잘못된 방으로 API를 호출할 수 있고, 직전의 if (!currentRoomId) 가드는 사실상 도달 불가입니다. 숫자 여부를 검증해 NaN을 차단하고, 유효하지 않으면 에러로 처리하는 편이 안전합니다.

적용 예시:

-      // roomId 우선 순위: URL 파라미터 > localStorage > 기본값 1
-      const currentRoomId = roomId || localStorage.getItem('currentRoomId') || '1';
-
-      if (!currentRoomId) {
-        setError('방 ID를 찾을 수 없습니다.');
-        setLoading(false);
-        return;
-      }
+      // roomId 우선 순위: URL 파라미터 > localStorage; 유효하지 않으면 에러 처리
+      const rawRoomId = roomId ?? localStorage.getItem('currentRoomId');
+      const parsedRoomId = Number(rawRoomId);
+      if (!Number.isFinite(parsedRoomId)) {
+        setError('유효한 방 ID를 찾을 수 없습니다.');
+        setLoading(false);
+        return;
+      }
       try {
         setLoading(true);
-        const response: RoomMembersResponse = await getRoomMembers(parseInt(currentRoomId));
+        const response: RoomMembersResponse = await getRoomMembers(parsedRoomId);

24-54: 언마운트 시 setState 호출 방지하여 경고/메모리릭 예방

비동기 호출 도중 언마운트되면 setState가 경고를 유발할 수 있습니다. 마운트 여부 플래그로 가드해 주세요.

   useEffect(() => {
-    const fetchMembers = async () => {
+    let mounted = true;
+    const fetchMembers = async () => {
       // roomId 우선 순위: URL 파라미터 > localStorage > 기본값 1
       const currentRoomId = roomId || localStorage.getItem('currentRoomId') || '1';

       if (!currentRoomId) {
-        setError('방 ID를 찾을 수 없습니다.');
-        setLoading(false);
+        if (mounted) setError('방 ID를 찾을 수 없습니다.');
+        if (mounted) setLoading(false);
         return;
       }

       try {
-        setLoading(true);
+        if (mounted) setLoading(true);
         const response: RoomMembersResponse = await getRoomMembers(parseInt(currentRoomId));

         if (response.isSuccess) {
           const convertedMembers = convertRoomMembersToMembers(response.data.userList);
-          setMembers(convertedMembers);
+          if (mounted) setMembers(convertedMembers);
         } else {
-          setError(response.message);
+          if (mounted) setError(response.message);
         }
       } catch (err) {
-        setError('독서메이트 목록을 불러오는 중 오류가 발생했습니다.');
+        if (mounted) setError('독서메이트 목록을 불러오는 중 오류가 발생했습니다.');
         console.error('독서메이트 조회 오류:', err);
       } finally {
-        setLoading(false);
+        if (mounted) setLoading(false);
       }
     };

     fetchMembers();
-  }, [roomId]);
+    return () => {
+      mounted = false;
+    };
+  }, [roomId]);

10-12: 중복 타입 표기 제거로 가독성 개선

getRoomMembers의 반환 타입이 이미 제네릭으로 정의되어 있어 RoomMembersResponse 타입 표기는 중복입니다. import도 함께 정리할 수 있습니다.

   import {
     getRoomMembers,
     convertRoomMembersToMembers,
-  type Member,
-  type RoomMembersResponse,
+  type Member,
   } from '@/api/rooms/getRoomMembers';
-        const response: RoomMembersResponse = await getRoomMembers(parseInt(currentRoomId));
+        const response = await getRoomMembers(parseInt(currentRoomId));

Also applies to: 37-37

src/pages/groupDetail/ParticipatedGroupDetail.tsx (4)

61-66: roomId 숫자 검증 추가로 잘못된 API 호출 차단

parseInt(roomId)는 '12abc' 같은 문자열도 12로 파싱합니다. 숫자 여부를 엄격히 검증하고, 유효하지 않으면 조기에 반환하세요.

-      if (!roomId) {
-        setError('방 ID가 없습니다.');
+      const parsedRoomId = Number(roomId);
+      if (!Number.isFinite(parsedRoomId)) {
+        setError('유효한 방 ID가 없습니다.');
         setLoading(false);
         return;
       }
...
-        const response = await getRoomPlaying(parseInt(roomId));
+        const response = await getRoomPlaying(parsedRoomId);

Also applies to: 69-71


152-168: 로딩/에러 상태에서 뒤로가기 버튼 미노출 — 간단한 헤더 추가 제안

에러/로딩 화면에 상단 뒤로가기가 없으면 사용자가 갇힐 수 있습니다. 최소한의 헤더만 노출해 주세요.

   if (loading) {
     return (
       <ParticipatedWrapper>
-        <LoadingContainer>로딩 중...</LoadingContainer>
+        <Header>
+          <IconButton src={leftArrow} onClick={handleBackButton} />
+        </Header>
+        <LoadingContainer>로딩 중...</LoadingContainer>
       </ParticipatedWrapper>
     );
   }

   // 에러 상태
   if (error || !roomData) {
     return (
       <ParticipatedWrapper>
-        <ErrorContainer>{error || '데이터를 불러올 수 없습니다.'}</ErrorContainer>
+        <Header>
+          <IconButton src={leftArrow} onClick={handleBackButton} />
+        </Header>
+        <ErrorContainer>{error || '데이터를 불러올 수 없습니다.'}</ErrorContainer>
       </ParticipatedWrapper>
     );
   }

59-86: 언마운트 시 setState 호출 방지

API 대기 중 페이지 이탈 시 setState 경고를 막기 위해 마운트 가드를 추가하세요.

   useEffect(() => {
-    const fetchRoomDetail = async () => {
+    let mounted = true;
+    const fetchRoomDetail = async () => {
       if (!roomId) {
-        setError('방 ID가 없습니다.');
-        setLoading(false);
+        if (mounted) setError('방 ID가 없습니다.');
+        if (mounted) setLoading(false);
         return;
       }

       try {
-        setLoading(true);
+        if (mounted) setLoading(true);
         const response = await getRoomPlaying(parseInt(roomId));

         if (response.isSuccess) {
-          setRoomData(response);
+          if (mounted) setRoomData(response);
         } else {
-          setError(response.message);
+          if (mounted) setError(response.message);
         }
       } catch (err) {
-        setError('방 정보를 불러오는 중 오류가 발생했습니다.');
+        if (mounted) setError('방 정보를 불러오는 중 오류가 발생했습니다.');
         console.error('방 상세 정보 조회 오류:', err);
       } finally {
-        setLoading(false);
+        if (mounted) setLoading(false);
       }
     };

     fetchRoomDetail();
-  }, [roomId]);
+    return () => {
+      mounted = false;
+    };
+  }, [roomId]);

181-185: 주석과 구현 불일치 정정

주석은 categoryColor 사용을 언급하지만 실제로는 data.category를 반환합니다. 혼동을 줄이도록 주석을 현재 구현에 맞춰 주세요.

-  // 장르에 따른 배경색 결정 (카테고리 컬러 사용)
+  // 장르(카테고리) 기반 배경 이미지 결정
   const getGenreForBackground = () => {
-    // categoryColor를 사용하거나 기본값으로 장르명 사용
-    return data.category;
+    // 현재는 API의 category 문자열을 그대로 사용
+    return data.category;
   };
📜 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 efa5f1b and 7829941.

📒 Files selected for processing (7)
  • src/api/rooms/getRoomMembers.ts (1 hunks)
  • src/api/rooms/getRoomPlaying.ts (1 hunks)
  • src/components/members/MemberList.tsx (3 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx (6 hunks)
  • src/pages/groupMembers/GroupMembers.styled.ts (1 hunks)
  • src/pages/groupMembers/GroupMembers.tsx (2 hunks)
  • src/pages/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/api/rooms/getRoomMembers.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/api/rooms/getRoomPlaying.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/components/members/MemberList.tsx (1)
src/components/members/MemberList.styled.ts (1)
  • ProfileImage (50-56)
src/pages/groupMembers/GroupMembers.tsx (3)
src/api/rooms/getRoomMembers.ts (4)
  • Member (22-28)
  • RoomMembersResponse (12-19)
  • getRoomMembers (46-54)
  • convertRoomMembersToMembers (30-44)
src/mocks/members.mock.ts (1)
  • Member (1-8)
src/pages/groupMembers/GroupMembers.styled.ts (4)
  • Wrapper (4-13)
  • LoadingContainer (15-22)
  • ErrorContainer (24-33)
  • EmptyContainer (35-42)
src/pages/groupDetail/ParticipatedGroupDetail.tsx (5)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/api/rooms/getRoomPlaying.ts (4)
  • RoomPlayingResponse (17-41)
  • getRoomPlaying (64-72)
  • Poll (44-49)
  • convertVotesToPolls (52-62)
src/pages/groupDetail/ParticipatedGroupDetail.styled.ts (1)
  • ParticipatedWrapper (4-16)
src/components/group/HotTopicSection.tsx (1)
  • Poll (25-30)
src/pages/groupDetail/GroupDetail.styled.ts (1)
  • TopBackground (19-28)
🔇 Additional comments (9)
src/pages/groupMembers/GroupMembers.styled.ts (1)

15-22: 전역 CSS 변수 사용 여부 확인 요청

현재 var(--color-grey-200), var(--color-red), var(--string-size-base) CSS 변수를 사용하고 있습니다. 전역에서 해당 변수들이 항상 정의되는지(특히 비로그인/다크모드 등 테마 전환 시) 확인 부탁드립니다. 미정의 시 폴백 색상/크기 지정이 필요할 수 있습니다.

Also applies to: 24-33, 35-42

src/pages/index.tsx (2)

56-57: 라우팅 파라미터화 적절

  • group/detail/joined/:roomId, group/:roomId/members 경로 정의가 명확하고 기존 group/detail/:roomId와 충돌하지 않습니다(정적 세그먼트가 더 높은 우선순위).

56-57: 확인: 정적 경로 사용처 없음 — 라우트 정의만 존재

저장소 전체(rg) 검색 결과 '/group/detail/joined' 및 '/group/members' 문자열은 src/pages/index.tsx의 라우트 정의에서만 발견되었습니다. 다른 파일에서 정적 경로로 링크하거나 navigate하는 사용처는 없습니다. 따라서 내부 링크 업데이트 누락은 없습니다.

  • src/pages/index.tsx
    • 56행: <Route path="group/detail/joined/:roomId" element={} />
    • 57행: <Route path="group/:roomId/members" element={} />
src/components/members/MemberList.tsx (1)

5-5: 타입 소스 전환 적절

Member 타입을 목에서 API 기반 타입으로 전환한 점 좋습니다. 하위 컴포넌트 사용처에서도 동일 타입을 공유하게 되어 유지보수에 유리합니다.

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

3-9: Swagger 필드명 재확인 요청 (alias/aliasName, followerCount/subscriberCount)

PR 설명에는 subscriberCount → followersCount 매핑이라고 되어 있는데, 인터페이스는 followerCount만 정의하고 있습니다. 또한 alias vs aliasName 필드명도 확인이 필요해 보입니다. 실제 응답 스키마와 불일치 시 팔로워 수가 0으로만 표시되거나 역할명이 누락될 수 있습니다.

src/pages/groupMembers/GroupMembers.tsx (2)

65-95: 로딩/에러 상태에서의 헤더 포함, UX 일관성 좋습니다

Loading/Error 조기 반환에서도 TitleHeader를 노출해 뒤로가기를 보장하는 패턴은 사용자 경험 개선에 유익합니다.


60-63: 확인 완료 — /otherfeed/:memberId 네비게이션은 유효합니다 (/otherfeed/:userId 라우트 존재)

src/pages/index.tsx에 Route path="otherfeed/:userId"가 정의되어 있고, OtherFeedPage는 useParams<{ userId: string }>()를 사용하므로 navigate(/otherfeed/${memberId})로 생성되는 /otherfeed/ 경로에 정상 매칭됩니다.

문제 없음(참고 위치)

  • src/pages/index.tsx — 69: <Route path="otherfeed/:userId" element={} />
  • src/pages/feed/OtherFeedPage.tsx — useParams<{ userId: string }>() (userId로 파라미터 사용)
  • src/pages/groupMembers/GroupMembers.tsx — handleMemberClick: navigate(/otherfeed/${memberId}) (라인 60-63)
  • 기타 호출 예: src/components/members/MemberList.tsx, src/components/feed/UserProfileItem.tsx, src/components/feed/FollowList.tsx, src/components/common/Post/PostHeader.tsx 등 (모두 /otherfeed/{id}로 네비게이트)
src/pages/groupDetail/ParticipatedGroupDetail.tsx (2)

243-271: 데이터 매핑 전반 LGTM

  • 책 정보, 진행 현황, 멤버 수, 장르, 공개 여부 등 UI 매핑이 Swagger 스키마와 일관됩니다.
  • Poll 변환 로직을 통한 HotTopicSection 연동도 자연스럽습니다.

172-175: HotTopicSection의 Poll 타입 호환성 — 검증 완료 (문제 없음)

convertVotesToPolls가 반환하는 Poll 구조가 HotTopicSection이 기대하는 Poll/VoteOption 구조와 일치함을 확인했습니다.

  • src/api/rooms/getRoomPlaying.ts
    • export interface Poll { id: string; question: string; options: { id: string; text: string }[]; pageNumber: number } (≈라인 44)
    • convertVotesToPolls는 vote.voteItems[].itemName을 options[].text로 매핑함 (export const convertVotesToPolls … ≈라인 52)
  • src/components/group/HotTopicSection.tsx
    • export interface VoteOption { id: string; text: string } (≈라인 20)
    • export interface Poll { id: string; question: string; options: VoteOption[]; pageNumber: number } (≈라인 25)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx
    • const polls: Poll[] = convertVotesToPolls(data.currentVotes); 후 HotTopicSection에 그대로 전달됨 (≈라인 173)

결론: 구조적(Structural) 타입이 동일하므로 변경 불필요합니다.

Comment on lines +30 to +44
export const convertRoomMembersToMembers = (roomMembers: RoomMember[]): Member[] => {
const convertedMembers = roomMembers.map(member => {
const convertedMember: Member = {
id: member.userId.toString(),
nickname: member.nickname || '익명',
role: member.aliasName || '독서메이트',
followersCount: member.followerCount || 0,
profileImageUrl: member.imageUrl || undefined,
};

return convertedMember;
});

return convertedMembers;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

필드명 변동에 견고한 매핑 구현 (followerCount/subscriberCount 모두 지원)

서버 필드명이 변동될 가능성에 대비해 두 키를 모두 안전하게 처리하는 편이 좋습니다. 아래처럼 인터페이스에 subscriberCount?를 추가하고, 변환 함수에서 nullish coalescing을 사용하세요.

 export interface RoomMember {
   userId: number;
   nickname: string;
   imageUrl: string;
-  aliasName: string;
-  followerCount: number;
+  aliasName: string;
+  followerCount?: number;
+  subscriberCount?: number;
 }
 
 export const convertRoomMembersToMembers = (roomMembers: RoomMember[]): Member[] => {
-  const convertedMembers = roomMembers.map(member => {
-    const convertedMember: Member = {
-      id: member.userId.toString(),
-      nickname: member.nickname || '익명',
-      role: member.aliasName || '독서메이트',
-      followersCount: member.followerCount || 0,
-      profileImageUrl: member.imageUrl || undefined,
-    };
-
-    return convertedMember;
-  });
-
-  return convertedMembers;
+  return roomMembers.map(member => ({
+    id: member.userId.toString(),
+    nickname: member.nickname || '익명',
+    role: member.aliasName || '독서메이트',
+    followersCount: member.followerCount ?? member.subscriberCount ?? 0,
+    profileImageUrl: member.imageUrl || undefined,
+  }));
 };

원하시면 간단한 단위 테스트 케이스(두 키 각각/동시에 존재/둘 다 없음)를 추가해 드릴게요.

📝 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 convertRoomMembersToMembers = (roomMembers: RoomMember[]): Member[] => {
const convertedMembers = roomMembers.map(member => {
const convertedMember: Member = {
id: member.userId.toString(),
nickname: member.nickname || '익명',
role: member.aliasName || '독서메이트',
followersCount: member.followerCount || 0,
profileImageUrl: member.imageUrl || undefined,
};
return convertedMember;
});
return convertedMembers;
};
export interface RoomMember {
userId: number;
nickname: string;
imageUrl: string;
aliasName: string;
followerCount?: number;
subscriberCount?: number;
}
export const convertRoomMembersToMembers = (roomMembers: RoomMember[]): Member[] => {
return roomMembers.map(member => ({
id: member.userId.toString(),
nickname: member.nickname || '익명',
role: member.aliasName || '독서메이트',
followersCount: member.followerCount ?? member.subscriberCount ?? 0,
profileImageUrl: member.imageUrl || undefined,
}));
};
🤖 Prompt for AI Agents
In src/api/rooms/getRoomMembers.ts around lines 30 to 44, the conversion assumes
the server field is followerCount only; add robustness by updating the
RoomMember type to include optional subscriberCount? and change the mapping to
use nullish coalescing (prefer member.followerCount ?? member.subscriberCount ??
0) for followersCount; keep other fallbacks (nickname, aliasName, imageUrl)
as-is and ensure the convertedMember.followersCount uses that combined value so
the function handles either field being present, both present, or neither.

Comment on lines +46 to +54
export const getRoomMembers = async (roomId: number): Promise<RoomMembersResponse> => {
try {
const response = await apiClient.get<RoomMembersResponse>(`/rooms/${roomId}/users`);
return response.data;
} catch (error) {
console.error('독서메이트 조회 API 오류:', error);
throw error;
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

isSuccess=false 응답 처리 누락

API가 200이어도 isSuccess가 false일 수 있습니다. 현재는 그대로 데이터를 반환해 UI에서 혼동될 수 있으므로 명시적으로 에러를 던지도록 수정하세요.

 export const getRoomMembers = async (roomId: number): Promise<RoomMembersResponse> => {
   try {
-    const response = await apiClient.get<RoomMembersResponse>(`/rooms/${roomId}/users`);
-    return response.data;
+    const response = await apiClient.get<RoomMembersResponse>(`/rooms/${roomId}/users`);
+    const data = response.data;
+    if (!data.isSuccess) {
+      throw new Error(`[${data.code}] ${data.message}`);
+    }
+    return data;
   } catch (error) {
     console.error('독서메이트 조회 API 오류:', error);
     throw error;
   }
 };
📝 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 getRoomMembers = async (roomId: number): Promise<RoomMembersResponse> => {
try {
const response = await apiClient.get<RoomMembersResponse>(`/rooms/${roomId}/users`);
return response.data;
} catch (error) {
console.error('독서메이트 조회 API 오류:', error);
throw error;
}
};
export const getRoomMembers = async (roomId: number): Promise<RoomMembersResponse> => {
try {
const response = await apiClient.get<RoomMembersResponse>(`/rooms/${roomId}/users`);
const data = response.data;
if (!data.isSuccess) {
throw new Error(`[${data.code}] ${data.message}`);
}
return data;
} catch (error) {
console.error('독서메이트 조회 API 오류:', error);
throw error;
}
};
🤖 Prompt for AI Agents
In src/api/rooms/getRoomMembers.ts around lines 46 to 54, the function currently
returns response.data even when the backend returns isSuccess: false; update the
success handling to check response.data.isSuccess after the GET and, if it is
false, throw an Error (or a typed error) containing the backend message or a
clear description (e.g., `throw new Error(response.data.message || 'Failed to
get room members')`) so callers receive an exception instead of invalid data;
keep the existing try/catch but ensure the thrown error is re-thrown after
logging.

Comment on lines +64 to +72
export const getRoomPlaying = async (roomId: number): Promise<RoomPlayingResponse> => {
try {
const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}/playing`);
return response.data;
} catch (error) {
console.error('진행중인 방 상세 정보 조회 API 오류:', error);
throw error;
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

isSuccess=false 응답 처리 누락

멤버 조회와 동일하게, isSuccess가 false인 경우 명시적으로 에러를 던지는 것이 안전합니다.

 export const getRoomPlaying = async (roomId: number): Promise<RoomPlayingResponse> => {
   try {
-    const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}/playing`);
-    return response.data;
+    const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}/playing`);
+    const data = response.data;
+    if (!data.isSuccess) {
+      throw new Error(`[${data.code}] ${data.message}`);
+    }
+    return data;
   } catch (error) {
     console.error('진행중인 방 상세 정보 조회 API 오류:', error);
     throw error;
   }
 };
📝 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 getRoomPlaying = async (roomId: number): Promise<RoomPlayingResponse> => {
try {
const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}/playing`);
return response.data;
} catch (error) {
console.error('진행중인 방 상세 정보 조회 API 오류:', error);
throw error;
}
};
export const getRoomPlaying = async (roomId: number): Promise<RoomPlayingResponse> => {
try {
const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}/playing`);
const data = response.data;
if (!data.isSuccess) {
throw new Error(`[${data.code}] ${data.message}`);
}
return data;
} catch (error) {
console.error('진행중인 방 상세 정보 조회 API 오류:', error);
throw error;
}
};
🤖 Prompt for AI Agents
In src/api/rooms/getRoomPlaying.ts around lines 64 to 72, the current
implementation returns response.data without checking response.data.isSuccess;
add the same explicit isSuccess check as used in member retrieval: after
receiving response.data, if response.data.isSuccess === false then throw an
Error (use response.data.message or a descriptive message like "Failed to get
room playing") so callers receive an exception for failed API responses; keep
the try/catch and only log/throw for unexpected network errors as currently
done.

Comment on lines +51 to +55
{member.profileImageUrl ? (
<ProfileImageWithSrc src={member.profileImageUrl} alt={`${member.nickname} 프로필`} />
) : (
<ProfileImage />
)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

프로필 이미지 유무에 따른 아바타 크기 불일치(48px vs 36px)로 인한 레이아웃 흔들림

ProfileImage는 48x48(참조: MemberList.styled.ts)인데, ProfileImageWithSrc는 36x36이라 목록 정렬이 틀어질 수 있습니다. 동일 크기로 통일하는 것을 권장합니다.

아바타 크기 통일(48px) 및 토큰 일관성(가능 시 colors 사용) 제안:

-const ProfileImageWithSrc = styled.img`
-  width: 36px;
-  height: 36px;
-  border-radius: 50%;
-  background-color: var(--color-grey-400);
-  flex-shrink: 0;
-  object-fit: cover;
-`;
+const ProfileImageWithSrc = styled.img`
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  background-color: var(--color-grey-400);
+  flex-shrink: 0;
+  object-fit: cover;
+`;

참고: 디자인 토큰 일관성을 위해 background-colorcolors.grey['400']로 맞추고 싶다면 상단에 colors import가 필요합니다(선택):

import { colors } from '../../styles/global/global';

그리고 속성 변경:

-  background-color: var(--color-grey-400);
+  background-color: ${colors.grey['400']};

Also applies to: 71-79

🤖 Prompt for AI Agents
In src/components/members/MemberList.tsx around lines 51-55 (and also apply same
change to 71-79), the avatar components use different sizes causing layout
shift: ProfileImage is 48x48 while ProfileImageWithSrc is 36x36; change
ProfileImageWithSrc to use the same 48x48 dimensions and matching styling as
ProfileImage (including border-radius and margin rules) so list alignment is
consistent. If you want token consistency, import colors from
'../../styles/global/global' and set its background-color to colors.grey['400']
(or match the existing token used by ProfileImage); ensure props like src and
alt are preserved and update any related styled-component definitions rather
than adding ad-hoc inline styles.

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

🧹 Nitpick comments (1)
src/components/members/MemberList.styled.ts (1)

50-56: 프로필 아바타 스타일 중복 최소화 제안 (styled.ts로 통합)

현재 TSX 내부에 별도의 ProfileImageWithSrc(36x36, object-fit: cover)가 있고, 여기에는 div 기반 ProfileImage가 있습니다. 아바타 공통 스타일(사이즈/원형/배경/축소 방지)을 이 styled 파일로 모아두면 중복이 줄고 유지보수가 쉬워집니다.

아래처럼 아바타 공통 스타일을 추출하고, div/img 두 변형을 이 파일에서 모두 export하는 방식을 권장합니다:

// 상단에 추가
import { css } from '@emotion/react';

const AVATAR_SIZE = 36;
const avatarBase = css`
  width: ${AVATAR_SIZE}px;
  height: ${AVATAR_SIZE}px;
  border-radius: 50%;
  background-color: ${colors.grey['400']};
  flex-shrink: 0;
`;

// 기존 ProfileImage 대체
export const ProfileImage = styled.div`
  ${avatarBase}
`;

// TSX에 있던 ProfileImageWithSrc를 이 파일로 이동/통합
export const ProfileImageWithSrc = styled.img`
  ${avatarBase}
  object-fit: cover;
`;

이렇게 하면 사이즈를 변경할 때 한 곳만 수정하면 되고(예: AVATAR_SIZE), 스타일 괴리 가능성을 낮출 수 있습니다. TSX에서는 두 컴포넌트를 여기서 import하여 사용하면 됩니다.

📜 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 7829941 and 842f143.

📒 Files selected for processing (1)
  • src/components/members/MemberList.styled.ts (1 hunks)
🔇 Additional comments (1)
src/components/members/MemberList.styled.ts (1)

51-52: 아바타 크기 36px로 축소 — 멤버 리스트와의 일관성 확보 좋습니다

MemberList.tsx에서 원격 아바타(ProfileImageWithSrc)도 36x36으로 맞춘 것으로 보여, UI 일관성이 좋아졌습니다.

@ljh130334 ljh130334 merged commit aaf07de into develop Aug 15, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-detail branch August 19, 2025 01:58
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.

1 participant