Conversation
모임방 인원 다 찼을 때, 참여하기 버튼 상태 비활성화로 변경
기획 디자인 요구사항에 따라 전체 tab 추가
isAllcategory 파라미터 추가와 적용
기획 디자인 요구사항에 따라 모집중인 모임방도 표기하도록 변경 되었고 그에 따른 분기처리 로직 추가
최근 생성된 독서 모임방 추가
디자인 요구사항에 따라 Tab 요소 배치 개수 수정
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough검색/그룹 목록에 전체 모임방 흐름과 최근 생성 섹션이 추가되고, API 타입과 검색 파라미터가 확장되었습니다. 캐러셀에 좌/우 내비게이션과 마감일 표시가 도입되며, 그룹 상세의 하단 버튼 비활성 조건과 스타일이 확장되었습니다. Changes
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: 전체 모임방 결과 표시
sequenceDiagram
autonumber
participant GS as GroupSearch 페이지
participant API as getSearchRooms
GS->>API: isAllCategory=true (키워드 생략, 쿼리에 isAllCategory=true)
API-->>GS: 결과
GS->>API: isAllCategory=false (키워드/카테고리 포함)
API-->>GS: 결과
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this comment.
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.
⛔ Files ignored due to path filters (3)
src/assets/common/back.svgis excluded by!**/*.svgsrc/assets/common/next.svgis excluded by!**/*.svgsrc/assets/common/searchChar.svgis 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 검색 로직 OKref를 통해 최신 category 보장하는 접근 적절합니다.
Also applies to: 216-221
| const timer = setTimeout(initializeScroll, 100); | ||
|
|
||
| const handleResize = () => { | ||
| setTimeout(initializeScroll, 50); | ||
| }; |
There was a problem hiding this comment.
리사이즈 디바운스 미적용(타임아웃 누적) — 메모리/재계산 폭주 위험
현재 리사이즈마다 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);
};추가로, initializeScroll은 requestAnimationFrame 내 실행을 고려하면 레이아웃 안정성이 좋아집니다(선택 사항).
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.
| const res = await getSearchRooms( | ||
| searchTerm.trim(), | ||
| toSortKey(selectedFilter), | ||
| nextCursor, | ||
| isFinalized, | ||
| category, | ||
| isAllCategory, | ||
| ); |
There was a problem hiding this comment.
무한스크롤 버그: '전체 모임방'(빈 검색어)에서 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.
| 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 수정사항에 따라 엔드포인트를 변경했습니다. 또한, 클릭이벤트 분기처리 로직을 추가했습니다.
There was a problem hiding this comment.
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.
📒 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: 변경 없음(공백 정리만).동작 영향 없으므로 그대로 두셔도 됩니다.
There was a problem hiding this comment.
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.
📒 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,isAllCategory를getSearchRooms로 일관되게 전파하고 초기 상태를 리셋하는 흐름이 명확합니다. 👍Also applies to: 90-97
256-271: IntersectionObserver 콜백 의존성 구성 적절
lastRoomElementCallback이loadMore를 의존성으로 가지므로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과 대/소문자까지 정확히 일치합니다.
| useEffect(() => { | ||
| if (location.state?.allRooms) { | ||
| setSearchTerm(''); | ||
| setSearchStatus('searched'); | ||
| setShowTabs(true); | ||
| setCategory(''); | ||
| searchFirstPage('', toSortKey(selectedFilter), 'searched', '', true); | ||
| } | ||
| }, [location.state?.allRooms, searchFirstPage, selectedFilter, toSortKey]); | ||
|
|
There was a problem hiding this comment.
중복 검색 요청: effect('searched')와 직접 호출이 겹칩니다
location.state?.allRoomseffect에서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.
| <> | ||
| <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> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
클릭 가능한 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.
There was a problem hiding this comment.
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: 디바운스 이중 스케줄링 단순화 제안
handleChange와searchStatus==='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.
📒 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 (카테고리 변경 추적)
lastRoomElementCallback이loadMore를 의존성에 포함하므로,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도 존재합니다.)
#️⃣연관된 이슈
[FIX] 9월 3주차 QA 사항 - 호준 #248
📝작업 내용
모임방 인원 다 찼을때, 참여하기 버튼 상태 변경
전체 모임방 보여주는 플로우 추가
2025-09-21.4.56.29.mov
캐러셀 넘기기 버튼 & 내 모임방 캐러셀 & 추천 캐러셀
2025-09-21.4.54.54.mov
Summary by CodeRabbit
신기능
UI/UX 개선
기타