Skip to content

[FIX] 모임방 사용선 개선#249

Merged
ho0010 merged 16 commits into
developfrom
fix/QA9-3
Sep 22, 2025
Merged

[FIX] 모임방 사용선 개선#249
ho0010 merged 16 commits into
developfrom
fix/QA9-3

Conversation

@ho0010
Copy link
Copy Markdown
Collaborator

@ho0010 ho0010 commented Sep 21, 2025

#️⃣연관된 이슈

[FIX] 9월 3주차 QA 사항 - 호준 #248

📝작업 내용

모임방 인원 다 찼을때, 참여하기 버튼 상태 변경

image

전체 모임방 보여주는 플로우 추가

2025-09-21.4.56.29.mov

캐러셀 넘기기 버튼 & 내 모임방 캐러셀 & 추천 캐러셀

2025-09-21.4.54.54.mov

Summary by CodeRabbit

  • 신기능

    • 그룹 페이지에 ‘최근 생성된 독서 모임방’ 섹션 추가 및 ‘전체 모임방 둘러보기’ 버튼 연동
    • 검색에 ‘전체’ 카테고리 옵션 및 전체 카테고리 검색 지원
  • UI/UX 개선

    • 캐러셀 좌우 내비게이션 버튼 및 반응형 동작 추가
    • 내 모임 카드에 마감일 표기 및 텍스트·간격 개선
    • 완료/탭 영역 여백·작은 화면 탭 정렬 개선
    • 상세 하단 버튼 비활성화 스타일 및 정원 충족 시 자동 비활성화
    • 캐러셀 초기화·리사이즈 안정성 개선
  • 기타

    • 검색 흐름 개선: 위치 기반 자동 전체보기 트리거 및 로드 추가 처리
    • 백엔드에서 최근 목록 및 마감일 데이터 제공 추가

모임방 인원 다 찼을 때, 참여하기 버튼 상태 비활성화로 변경
기획 디자인 요구사항에 따라 전체 tab 추가
isAllcategory 파라미터 추가와 적용
기획 디자인 요구사항에 따라 모집중인 모임방도 표기하도록 변경 되었고 그에 따른 분기처리 로직 추가
최근 생성된 독서 모임방 추가
디자인 요구사항에 따라 Tab 요소 배치 개수 수정
@vercel
Copy link
Copy Markdown

vercel Bot commented Sep 21, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Sep 22, 2025 1:50am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 21, 2025

Walkthrough

검색/그룹 목록에 전체 모임방 흐름과 최근 생성 섹션이 추가되고, API 타입과 검색 파라미터가 확장되었습니다. 캐러셀에 좌/우 내비게이션과 마감일 표시가 도입되며, 그룹 상세의 하단 버튼 비활성 조건과 스타일이 확장되었습니다.

Changes

Cohort / File(s) Summary
Rooms API 타입 확장
src/api/rooms/getJoinedRooms.ts, src/api/rooms/getRoomsByCategory.ts
JoinedRoomItemdeadlineDate?: string | null 추가. RoomsResponse.datarecentRoomList: RoomItem[] 추가.
검색 API 파라미터 분기 추가
src/api/rooms/getSearchRooms.ts
getSearchRoomsisAllCategory?: boolean 인자 추가; isAllCategory=true일 때 키워드 생략 및 쿼리에 isAllCategory=true 포함.
그룹 검색 페이지/컴포넌트
src/pages/groupSearch/GroupSearch.tsx, src/components/search/GroupSearchResult.tsx
AllRooms 버튼·"전체" 탭 추가, location.state.allRooms 처리로 전체 검색 트리거; 검색 파라미터(categoryParam, isAllCategory) 전파 및 디바운스/상태 보강.
그룹 목록 페이지(그룹 홈)
src/pages/group/Group.tsx
recent 섹션 추가(응답의 recentRoomList 사용) 및 AllRooms 버튼 추가, 배경색/스타일 일부 변경.
마이그룹 UI 개선
src/components/group/MyGroupBox.tsx, src/components/group/MyGroupCard.tsx, src/components/group/MyGroupModal.tsx, src/components/group/CompletedGroupModal.tsx
캐러셀 좌/우 내비 버튼 추가(다중 항목 시), deadlineDatedeadLine 매핑으로 마감일 표시 분기 및 여백/타이포·스타일 조정.
리크루팅 캐러셀/탭
src/components/group/RecruitingGroupCarousel.tsx, src/components/group/RecruitingGroupBox.tsx
캐러셀 내비 버튼(이전/다음)과 호버/반응형 표시 추가; 탭 간격 확대 및 작은 화면 레이아웃 조정.
훅 - 캐러셀 초기화 개선
src/hooks/useInfiniteCarousel.ts
초기화 지연(100ms), 윈도우 리사이즈 디바운스(50ms) 및 리사이즈 리스너 등록/해제 추가.
그룹 상세 페이지 버튼/스타일
src/pages/groupDetail/GroupDetail.tsx, src/pages/groupDetail/GroupDetail.styled.ts
하단 버튼의 disabled 조건 확대(비호스트·미참여자이면서 정원 도달 시 비활성). disabled 상태 스타일(회색 배경, cursor: not-allowed) 추가.
기타 API 변경
src/api/rooms/getRoomPlaying.ts, src/api/rooms/getRoomDetail.ts
getRoomPlaying 요청 엔드포인트를 /rooms/{roomId}로 변경. getRoomDetail은 캐치 블록 공백만 조정.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant G as Group 페이지
  participant R as Router
  participant S as GroupSearch 페이지
  participant API as getSearchRooms

  U->>G: 전체 모임방 클릭
  G->>R: navigate('/group/search', { state: { allRooms: true } })
  R-->>S: 진입 (state.allRooms=true)
  S->>S: UI 초기화(검색어/필터 비움)
  S->>API: getSearchRooms('', sort, cursor?, isFinalized?, category='', isAllCategory=true)
  API-->>S: 결과 반환
  S-->>U: 전체 모임방 결과 표시
Loading
sequenceDiagram
  autonumber
  participant GS as GroupSearch 페이지
  participant API as getSearchRooms

  GS->>API: isAllCategory=true (키워드 생략, 쿼리에 isAllCategory=true)
  API-->>GS: 결과
  GS->>API: isAllCategory=false (키워드/카테고리 포함)
  API-->>GS: 결과
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested labels

📬 API

Suggested reviewers

  • heeeeyong
  • ljh130334

Poem

깡총깡총, 토끼가 말해요 🐇
전체 방 훑고, 최근 방도 척척 줍줍 ✨
좌우로 슉슉, 캐러셀 춤을 추고 ➡️⬅️
마감일 보이면 초침이 콩닥⏳
꽉 찬 방은 잠깐 멈춰 쉬어요.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive 제목 "[FIX] 9월 3주차 QA 사항 - 호준"은 PR이 모임방 참여 버튼 비활성화, 전체 모임방 플로우 추가, 캐러셀 네비게이션 등 여러 구체적 변경을 포함하고 있어 관련성은 있으나 "QA 사항"이라는 표현이 너무 광범위해 핵심 변경사항이 한눈에 들어오지 않습니다. 따라서 제목만으로는 PR의 주요 변경을 명확히 판단하기 어려워 결론을 내리기 곤란합니다. 제목을 더 구체적으로 바꿔 주세요(예: "[FIX] 인원 찬 모임방 참여 버튼 비활성화 및 전체/추천/내 모임 캐러셀 추가" 또는 "[FEAT] 전체 모임방 플로우 및 캐러셀 네비게이션 추가"). 핵심 변경사항 하나를 짧게 강조하고 필요하면 태그(FIX/FEAT/REF)를 유지하면 PR 히스토리에서 의도를 더 쉽게 파악할 수 있습니다.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/QA9-3

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

❤️ Share

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

@ho0010 ho0010 self-assigned this Sep 21, 2025
Copy link
Copy Markdown

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/components/group/MyGroupModal.tsx (1)

47-81: 중복 useEffect로 인한 이중 요청/깜빡임 가능성

selected 의존의 방 조회 useEffect가 두 번 존재합니다. 실서비스에서도 2회 호출/경합이 발생할 수 있어 제거/통합이 필요합니다.

다음과 같이 상단 블록을 제거하고(또는 공통 함수로 추출) 단일 이펙트만 유지 제안:

-  useEffect(() => {
-    const fetchRooms = async () => {
-      try {
-        setIsLoading(true);
-        setError(null);
-        setNextCursor(null);
-        setIsLast(false);
-        const roomType: RoomType =
-          selected === '진행중'
-            ? 'playing'
-            : selected === '모집중'
-              ? 'recruiting'
-              : 'playingAndRecruiting';
-        const response = await getMyRooms(roomType, null);
-        if (response.isSuccess) {
-          setRooms(response.data.roomList);
-          setNextCursor(response.data.nextCursor);
-          setIsLast(response.data.isLast);
-        } else {
-          setError(response.message);
-        }
-      } catch (error) {
-        console.error('방 목록 조회 실패:', error);
-        setError('방 목록을 불러오는데 실패했습니다.');
-      } finally {
-        setIsLoading(false);
-      }
-    };
-    fetchRooms();
-  }, [selected]);

Also applies to: 122-155

🧹 Nitpick comments (28)
src/pages/groupDetail/GroupDetail.styled.ts (1)

280-283: 비활성화 색상 대비 검토 (가독성 향상 제안)

grey[300] 배경에 흰 텍스트는 대비가 낮을 수 있습니다. WCAG에서 disabled는 엄격 요건 대상은 아니지만, 가독성을 위해 비활성 상태에서 텍스트 색을 더 어둡게 하는 것을 권장합니다.

다음 변경으로 가독성을 개선해 보세요:

   &:disabled {
     background-color: ${colors.grey[300]};
+    color: ${colors.black.main};
     cursor: not-allowed;
   }
src/pages/groupDetail/GroupDetail.tsx (2)

101-114: 문자열 매칭 기반 에러 분기 의존 — 안정적인 코드/상태 기반으로 전환 권장

서버 메시지 문자열 변경 시 의도치 않은 라우팅이 발생할 수 있습니다. 가능하다면 에러 코드(또는 HTTP status)로 분기하고, 하위 호환용으로 문자열 매칭은 보조로 두는 형태를 권장합니다.

       } catch (error: unknown) {
         console.error('방 상세 정보 조회 실패:', error);
 
-        if (error instanceof Error && error.message === '모집기간이 만료된 방입니다.') {
+        // 우선 코드/상태 기반 분기
+        const code = (error as any)?.code;
+        if (code === 'ROOM_RECRUITMENT_EXPIRED') {
+          navigate(`/group/detail/joined/${roomId}`, { replace: true });
+          return;
+        }
+        if (code === 'ROOM_FORBIDDEN') {
+          navigate('/group', { replace: true });
+          return;
+        }
+
+        // (호환) 과거/서버 메시지 문자열 매칭
+        if (error instanceof Error && error.message === '모집기간이 만료된 방입니다.') {
           navigate(`/group/detail/joined/${roomId}`, { replace: true });
           return;
         }
 
-        if (error instanceof Error && error.message === '방 접근 권한이 없습니다.') {
+        if (error instanceof Error && error.message === '방 접근 권한이 없습니다.') {
           navigate('/group', { replace: true });
           return;
         }
 
         setError('방 정보를 불러오는데 실패했습니다.');

360-363: 정원 초과 시 버튼 레이블도 상태 반영 권장 + disabled 계산 가독성 개선

지금은 정원 초과 시 비활성만 되고 레이블은 ‘참여하기’로 유지됩니다. 유저 인지성을 위해 ‘정원 마감’ 등 상태를 문구에도 반영하는 것을 권장합니다. 또한 isFull/isJoinDisabled로 계산 값을 분리하면 가독성이 올라갑니다.

   const {
     roomName,
@@
     recommendRooms,
   } = roomData;
 
+  // 파생 상태
+  const isFull = memberCount >= recruitCount;
+  const isJoinDisabled =
+    isSubmitting || (!roomData.isHost && !roomData.isJoining && isFull);
@@
-      <BottomButton
-        onClick={handleBottomButtonClick}
-        disabled={isSubmitting || (!roomData.isHost && !isJoining && memberCount >= recruitCount)}
-      >
-        {roomData.isHost ? '모집 마감하기' : isJoining ? '참여 취소하기' : '참여하기'}
+      <BottomButton
+        onClick={handleBottomButtonClick}
+        disabled={isJoinDisabled}
+        title={
+          !roomData.isHost && !roomData.isJoining && isFull ? '정원이 가득 찼어요' : undefined
+        }
+      >
+        {roomData.isHost
+          ? '모집 마감하기'
+          : roomData.isJoining
+          ? '참여 취소하기'
+          : isFull
+          ? '정원 마감'
+          : '참여하기'}
       </BottomButton>
src/components/group/RecruitingGroupBox.tsx (3)

105-109: 비정형 브레이크포인트(373px) 사용 — 375px 등 표준값 권장

디바이스 표준(360/375/390 등)에 맞춘 브레이크포인트가 유지보수에 유리합니다.

다음처럼 375px로 정규화 제안:

-  @media (max-width: 373px) {
+  @media (max-width: 375px) {
     max-width: 240px;
     margin-left: auto;
     margin-right: auto;
   }

41-45: 접근성: Tab 버튼에 선택 상태를 스크린리더에 노출

현재 시각적 스타일만 있고 접근성 속성은 없습니다. aria-pressed 추가로 선택 상태를 알릴 것을 권장합니다. (GroupSearchResult에는 이미 aria-pressed 사용)

-          <Tab key={tab} selected={tab === selected} onClick={() => setSelected(tab)}>
+          <Tab
+            key={tab}
+            selected={tab === selected}
+            aria-pressed={tab === selected}
+            onClick={() => setSelected(tab)}
+          >

31-34: 상세 진입 경로 불일치 가능성 — 실패 시에도 상세 URL로 통일 권장

성공 시 /group/detail/:id, 에러 시 /group/:id로 분기되어 UX가 흔들릴 수 있습니다. 실패 시에도 상세로 유도하고, API 실패는 토스트로 안내하는 흐름이 안정적입니다.

-      console.error('방 상세 정보 조회 오류:', error);
-      navigate(`/group/${groupId}`);
+      console.error('방 상세 정보 조회 오류:', error);
+      navigate(`/group/detail/${roomId}`);
src/components/group/MyGroupModal.tsx (2)

206-214: 접근성: 탭 버튼에 선택 상태 노출

탭 토글에 aria-pressed 추가 권장(동일 패턴이 GroupSearchResult에 이미 존재).

-            <Tab
+            <Tab
               key={tab}
               selected={tab === selected}
-              onClick={() => setSelected(prev => (prev === tab ? '' : tab))}
+              aria-pressed={tab === selected}
+              onClick={() => setSelected(prev => (prev === tab ? '' : tab))}
             >

159-160: 오프바이원: 주석과 guard 초기값 불일치

주석은 “최대 3페이지”인데 guard = 2면 2회만 프리로드합니다. 주석/코드 중 하나를 맞춰주세요.

-      let guard = 2; // 최대 3페이지까지 자동 프리로드(필요시 늘리기)
+      let guard = 3; // 최대 3페이지까지 자동 프리로드(필요시 늘리기)
src/api/rooms/getJoinedRooms.ts (1)

9-10: deadlineDate 필드 추가 OK — 네이밍 일관성 고려

타 엔드포인트(roomName, deadlineDate)와의 네이밍이 혼재되어 있어 UI 매핑층에서 공통 모델로 정규화하는 것을 권장합니다.

간단한 DTO 계층으로 공통 deadLine으로 매핑해 사용하면 뷰 로직 단순화에 도움이 됩니다.

src/api/rooms/getSearchRooms.ts (2)

36-38: 키워드 공백 처리 누락 — 불필요한 빈 검색어 전송 방지

' ' 같은 공백만 있는 키워드는 제외되도록 trim 권장.

-    if (!isAllCategory && keyword) {
-      params.append('keyword', keyword);
+    if (!isAllCategory) {
+      const k = keyword?.trim();
+      if (k) params.append('keyword', k);
     }

32-33: 시그니처 변경 — 호출부에서 6번째 인자(isAllCategory) 명시 전달 필요

getSearchRooms에 isAllCategory: boolean = false 파라미터가 추가되었습니다. 기본값이 있어 즉시 오류는 없지만 호출 의도 명확화를 위해 모든 호출부에서 6번째 인자를 명시적으로 전달하세요.

영향 호출부:

  • src/pages/groupSearch/GroupSearch.tsx:90
  • src/pages/groupSearch/GroupSearch.tsx:229
src/components/group/MyGroupCard.tsx (2)

29-43: 진행도/마감 교차 렌더링은 좋습니다. 진행도 경계값 클램프와 접근성 속성 추가 권장

  • 진행도 값이 0~100 범위를 벗어나면 UI가 깨질 수 있습니다. 클램프 적용 제안.
  • 진행 막대에 role/aria 속성을 추가해 스크린리더 지원을 개선하세요.
  • isMine=false 경로에서 userName이 없으면 “undefined님의 진행도”가 노출될 수 있습니다. 안전한 fallback이 필요합니다.
-              <ProgressText>
-                {isMine ? '내 진행도' : `${group.userName}님의 진행도`}{' '}
-                <Percent>{Math.floor(group.progress || 0)}%</Percent>
-              </ProgressText>
-              <Bar>
-                <Fill width={group.progress || 0} />
-              </Bar>
+              <ProgressText>
+                {isMine ? '내 진행도' : `${group.userName ?? '참여자'}님의 진행도`}{' '}
+                <Percent>{Math.max(0, Math.min(100, Math.floor(group.progress ?? 0)))}%</Percent>
+              </ProgressText>
+              <Bar
+                role="progressbar"
+                aria-label={isMine ? '내 진행도' : `${group.userName ?? '참여자'}님의 진행도`}
+                aria-valuemin={0}
+                aria-valuemax={100}
+                aria-valuenow={Math.max(0, Math.min(100, Math.floor(group.progress ?? 0)))}
+              >
+                <Fill width={Math.max(0, Math.min(100, group.progress ?? 0))} />
+              </Bar>

114-126: 마감 텍스트 스타일 추가 LGTM. 복붙 스타일은 공통화 고려

ProgressText/Percent와 유사한 스타일이 중복됩니다. 스타일 토큰/컴포넌트 공통화로 유지보수성을 높일 수 있습니다.

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

66-80: 이전/다음 스크롤 로직: 스텝 하드코딩(20px) 제거 권장

카드 간 여백이 스타일 변경 시 어긋날 수 있습니다. 실제 카드 폭+간격을 계산하거나 scrollIntoView({block:'nearest', inline:'center'}) 사용을 권장합니다. 드래그 상태 변수들은 useRef로 관리하면 리렌더에도 안전합니다.


84-89: Nav 버튼에 type, 접근성 이름 추가

버튼에 type 누락, 접근성 이름은 img alt에만 의존. 명시적 aria-label과 type="button"을 권장합니다.

-      <NavButton className="nav-button prev" onClick={handlePrevClick}>
+      <NavButton className="nav-button prev" onClick={handlePrevClick} type="button" aria-label="이전으로 이동">
         <img src={backIcon} alt="이전" />
       </NavButton>
-      <NavButton className="nav-button next" onClick={handleNextClick}>
+      <NavButton className="nav-button next" onClick={handleNextClick} type="button" aria-label="다음으로 이동">
         <img src={nextIcon} alt="다음" />
       </NavButton>

119-126: 호버 시에만 버튼 표시 → 키보드 포커스 접근성 보완 필요

키보드 사용자도 버튼이 보여야 합니다. :focus-within 지원을 추가하세요.

-  &:hover .nav-button {
+  &:hover .nav-button,
+  &:focus-within .nav-button {
     opacity: 1;
     visibility: visible;
   }

127-155: Nav 버튼 포커스 표시/터치 타깃 강화 권장

  • 포커스 링 부재. :focus-visible 스타일 추가 권장.
  • 최소 터치 타깃(44px) 권장.
 const NavButton = styled.button`
   position: absolute;
   top: 50%;
   transform: translateY(-50%);
   z-index: 10;
-  border-radius: 50%;
+  border-radius: 50%;
   border: none;
-  background: transparent;
+  background: transparent;
   cursor: pointer;
   visibility: hidden;
   transition: all 0.1s ease;
+  width: 44px;
+  height: 44px;
+  display: grid;
+  place-items: center;
+
+  &:focus-visible {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }
src/components/group/MyGroupBox.tsx (3)

114-128: 이전/다음 스크롤 스텝 하드코딩(12px) → 계산식/scrollIntoView로 개선 권장

RecruitingGroupCarousel과 스텝 값(20px)도 상이합니다. 공통 유틸로 일관화하거나 scrollIntoView 전환을 권장합니다.

-  const handlePrevClick = () => {
+  const handlePrevClick = () => {
     if (scrollRef.current) {
       const container = scrollRef.current;
       const cardWidth = cardRefs.current[0]?.offsetWidth || 0;
-      container.scrollLeft -= cardWidth + 12;
+      container.scrollLeft -= cardWidth + getGap(container);
     }
   };
 
-  const handleNextClick = () => {
+  const handleNextClick = () => {
     if (scrollRef.current) {
       const container = scrollRef.current;
       const cardWidth = cardRefs.current[0]?.offsetWidth || 0;
-      container.scrollLeft += cardWidth + 12;
+      container.scrollLeft += cardWidth + getGap(container);
     }
   };

또는:

// 컴포넌트 외부 유틸
function getGap(container: HTMLElement) {
  const style = getComputedStyle(container);
  const gap = parseFloat(style.columnGap || style.gap || '0');
  return Number.isFinite(gap) ? gap : 0;
}

147-157: Nav 버튼에 type/접근성 라벨 추가

키보드/리더 친화 강화.

-              <NavButton className="nav-button prev" onClick={handlePrevClick}>
+              <NavButton className="nav-button prev" onClick={handlePrevClick} type="button" aria-label="이전으로 이동">
                 <img src={backIcon} alt="이전" />
               </NavButton>
-              <NavButton className="nav-button next" onClick={handleNextClick}>
+              <NavButton className="nav-button next" onClick={handleNextClick} type="button" aria-label="다음으로 이동">
                 <img src={nextIcon} alt="다음" />
               </NavButton>

240-278: 호버 기반 표시 → 포커스 접근성 추가 및 포커스 링 권장

키보드 사용성을 위해 .nav-button:focus-within에도 노출, :focus-visible 스타일을 추가하세요.

 const CarouselContainer = styled.div`
   position: relative;
   width: 100%;
 
-  &:hover .nav-button {
+  &:hover .nav-button,
+  &:focus-within .nav-button {
     opacity: 1;
     visibility: visible;
   }
 `;
 
 const NavButton = styled.button`
@@
   transition: all 0.1s ease;
+  width: 44px;
+  height: 44px;
+  display: grid;
+  place-items: center;
@@
   img {
     filter: invert(1);
   }
 
   @media (max-width: 768px) {
     display: none;
   }
+
+  &:focus-visible {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }
`;
src/pages/group/Group.tsx (3)

47-47: 최근 섹션 집계 로직 OK. 중복 제거와 실패 내성 고려 제안

  • 동일 방이 여러 카테고리에 속하면 중복 노출 가능. ID 기준 dedupe 권장.
  • 카테고리별 API 한 곳 실패가 전체 실패로 전파됩니다. Promise.allSettled로 부분 성공 허용을 권장합니다.
-      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'),
-          );
-          const recentGroups = response.data.recentRoomList.map(room =>
-            convertRoomItemToGroup(room, category, 'recent'),
-          );
-          deadlineRoomsData.push(...deadlineGroups);
-          popularRoomsData.push(...popularGroups);
-          recentRoomsData.push(...recentGroups);
-        }
-      }
+      const results = await Promise.allSettled(
+        categories.map(async category => {
+          const res = await getRoomsByCategory(category);
+          if (!res.isSuccess) return { deadline: [], popular: [], recent: [] };
+          return {
+            deadline: res.data.deadlineRoomList.map(r => convertRoomItemToGroup(r, category, 'deadline')),
+            popular: res.data.popularRoomList.map(r => convertRoomItemToGroup(r, category, 'popular')),
+            recent: res.data.recentRoomList.map(r => convertRoomItemToGroup(r, category, 'recent')),
+          };
+        }),
+      );
+      for (const r of results) {
+        if (r.status !== 'fulfilled') continue;
+        deadlineRoomsData.push(...r.value.deadline);
+        popularRoomsData.push(...r.value.popular);
+        recentRoomsData.push(...r.value.recent);
+      }
+      // 중복 제거
+      const uniqBy = <T, K>(arr: T[], key: (t: T) => K) =>
+        Array.from(new Map(arr.map(i => [key(i), i])).values());
+      const deadlineUniq = uniqBy(deadlineRoomsData, g => g.id);
+      const popularUniq = uniqBy(popularRoomsData, g => g.id);
+      const recentUniq = uniqBy(recentRoomsData, g => g.id);

그리고 아래 setSections에서는 recentUniq 등 사용.

Also applies to: 58-60, 63-63


112-116: 카피 오타 수정 권장: “들러보세요” → “둘러보세요”

자연스러운 문장: “전체 모임방을 한눈에 둘러보세요!”

-        전체 모임방을 한 눈에 들러보세요!
+        전체 모임방을 한눈에 둘러보세요!

138-155: 클릭 가능한 UI는 button/anchor 사용 권장

현재 styled.div + onClick입니다. 키보드 접근성/역할 명시를 위해 button으로 전환을 권장합니다.

-const AllRoomsButton = styled.div`
+const AllRoomsButton = styled.button`
   display: flex;
   position: relative;
   font-size: ${typography.fontSize.sm};
   font-weight: ${typography.fontWeight.medium};
   width: 83%;
   border-radius: 12px;
   padding: 14px 12px;
   margin-bottom: 12px;
   color: ${colors.white};
   background-color: ${colors.darkgrey.main};
   cursor: pointer;
+  border: none;
+  text-align: left;
+  &:focus-visible {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }
   > img {
     position: absolute;
     right: 5%;
     top: -2px;
   }
`;

그리고 사용처:

-      <AllRoomsButton onClick={handleAllRoomsClick}>
+      <AllRoomsButton type="button" onClick={handleAllRoomsClick}>
src/pages/groupSearch/GroupSearch.tsx (5)

116-125: 전체 모임방 진입 시 초기화 로직 OK. 추가 제안: 일회성 상태 소모

뒤로가기/재진입 시 중복 트리거 방지를 위해 처리 후 navigate(location.pathname, { replace: true, state: {} })로 state를 비우는 패턴을 권장합니다.


145-145: 검색 트리거 호출부 업데이트 OK. 중복 로직은 헬퍼로 정리 가능

동일한 searchFirstPage(..., category, isAllCategory) 호출이 반복됩니다. 헬퍼로 추출하면 가독성이 좋아집니다.

Also applies to: 160-160, 171-173, 174-184


186-197: ref 동기화 패턴 OK. setTimeout 타입 개선 제안

NodeJS.Timeout 대신 ReturnType<typeof setTimeout>을 사용하면 DOM/Node 환경 모두 안전합니다.

-  const [searchTimeoutId, setSearchTimeoutId] = useState<NodeJS.Timeout | null>(null);
+  const [searchTimeoutId, setSearchTimeoutId] = useState<ReturnType<typeof setTimeout> | null>(null);

352-369: Idle 화면의 ‘전체 모임방’ 버튼 접근성/역할 개선

클릭 가능한 요소는 버튼/링크로. 또한 alt 텍스트는 충분하지만 버튼 자체에 접근성 이름을 주는 것이 좋습니다.

-            <AllRoomsButton onClick={handleAllRoomsClick}>
+            <AllRoomsButton onClick={handleAllRoomsClick} type="button" aria-label="전체 모임방 둘러보기">
               <p>전체 모임방 둘러보기</p>
               <img src={rightChevron} alt="전체 모임방 버튼" />
             </AllRoomsButton>

387-396: AllRoomsButton을 button으로 전환 권장

키보드 포커스/역할 명확화.

-const AllRoomsButton = styled.div`
+const AllRoomsButton = styled.button`
   display: flex;
   justify-content: space-between;
   padding: 30px 20px;
-  background-color: transparent;
+  background-color: transparent;
   color: ${colors.grey[100]};
   font-size: ${typography.fontSize.lg};
   font-weight: ${typography.fontWeight.semibold};
   cursor: pointer;
+  border: none;
+  text-align: left;
+  &:focus-visible {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }
`;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 aba2121 and 76faa17.

⛔ Files ignored due to path filters (3)
  • src/assets/common/back.svg is excluded by !**/*.svg
  • src/assets/common/next.svg is excluded by !**/*.svg
  • src/assets/common/searchChar.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • src/api/rooms/getJoinedRooms.ts (1 hunks)
  • src/api/rooms/getRoomsByCategory.ts (1 hunks)
  • src/api/rooms/getSearchRooms.ts (1 hunks)
  • src/components/group/CompletedGroupModal.tsx (1 hunks)
  • src/components/group/MyGroupBox.tsx (6 hunks)
  • src/components/group/MyGroupCard.tsx (4 hunks)
  • src/components/group/MyGroupModal.tsx (1 hunks)
  • src/components/group/RecruitingGroupBox.tsx (1 hunks)
  • src/components/group/RecruitingGroupCarousel.tsx (4 hunks)
  • src/components/search/GroupSearchResult.tsx (2 hunks)
  • src/hooks/useInfiniteCarousel.ts (1 hunks)
  • src/pages/group/Group.tsx (7 hunks)
  • src/pages/groupDetail/GroupDetail.styled.ts (1 hunks)
  • src/pages/groupDetail/GroupDetail.tsx (2 hunks)
  • src/pages/groupSearch/GroupSearch.tsx (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/components/group/MyGroupCard.tsx (2)
src/components/group/RecordSection.styled.ts (1)
  • ProgressText (71-75)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
src/pages/groupSearch/GroupSearch.tsx (3)
src/api/rooms/getSearchRooms.ts (1)
  • getSearchRooms (26-52)
src/api/recentsearch/deleteRecentSearch.ts (1)
  • deleteRecentSearch (11-23)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
src/pages/groupDetail/GroupDetail.tsx (1)
src/pages/groupDetail/GroupDetail.styled.ts (1)
  • BottomButton (262-284)
src/pages/groupDetail/GroupDetail.styled.ts (1)
src/styles/global/global.ts (1)
  • colors (4-53)
src/pages/group/Group.tsx (2)
src/api/rooms/getRoomsByCategory.ts (1)
  • RoomItem (4-11)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
🔇 Additional comments (28)
src/components/group/RecruitingGroupBox.tsx (1)

101-101: 탭 간격 확장은 OK

모바일 탭 간격을 8px로 늘려 터치 정확도에 도움이 됩니다. 별도 액션 필요 없습니다.

src/components/group/MyGroupModal.tsx (2)

259-259: 여백 통일(20px) 변경은 합리적

모달 내 탭 영역의 상하좌우 여백을 일관화하여 레이아웃 안정성이 좋아집니다.


40-44: 필드명 확인 필요: endDate vs deadlineDate

다른 API에서는 deadlineDate를 사용합니다(getJoinedRooms 등). Room 타입의 실제 필드명이 endDate인지 확인 부탁드립니다. 혼재 시 안전 매핑 권장.

다음과 같이 폴백 매핑을 검토해 주세요:

-      deadLine: room.endDate || '',
+      deadLine: (room as any).deadlineDate ?? (room as any).endDate ?? '',
src/components/group/CompletedGroupModal.tsx (1)

106-107: 여백 통일(20px) 적용 적절

텍스트 블록의 상단 여백 과다 문제를 해결합니다. 별도 조치 없음.

src/components/search/GroupSearchResult.tsx (2)

9-10: “전체” 카테고리 추가 좋습니다

as const로 리터럴 타입 고정도 적절합니다.


63-69: '전체' 탭 선택 시 isAllCategory가 API로 전달됨 — 검증 완료
GroupSearch.tsx에서 isAllCategory를 계산(!searchTerm.trim() && category === '')하여 getSearchRooms의 6번째 인자로 전달합니다. 확인 위치: src/pages/groupSearch/GroupSearch.tsx (getSearchRooms 호출 라인 ~90, ~229; searchFirstPage 전달 라인 ~202–204), 시그니처: src/api/rooms/getSearchRooms.ts (isAllCategory: boolean = false).

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

43-44: 플래그 전송 로직 명확 — OK

isAllCategory를 쿼리로 명시 전달하는 방식은 서버 구분 로직에 유용합니다.

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

20-21: recentRoomList 추가 OK — 백엔드 반환 보장 확인 필요

타입 확장은 합리적입니다. 다만 제공하신 스크립트가 "No files were searched"를 반환해 사용처 검증이 되지 않았습니다. BE가 항상 data.recentRoomList를 반환하는지 확인하거나 소비부에 널/undefined 방어 또는 기본값을 적용하세요.

검증(로컬 실행 예):

rg -n -uu -C2 'recentRoomList'

권장 타입 변경:

-    recentRoomList: RoomItem[];
+    recentRoomList?: RoomItem[];
src/components/group/MyGroupCard.tsx (3)

15-16: deadLine 존재 여부 체크는 OK. 포맷만 확인 부탁

API에서 deadLine이 이미 “D-3”, “2일” 등 사용자 친화 포맷인지(아니면 ISO 날짜 문자열인지) 확인해 주세요. 날짜 원문이면 카드에서 상대시간/디데이로 변환하는 편이 UX에 더 좋습니다.


25-25: 참여 인원 텍스트 변경 LGTM

“명” 접미사로 간결해졌습니다. 별도 이슈 없습니다.


93-93: 타이포그래피 상향(sm) 변경 OK

가독성 향상에 긍정적입니다.

src/components/group/RecruitingGroupCarousel.tsx (2)

5-6: 아이콘 import 추가 OK


168-170: 반응형 패딩/폭 조정 OK. 스크롤 스텝과 일관성 확인

모바일에서 padding/min-width 변경 시 버튼 스크롤 스텝(cardWidth + 20)이 맞지 않을 수 있습니다. 스텝 계산 방식 개선 시 함께 정합성 검토 부탁드립니다.

Also applies to: 176-176, 180-183

src/components/group/MyGroupBox.tsx (3)

5-6: 캐러셀 네비 아이콘 import OK


33-34: deadLine 매핑 OK. API 스키마 확인만 요청

JoinedRoomItem.deadlineDate가 null 가능이면 현재 코드로 undefined 처리되어 안전합니다. 포맷이 UI 기대(상대시간/디데이)와 일치하는지 확인 부탁.


190-190: (변경 없음) 마크업 정리만 발생

별도 코멘트 없습니다.

src/pages/group/Group.tsx (7)

14-14: 검색 캐릭터 자산 import OK


16-16: 테마 토큰 import OK


21-21: listType에 'recent' 추가 OK

타입 안정성 확보에 유익합니다.


37-37: 초기 섹션에 '최근 생성' 추가 OK


68-71: 섹션 세팅 OK

표시 순서 의도와 일치합니다.

Also applies to: 75-75


96-103: 전체 모임방 네비게이션 로직 OK

검색 페이지로 state 전달 흐름 적절합니다.


135-136: 다크 배경 적용 OK

하위 섹션 텍스트 대비(contrast)는 기존 색상 토큰 사용으로 안전해 보입니다.

src/pages/groupSearch/GroupSearch.tsx (5)

5-5: 자산 import OK


14-14: useLocation 도입 OK

/group에서 전달한 state 처리 흐름과 일치합니다.

Also applies to: 21-21


75-114: searchFirstPage 파라미터 확장/전달 OK. API 파라미터 호환만 확인

isAllCategory 전달로 쿼리 분기 깔끔합니다. 서버가 해당 파라미터를 인지하는지 확인 부탁드립니다.


198-206: 필터/카테고리 변경 시 재검색 로직 OK

isAllCategory 계산 포함되어 일관성 있습니다.


209-209: debounce 검색 로직 OK

ref를 통해 최신 category 보장하는 접근 적절합니다.

Also applies to: 216-221

Comment on lines +71 to +75
const timer = setTimeout(initializeScroll, 100);

const handleResize = () => {
setTimeout(initializeScroll, 50);
};
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

리사이즈 디바운스 미적용(타임아웃 누적) — 메모리/재계산 폭주 위험

현재 리사이즈마다 setTimeout만 호출되어 타임아웃이 누적됩니다. 이전 타이머를 취소하는 디바운스가 필요합니다.

다음처럼 디바운스/정리 추가 제안:

     const timer = setTimeout(initializeScroll, 100);

-    const handleResize = () => {
-      setTimeout(initializeScroll, 50);
-    };
+    let resizeTimer: ReturnType<typeof setTimeout> | null = null;
+    const handleResize = () => {
+      if (resizeTimer) clearTimeout(resizeTimer);
+      resizeTimer = setTimeout(initializeScroll, 150);
+    };

     container.addEventListener('scroll', handleScroll, { passive: true });
     window.addEventListener('resize', handleResize);

     return () => {
       clearTimeout(timer);
       container.removeEventListener('scroll', handleScroll);
-      window.removeEventListener('resize', handleResize);
+      window.removeEventListener('resize', handleResize);
+      if (resizeTimer) clearTimeout(resizeTimer);
     };

추가로, initializeScrollrequestAnimationFrame 내 실행을 고려하면 레이아웃 안정성이 좋아집니다(선택 사항).

Also applies to: 78-79, 82-84

🤖 Prompt for AI Agents
In src/hooks/useInfiniteCarousel.ts around lines 71-75 (and similarly at 78-79,
82-84), the resize handler currently calls setTimeout repeatedly causing timer
accumulation; change it to debounce by storing the timeout id in a ref (or let
variable), clear the previous timeout before setting a new one, and clear the
timeout in the effect cleanup; additionally, call initializeScroll inside
requestAnimationFrame (or wrap the initializeScroll invocation with
requestAnimationFrame) for layout stability.

Comment on lines 229 to 236
const res = await getSearchRooms(
searchTerm.trim(),
toSortKey(selectedFilter),
nextCursor,
isFinalized,
category,
isAllCategory,
);
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

무한스크롤 버그: '전체 모임방'(빈 검색어)에서 loadMore가 작동하지 않음

초기 가드가 !searchTerm.trim()이면 return이라, isAllCategory=true 시에도 추가 로드를 막습니다. 가드를 isAllCategory 예외로 수정해야 합니다.

-  const loadMore = useCallback(async () => {
-    if (!searchTerm.trim() || !nextCursor || isLast || isLoadingMore) return;
+  const loadMore = useCallback(async () => {
+    const isAllCategory = !searchTerm.trim() && category === '';
+    if ((!isAllCategory && !searchTerm.trim()) || !nextCursor || isLast || isLoadingMore) return;
@@
-      const isAllCategory = !searchTerm.trim() && category === '';
       const res = await getSearchRooms(
         searchTerm.trim(),
         toSortKey(selectedFilter),
         nextCursor,
         isFinalized,
         category,
         isAllCategory,
       );

추가로, lastRoomElementCallback의 의존성에는 category도 포함되어 있어야 안전합니다.

📝 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 res = await getSearchRooms(
searchTerm.trim(),
toSortKey(selectedFilter),
nextCursor,
isFinalized,
category,
isAllCategory,
);
const loadMore = useCallback(async () => {
const isAllCategory = !searchTerm.trim() && category === '';
if ((!isAllCategory && !searchTerm.trim()) || !nextCursor || isLast || isLoadingMore) return;
setIsLoadingMore(true);
try {
const res = await getSearchRooms(
searchTerm.trim(),
toSortKey(selectedFilter),
nextCursor,
isFinalized,
category,
isAllCategory,
);
// existing result handling...
return res;
} finally {
setIsLoadingMore(false);
}
}, [searchTerm, nextCursor, isLast, isLoadingMore, selectedFilter, isFinalized, category]);
🤖 Prompt for AI Agents
In src/pages/groupSearch/GroupSearch.tsx around lines 229-236, the
infinite-scroll guard currently returns early when !searchTerm.trim(), which
blocks loadMore for the "All categories" case; change the guard to skip the
return when isAllCategory is true (e.g., only return if !searchTerm.trim() &&
!isAllCategory) so empty search with isAllCategory can paginate, and also add
category to the dependency array of lastRoomElementCallback to ensure the
callback updates when category changes.

BE API 수정사항에 따라 엔드포인트를 변경했습니다. 또한, 클릭이벤트 분기처리 로직을 추가했습니다.
Copy link
Copy Markdown

@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 (2)
src/api/rooms/getRoomPlaying.ts (2)

73-78: Axios 오류 판별 로직 개선 및 매직 넘버 상수화

instanceof AxiosError 대신 axios.isAxiosError 사용이 번들 경계/다중 axios 사본 상황에서도 안전합니다. 또한 에러 코드 140011은 상수로 관리하는 편이 좋습니다. 서버 메시지가 있다면 포함해 디버깅 가독성도 높입니다.

다음 패치를 권장합니다:

   } catch (error: unknown) {
     console.error('진행중인 방 상세 정보 조회 API 오류:', error);
 
     // 방 접근 권한이 없는 경우
-    if (error instanceof AxiosError && error.response?.data?.code === 140011) {
-      throw new Error('방 접근 권한이 없습니다.');
-    }
+    if (axios.isAxiosError<{ code?: number; message?: string }>(error)) {
+      const code = error.response?.data?.code;
+      if (code === ROOM_ACCESS_DENIED_CODE) {
+        throw new Error(error.response?.data?.message ?? '방 접근 권한이 없습니다.', { cause: error });
+      }
+    }
 
     throw error;
   }

아래 보조 변경이 필요합니다(파일 상단):

-import { AxiosError } from 'axios';
+import axios from 'axios';

그리고 에러 코드 상수 추가(파일 상단 적절한 위치):

const ROOM_ACCESS_DENIED_CODE = 140011 as const;

에러 코드가 백엔드에서 변경되지 않았는지, 동일 엔드포인트에서도 같은 코드를 방출하는지 확인 부탁드립니다.


65-69: 확인 필요: /rooms/{roomId} 응답 스키마 호환성 점검 + AbortSignal 요청 취소 옵션 추가 권장

  • 검증 결과: 레거시 '/rooms/.+/playing' 호출은 없음. getRoomPlaying 소비처와 응답 필드 의존처가 존재하므로 백엔드/API 계약에서 다음 키들이 동일하게 반환되는지 확인 필요: currentVotes, userPercentage, progressStartDate, progressEndDate.

    • getRoomPlaying 호출: src/pages/groupDetail/ParticipatedGroupDetail.tsx:71
    • currentVotes 사용: src/pages/groupDetail/ParticipatedGroupDetail.tsx:227
    • progressStartDate/EndDate 사용: src/pages/groupDetail/ParticipatedGroupDetail.tsx:268; src/pages/groupDetail/GroupDetail.tsx:140-141,299-300
    • userPercentage 사용: src/pages/groupDetail/ParticipatedGroupDetail.tsx:305; src/components/group/MyGroupBox.tsx:30-34
  • 권장 변경(요청 취소 지원) — 적용 예시:

-export const getRoomPlaying = async (roomId: number): Promise<RoomPlayingResponse> => {
+export const getRoomPlaying = async (
+  roomId: number,
+  options?: { signal?: AbortSignal }
+): Promise<RoomPlayingResponse> => {
   try {
-    const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}`);
+    const response = await apiClient.get<RoomPlayingResponse>(`/rooms/${roomId}`, {
+      signal: options?.signal,
+    });
     return response.data;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 76faa17 and bdb69d1.

📒 Files selected for processing (3)
  • src/api/rooms/getRoomDetail.ts (1 hunks)
  • src/api/rooms/getRoomPlaying.ts (1 hunks)
  • src/components/group/MyGroupCard.tsx (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/api/rooms/getRoomDetail.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/group/MyGroupCard.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/rooms/getRoomPlaying.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
🔇 Additional comments (1)
src/api/rooms/getRoomPlaying.ts (1)

71-71: 변경 없음(공백 정리만).

동작 영향 없으므로 그대로 두셔도 됩니다.

Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (3)
src/pages/groupSearch/GroupSearch.tsx (3)

41-43: 브라우저 환경의 setTimeout 타입 선언 수정 제안

브라우저에서 setTimeout 반환형은 number입니다. NodeJS.Timeout은 TS 설정에 따라 오류를 유발할 수 있습니다. 교차 환경 호환을 위해 ReturnType<typeof setTimeout> 사용을 권장합니다.

-  const [searchTimeoutId, setSearchTimeoutId] = useState<NodeJS.Timeout | null>(null);
+  const [searchTimeoutId, setSearchTimeoutId] = useState<ReturnType<typeof setTimeout> | null>(null);

48-57: 최근검색 이중 호출 제거

초기 마운트 시 여기서 1회, 그리고 Line 59-63의 effect에서 searchStatus === 'idle' 조건으로 다시 1회 호출됩니다. 하나로 통합해 중복 네트워크 호출을 줄여주세요. 권장: 이 블록 제거 후 Line 59-63만 유지.

-  useEffect(() => {
-    (async () => {
-      try {
-        const response = await getRecentSearch('ROOM');
-        setRecentSearches(response.isSuccess ? response.data.recentSearchList : []);
-      } catch {
-        setRecentSearches([]);
-      }
-    })();
-  }, []);

186-197: 불필요 ref 정리 및 일관성

searchStatusRef, selectedFilterRef는 현재 읽히지 않습니다. 유지 목적이 없다면 제거해 가독성과 복잡도를 낮추세요. 또한 Line 198-206에서는 searchTermRef(ref)와 category(state)를 혼용하고 있어 일관성이 떨어집니다. 동일한 소스(둘 다 state 의존 or 둘 다 ref 의존)로 맞추는 것을 권장합니다.

-  const searchStatusRef = useRef(searchStatus);
-  const categoryRef = useRef(category);
-  const selectedFilterRef = useRef(selectedFilter);
-  const searchTermRef = useRef(searchTerm);
+  const categoryRef = useRef(category);
+  const searchTermRef = useRef(searchTerm);
@@
-    searchStatusRef.current = searchStatus;
-    categoryRef.current = category;
-    selectedFilterRef.current = selectedFilter;
-    searchTermRef.current = searchTerm;
+    categoryRef.current = category;
+    searchTermRef.current = searchTerm;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 bdb69d1 and 5eff73b.

📒 Files selected for processing (1)
  • src/pages/groupSearch/GroupSearch.tsx (11 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/groupSearch/GroupSearch.tsx (3)
src/api/rooms/getSearchRooms.ts (1)
  • getSearchRooms (26-52)
src/api/recentsearch/deleteRecentSearch.ts (1)
  • deleteRecentSearch (11-23)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
🔇 Additional comments (4)
src/pages/groupSearch/GroupSearch.tsx (4)

75-81: API 파라미터 확장(카테고리/전체검색) 전달은 적절합니다

searchFirstPage에서 categoryParam, isAllCategorygetSearchRooms로 일관되게 전파하고 초기 상태를 리셋하는 흐름이 명확합니다. 👍

Also applies to: 90-97


256-271: IntersectionObserver 콜백 의존성 구성 적절

lastRoomElementCallbackloadMore를 의존성으로 가지므로 category 등 변경 시 재생성됩니다. 스크롤 옵저버 누수 방지 로직도 적절합니다. 👍


225-231: 무한스크롤 가드 버그: ‘전체 모임방’에서 더보기 차단됨

if (!searchTerm.trim() || ...) return; 때문에 searchTerm === '' && category === ''(전체 모임방) 케이스에서 로드가 막힙니다. 아래처럼 isAllCategory를 가드 계산에 반영해야 합니다. (이 이슈는 과거 코멘트와 동일합니다.)

   const loadMore = useCallback(async () => {
-    if (!searchTerm.trim() || !nextCursor || isLast || isLoadingMore) return;
+    const termEmpty = !searchTerm.trim();
+    const isAllCategory = termEmpty && category === '';
+    if ((!isAllCategory && termEmpty) || !nextCursor || isLast || isLoadingMore) return;
@@
-      const isAllCategory = !searchTerm.trim() && category === '';
       const res = await getSearchRooms(
         searchTerm.trim(),
         toSortKey(selectedFilter),
         nextCursor,
         isFinalized,
         category,
         isAllCategory,
       );
@@
-    // eslint-disable-next-line react-hooks/exhaustive-deps
+    // deps 정리는 선택 사항이지만 불필요한 disable은 줄이는 것을 권장합니다.
   }, [searchTerm, nextCursor, isLast, isLoadingMore, selectedFilter, searchStatus, category]);

Also applies to: 231-238, 253-254


5-5: 해결: import 경로와 실제 파일명이 일치합니다

src/pages/groupSearch/GroupSearch.tsx의 import '../../assets/common/right-Chevron.svg'은 실제 파일 src/assets/common/right-Chevron.svg과 대/소문자까지 정확히 일치합니다.

Comment on lines +116 to +125
useEffect(() => {
if (location.state?.allRooms) {
setSearchTerm('');
setSearchStatus('searched');
setShowTabs(true);
setCategory('');
searchFirstPage('', toSortKey(selectedFilter), 'searched', '', true);
}
}, [location.state?.allRooms, searchFirstPage, selectedFilter, toSortKey]);

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

중복 검색 요청: effect('searched')와 직접 호출이 겹칩니다

  • location.state?.allRooms effect에서 searchFirstPage 호출
  • handleSearch/handleRecentSearchClick/handleAllRoomsClick에서도 즉시 searchFirstPage 호출
  • 동시에 Line 198-206의 effect가 searchStatus === 'searched'에 반응하여 다시 호출

결과적으로 동일 요청이 2회 이상 호출됩니다. ‘searched’ 상태 전환만 수행하고 실제 호출은 해당 effect 하나에서만 이루어지도록 정리해 주세요. 또한 location.state?.allRooms는 처리 후 replace로 초기화하여 재트리거를 막아야 합니다.

적용 패치:

@@
-  useEffect(() => {
-    if (location.state?.allRooms) {
-      setSearchTerm('');
-      setSearchStatus('searched');
-      setShowTabs(true);
-      setCategory('');
-      searchFirstPage('', toSortKey(selectedFilter), 'searched', '', true);
-    }
-  }, [location.state?.allRooms, searchFirstPage, selectedFilter, toSortKey]);
+  useEffect(() => {
+    if (!location.state?.allRooms) return;
+    setSearchTerm('');
+    setSearchStatus('searched');
+    setShowTabs(true);
+    setCategory('');
+    // 재트리거 방지: state 초기화
+    navigate(location.pathname, { replace: true, state: { ...location.state, allRooms: false } });
+  }, [location.state?.allRooms, navigate]);
@@
   const handleSearch = () => {
@@
-    setSearchStatus('searched');
-    setShowTabs(true);
-    searchFirstPage(term, toSortKey(selectedFilter), 'searched', category);
+    setSearchStatus('searched');
+    setShowTabs(true);
   };
@@
-  const handleRecentSearchClick = (recent: string) => {
+  const handleRecentSearchClick = (recent: string) => {
@@
-    setSearchStatus('searched');
-    setShowTabs(true);
-    searchFirstPage(recent.trim(), toSortKey(selectedFilter), 'searched', category);
+    setSearchStatus('searched');
+    setShowTabs(true);
   };
@@
-  const handleAllRoomsClick = () => {
+  const handleAllRoomsClick = () => {
@@
-    setSearchStatus('searched');
-    setShowTabs(true);
-    setCategory('');
-    searchFirstPage('', toSortKey(selectedFilter), 'searched', '', true);
+    setSearchStatus('searched');
+    setShowTabs(true);
+    setCategory('');
   };

Also applies to: 160-161, 171-172, 174-184

🤖 Prompt for AI Agents
In src/pages/groupSearch/GroupSearch.tsx around lines 116-125 (and also touch
lines 160-161, 171-172, 174-184 and the effect at ~198-206): the code currently
triggers duplicate network searches by calling searchFirstPage directly in the
location.state?.allRooms effect and in handlers
(handleSearch/handleRecentSearchClick/handleAllRoomsClick) while another effect
also reacts to searchStatus === 'searched' and calls searchFirstPage; refactor
so only the single effect that watches searchStatus === 'searched' performs
searchFirstPage, and change the other places (the location.state effect and the
handlers at the mentioned lines) to only update state
(setSearchTerm/setSearchStatus/setShowTabs/setCategory/setSelectedFilter as
needed) without calling searchFirstPage; additionally, after handling
location.state?.allRooms, call history.replace(...) (or location.replace
equivalent) to clear that state so it doesn't retrigger.

Comment on lines +346 to 364
<>
<RecentSearchTabs
recentSearches={recentSearches.map(i => i.searchTerm)}
handleDelete={async (term: string) => {
const x = recentSearches.find(i => i.searchTerm === term);
if (!x) return;
const res = await deleteRecentSearch(x.recentSearchId);
if (res.isSuccess) {
await fetchRecentSearches();
}
}}
handleRecentSearchClick={handleRecentSearchClick}
/>
<AllRoomsButton onClick={handleAllRoomsClick}>
<p>전체 모임방 둘러보기</p>
<img src={rightChevron} alt="전체 모임방 버튼" />
</AllRoomsButton>
</>
)}
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 → button 교체 및 아이콘 접근성 보완

  • 현재 <AllRoomsButton>이 div라 키보드 포커스/엔터로 작동하지 않습니다.
  • 우측 화살표는 장식 아이콘이므로 스크린리더에 숨기는 것이 좋습니다.
@@
-            <AllRoomsButton onClick={handleAllRoomsClick}>
+            <AllRoomsButton type="button" onClick={handleAllRoomsClick} aria-label="전체 모임방 둘러보기">
               <p>전체 모임방 둘러보기</p>
-              <img src={rightChevron} alt="전체 모임방 버튼" />
+              <img src={rightChevron} alt="" aria-hidden="true" />
             </AllRoomsButton>
@@
-const AllRoomsButton = styled.div`
+const AllRoomsButton = styled.button`
   display: flex;
-  justify-content: space-between;
+  justify-content: space-between;
+  align-items: center;
   padding: 30px 20px;
-  background-color: transparent;
+  background: transparent;
+  border: 0;
   color: ${colors.grey[100]};
   font-size: ${typography.fontSize.lg};
   font-weight: ${typography.fontWeight.semibold};
   cursor: pointer;
+  width: 100%;
+  text-align: left;
+
+  &:focus-visible {
+    outline: 2px solid ${colors.grey[100]};
+    border-radius: 8px;
+  }
 `;

Also applies to: 381-390

🤖 Prompt for AI Agents
In src/pages/groupSearch/GroupSearch.tsx around lines 346-364 and also 381-390,
replace the clickable div styled component AllRoomsButton with a semantic
<button type="button"> (or modify the styled-component to render a button) so it
is focusable and activates via keyboard/Enter, keep existing onClick handler and
styling, and ensure it does not submit forms by specifying type="button"; also
mark the right-chevron img as decorative by adding aria-hidden="true" and alt=""
(or use role="img" with aria-hidden) so screen readers ignore it while
preserving visual display.

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
src/pages/groupSearch/GroupSearch.tsx (1)

42-42: 브라우저 환경에서 NodeJS.Timeout 타입 오사용

브라우저 TSX에서 NodeJS.Timeout은 종종 타입 불일치(특히 clearTimeout)를 유발합니다. DOM/Node 양쪽 안전한 형태로 교체하세요.

-  const [searchTimeoutId, setSearchTimeoutId] = useState<NodeJS.Timeout | null>(null);
+  const [searchTimeoutId, setSearchTimeoutId] = useState<ReturnType<typeof setTimeout> | null>(null);
🧹 Nitpick comments (5)
src/pages/groupSearch/GroupSearch.tsx (5)

48-57: 최근 검색 2중 호출(마운트 시 중복 페치)

마운트 즉시 IIFE(48-57)와 searchStatus==='idle' 이펙트(59-64)가 모두 실행되어 중복 호출됩니다. 하나로 정리하세요(후자만 유지 권장).

-  useEffect(() => {
-    (async () => {
-      try {
-        const response = await getRecentSearch('ROOM');
-        setRecentSearches(response.isSuccess ? response.data.recentSearchList : []);
-      } catch {
-        setRecentSearches([]);
-      }
-    })();
-  }, []);
+  // 최초 마운트 시에도 searchStatus 기본값 'idle'에 의해 아래 effect가 한번만 실행됩니다.

Also applies to: 59-64


116-125: location.state 정리 시 다른 state 유실 위험

navigate(location.pathname, { replace: true })는 기존 state를 전부 드롭합니다. 다른 키가 있다면 함께 유지하도록 바꿔주세요.

-      navigate(location.pathname, { replace: true });
+      navigate(location.pathname, {
+        replace: true,
+        state: { ...(location.state ?? {}), allRooms: false },
+      });

143-149: 디바운스 이중 스케줄링 단순화 제안

handleChangesearchStatus==='searching' 이펙트가 모두 타이머를 잡습니다(서로 clear 하긴 함). 한 곳(이펙트)으로 통일하면 가독성과 리스크가 줄어듭니다.

   setSearchStatus('searching');
   setShowTabs(false);
-  const id = setTimeout(() => {
-    searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching', category);
-  }, 300);
-  setSearchTimeoutId(id);
+  // 디바운스는 아래 searching 이펙트 한 곳에서만 처리

Also applies to: 206-221


184-195: 불필요한 ref 정리

categoryRef만 사용되고, searchStatusRef/selectedFilterRef/searchTermRef는 읽히지 않습니다. 제거해 단순화하세요.

-  const searchStatusRef = useRef(searchStatus);
   const categoryRef = useRef(category);
-  const selectedFilterRef = useRef(selectedFilter);
-  const searchTermRef = useRef(searchTerm);
@@
-    searchStatusRef.current = searchStatus;
     categoryRef.current = category;
-    selectedFilterRef.current = selectedFilter;
-    searchTermRef.current = searchTerm;

196-205: isAllCategory 판단 및 의존성 구성 적절

빈 검색어+빈 카테고리일 때만 전체조회로 처리하는 로직이 명확합니다. searchFirstPage/toSortKey가 안정적이므로 lint 비활성화는 제거 가능해 보입니다.

-    // eslint-disable-next-line react-hooks/exhaustive-deps
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 5eff73b and 64720d0.

📒 Files selected for processing (1)
  • src/pages/groupSearch/GroupSearch.tsx (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/groupSearch/GroupSearch.tsx (3)
src/api/rooms/getSearchRooms.ts (1)
  • getSearchRooms (26-52)
src/api/recentsearch/deleteRecentSearch.ts (1)
  • deleteRecentSearch (11-23)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
🔇 Additional comments (6)
src/pages/groupSearch/GroupSearch.tsx (6)

75-82: 검색 API 파라미터 확장 적용 👍

categoryParam/isAllCategory 전달 흐름이 일관되고, keyword/isAllCategory의 상호배타 조건도 서버 규격과 정합적입니다.

이 변경에 맞춰 getSearchRooms의 타입/응답모델이 업데이트되었는지도 한번만 확인 부탁드립니다.

Also applies to: 90-97


159-171: 중복 호출 제거 흐름 반영 Good

핸들러에서 직접 API 호출을 제거하고 상태 전이만 하도록 정리되어, 'searched' 이펙트 한 곳에서 호출되도록 개선되었습니다.

location.state?.allRooms 트리거 케이스에서도 단일 호출만 발생하는지 네트워크 탭으로 한번 확인 부탁드려요.

Also applies to: 173-182


254-269: 의존성 전달 OK (카테고리 변경 추적)

lastRoomElementCallbackloadMore를 의존성에 포함하므로, loadMore의 의존성(포함: category)을 통해 최신 카테고리로 갱신됩니다. 추가 조치는 불필요합니다.

실기기에서 카테고리 변경 후 스크롤 페이징이 정상 동작하는지 체크만 부탁드립니다.


223-236: 전체 모임방 무한스크롤 차단 버그(가드 조건 오류)

!searchTerm.trim() 가드로 인해 '전체 모임방'(빈 검색어+빈 카테고리)에서 loadMore가 항상 return 됩니다. isAllCategory 예외를 반영해야 합니다.

-  const loadMore = useCallback(async () => {
-    if (!searchTerm.trim() || !nextCursor || isLast || isLoadingMore) return;
+  const loadMore = useCallback(async () => {
+    const isAllCategory = !searchTerm.trim() && category === '';
+    if ((!isAllCategory && !searchTerm.trim()) || !nextCursor || isLast || isLoadingMore) return;
@@
-      const isAllCategory = !searchTerm.trim() && category === '';
+      // 위에서 계산한 isAllCategory 사용

Also applies to: 251-252


344-361: 접근성/시맨틱: 클릭 div → button, 장식 아이콘 숨김

AllRoomsButton이 div라 키보드 접근성이 떨어집니다. button으로 교체하고 아이콘은 스크린리더에서 숨겨주세요.

-            <AllRoomsButton onClick={handleAllRoomsClick}>
+            <AllRoomsButton type="button" onClick={handleAllRoomsClick} aria-label="전체 모임방 둘러보기">
               <p>전체 모임방 둘러보기</p>
-              <img src={rightChevron} alt="전체 모임방 버튼" />
+              <img src={rightChevron} alt="" aria-hidden="true" />
             </AllRoomsButton>
@@
-const AllRoomsButton = styled.div`
+const AllRoomsButton = styled.button`
   display: flex;
-  justify-content: space-between;
+  justify-content: space-between;
+  align-items: center;
   padding: 30px 20px;
-  background-color: transparent;
+  background: transparent;
+  border: 0;
   color: ${colors.grey[100]};
   font-size: ${typography.fontSize.lg};
   font-weight: ${typography.fontWeight.semibold};
   cursor: pointer;
+  width: 100%;
+  text-align: left;
+
+  &:focus-visible {
+    outline: 2px solid ${colors.grey[100]};
+    border-radius: 8px;
+  }
 `;

Also applies to: 379-388


5-5: 확인 완료 — import 경로가 리포지토리와 일치합니다

src/assets/common/right-Chevron.svg 파일이 존재하므로 '../../assets/common/right-Chevron.svg' import는 대소문자·하이픈 관점에서 올바릅니다. (참고: src/assets/group/right-chevron.svg, src/assets/member/right-chevron.svg도 존재합니다.)

@ho0010 ho0010 merged commit d62d4b4 into develop Sep 22, 2025
3 checks passed
@heeeeyong heeeeyong added this to the 9월 3주차 QA milestone Sep 23, 2025
@ho0010 ho0010 changed the title [FIX] 9월 3주차 QA 사항 - 호준 [FIX] 모임방 사용선 개선 Nov 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants