diff --git a/src/api/rooms/getJoinedRooms.ts b/src/api/rooms/getJoinedRooms.ts index fb104d1b..175df941 100644 --- a/src/api/rooms/getJoinedRooms.ts +++ b/src/api/rooms/getJoinedRooms.ts @@ -1,12 +1,12 @@ import { apiClient } from '../index'; -// 가입한 방 목록 응답 데이터 타입 export interface JoinedRoomItem { roomId: number; bookImageUrl: string; roomTitle: string; memberCount: number; userPercentage: number; + deadlineDate?: string | null; } export interface JoinedRoomsResponse { diff --git a/src/api/rooms/getRoomDetail.ts b/src/api/rooms/getRoomDetail.ts index 079a862f..fa3567c1 100644 --- a/src/api/rooms/getRoomDetail.ts +++ b/src/api/rooms/getRoomDetail.ts @@ -45,19 +45,19 @@ export const getRoomDetail = async (roomId: number): Promise return response.data; } catch (error: unknown) { console.error('방 상세 정보 조회 API 오류:', error); - + if (error instanceof AxiosError) { // 모집기간이 만료된 방인 경우 if (error.response?.data?.code === 100004) { throw new Error('모집기간이 만료된 방입니다.'); } - + // 방 접근 권한이 없는 경우 if (error.response?.data?.code === 140011) { throw new Error('방 접근 권한이 없습니다.'); } } - + throw error; } }; diff --git a/src/api/rooms/getRoomPlaying.ts b/src/api/rooms/getRoomPlaying.ts index d21d0c72..1cd00281 100644 --- a/src/api/rooms/getRoomPlaying.ts +++ b/src/api/rooms/getRoomPlaying.ts @@ -64,16 +64,16 @@ export const convertVotesToPolls = (currentVotes: CurrentVote[]): Poll[] => { export const getRoomPlaying = async (roomId: number): Promise => { try { - const response = await apiClient.get(`/rooms/${roomId}/playing`); + const response = await apiClient.get(`/rooms/${roomId}`); return response.data; } catch (error: unknown) { console.error('진행중인 방 상세 정보 조회 API 오류:', error); - + // 방 접근 권한이 없는 경우 if (error instanceof AxiosError && error.response?.data?.code === 140011) { throw new Error('방 접근 권한이 없습니다.'); } - + throw error; } }; diff --git a/src/api/rooms/getRoomsByCategory.ts b/src/api/rooms/getRoomsByCategory.ts index b2c55583..55db5df3 100644 --- a/src/api/rooms/getRoomsByCategory.ts +++ b/src/api/rooms/getRoomsByCategory.ts @@ -17,6 +17,7 @@ export interface RoomsResponse { data: { deadlineRoomList: RoomItem[]; popularRoomList: RoomItem[]; + recentRoomList: RoomItem[]; }; } diff --git a/src/api/rooms/getSearchRooms.ts b/src/api/rooms/getSearchRooms.ts index b0a81245..52cd052f 100644 --- a/src/api/rooms/getSearchRooms.ts +++ b/src/api/rooms/getSearchRooms.ts @@ -29,14 +29,18 @@ export const getSearchRooms = async ( cursor?: string, isFinalized: boolean = false, category: string = '', + isAllCategory: boolean = false, ): Promise => { try { const params = new URLSearchParams(); - params.append('keyword', keyword); + if (!isAllCategory && keyword) { + params.append('keyword', keyword); + } params.append('sort', sort); params.append('isFinalized', String(isFinalized)); if (cursor) params.append('cursor', cursor); if (category) params.append('category', category); + if (isAllCategory) params.append('isAllCategory', 'true'); const url = `/rooms/search?${params.toString()}`; const response = await apiClient.get(url); diff --git a/src/assets/common/back.svg b/src/assets/common/back.svg new file mode 100644 index 00000000..7ae15c91 --- /dev/null +++ b/src/assets/common/back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/common/next.svg b/src/assets/common/next.svg new file mode 100644 index 00000000..643f06ca --- /dev/null +++ b/src/assets/common/next.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/common/searchChar.svg b/src/assets/common/searchChar.svg new file mode 100644 index 00000000..36253b39 --- /dev/null +++ b/src/assets/common/searchChar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/group/CompletedGroupModal.tsx b/src/components/group/CompletedGroupModal.tsx index 6c222d54..afa624ab 100644 --- a/src/components/group/CompletedGroupModal.tsx +++ b/src/components/group/CompletedGroupModal.tsx @@ -103,7 +103,7 @@ const Text = styled.p` font-size: ${typography.fontSize.sm}; font-weight: ${typography.fontWeight.regular}; color: ${colors.white}; - margin: 96px 20px 20px 20px; + margin: 20px; `; const Content = styled.div<{ isEmpty?: boolean }>` diff --git a/src/components/group/MyGroupBox.tsx b/src/components/group/MyGroupBox.tsx index 05328dc2..5dc9659e 100644 --- a/src/components/group/MyGroupBox.tsx +++ b/src/components/group/MyGroupBox.tsx @@ -2,6 +2,8 @@ import { MyGroupCard } from './MyGroupCard'; import { useInfiniteCarousel } from '../../hooks/useInfiniteCarousel'; import styled from '@emotion/styled'; import rightChevron from '../../assets/common/right-Chevron.svg'; +import backIcon from '@/assets/common/back.svg'; +import nextIcon from '@/assets/common/next.svg'; import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { getJoinedRooms, type JoinedRoomItem } from '@/api/rooms/getJoinedRooms'; @@ -28,6 +30,7 @@ const convertJoinedRoomToGroup = (room: JoinedRoomItem): Group => ({ participants: room.memberCount, coverUrl: room.bookImageUrl, progress: room.userPercentage, + deadLine: room.deadlineDate || undefined, }); interface MyGroupProps { @@ -108,6 +111,22 @@ export function MyGroupBox({ onMyGroupsClick }: MyGroupProps) { isDragging = false; }; + const handlePrevClick = () => { + if (scrollRef.current) { + const container = scrollRef.current; + const cardWidth = cardRefs.current[0]?.offsetWidth || 0; + container.scrollLeft -= cardWidth + 12; + } + }; + + const handleNextClick = () => { + if (scrollRef.current) { + const container = scrollRef.current; + const cardWidth = cardRefs.current[0]?.offsetWidth || 0; + container.scrollLeft += cardWidth + 12; + } + }; + return (
@@ -125,7 +144,17 @@ export function MyGroupBox({ onMyGroupsClick }: MyGroupProps) { {error} ) : groups.length > 0 ? ( - <> + + {!isSingle && ( + <> + + 이전 + + + 다음 + + + )} {isSingle ? ( )} - + ) : ( @@ -208,6 +237,45 @@ const MoreButton = styled.button` } `; +const CarouselContainer = styled.div` + position: relative; + width: 100%; + + &:hover .nav-button { + opacity: 1; + visibility: visible; + } +`; + +const NavButton = styled.button` + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 10; + border-radius: 50%; + border: none; + background: transparent; + cursor: pointer; + visibility: hidden; + transition: all 0.1s ease; + + &.prev { + left: 4%; + } + + &.next { + right: 4%; + } + + img { + filter: invert(1); + } + + @media (max-width: 768px) { + display: none; + } +`; + const Carousel = styled.div` display: flex; padding: 0; diff --git a/src/components/group/MyGroupCard.tsx b/src/components/group/MyGroupCard.tsx index 573a6070..46e9490b 100644 --- a/src/components/group/MyGroupCard.tsx +++ b/src/components/group/MyGroupCard.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import peopleImg from '../../assets/common/people.svg'; import type { Group } from './MyGroupBox'; import { colors, typography } from '@/styles/global/global'; +import { useNavigate } from 'react-router-dom'; interface MyGroupCardProps { group: Group; @@ -12,25 +13,44 @@ interface MyGroupCardProps { export const MyGroupCard = forwardRef((props, ref) => { const { group, onClick, isMine } = props; + const navigate = useNavigate(); + const hasDeadline = group.deadLine != null; + + const handleClick = () => { + if (hasDeadline) { + navigate(`/group/detail/${group.id}`); + } else { + onClick?.(); + } + }; + return ( - +
{group.title} - {group.participants}명 참여 + {group.participants}명
- - {isMine ? '내 진행도' : `${group.userName}님의 진행도`}{' '} - {Math.floor(group.progress || 0)}% - - - - + {hasDeadline ? ( + + 시작까지 {group.deadLine} + + ) : ( + <> + + {isMine ? '내 진행도' : `${group.userName}님의 진행도`}{' '} + {Math.floor(group.progress || 0)}% + + + + + + )}
@@ -80,7 +100,7 @@ const Participants = styled.p` display: flex; align-items: center; gap: 4px; - font-size: ${typography.fontSize.xs}; + font-size: ${typography.fontSize.sm}; font-weight: ${typography.fontWeight.medium}; color: ${colors.grey[300]}; margin: 8px 0; @@ -101,6 +121,19 @@ const Percent = styled.span` font-weight: ${typography.fontWeight.semibold}; `; +const DeadlineText = styled.p` + font-size: ${typography.fontSize.sm}; + color: ${colors.grey[300]}; + margin: 12px 0; +`; + +const DeadlineValue = styled.span` + font-size: ${typography.fontSize.base}; + color: ${colors.purple.main}; + font-weight: ${typography.fontWeight.semibold}; + margin-left: 4px; +`; + const Bar = styled.div` width: 100%; height: 6px; diff --git a/src/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index 097c7092..caa763c6 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -255,7 +255,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { const TabContainer = styled.div` display: flex; gap: 8px; - margin: 76px 20px 20px 20px; + margin: 20px; `; const Tab = styled.button<{ selected: boolean }>` diff --git a/src/components/group/RecruitingGroupBox.tsx b/src/components/group/RecruitingGroupBox.tsx index b46a6fac..bc988d75 100644 --- a/src/components/group/RecruitingGroupBox.tsx +++ b/src/components/group/RecruitingGroupBox.tsx @@ -98,9 +98,15 @@ const Title = styled.h2` const TabContainer = styled.div` display: flex; flex-wrap: wrap; - gap: 4px; + gap: 8px; justify-content: center; margin-bottom: 24px; + + @media (max-width: 373px) { + max-width: 240px; + margin-left: auto; + margin-right: auto; + } `; const Tab = styled.button<{ selected?: boolean }>` diff --git a/src/components/group/RecruitingGroupCarousel.tsx b/src/components/group/RecruitingGroupCarousel.tsx index ba16b701..718ede42 100644 --- a/src/components/group/RecruitingGroupCarousel.tsx +++ b/src/components/group/RecruitingGroupCarousel.tsx @@ -2,6 +2,8 @@ import styled from '@emotion/styled'; import type { Group } from './MyGroupBox'; import { RecruitingGroupBox } from './RecruitingGroupBox'; import { useInfiniteCarousel } from '@/hooks/useInfiniteCarousel'; +import backIcon from '@/assets/common/back.svg'; +import nextIcon from '@/assets/common/next.svg'; export interface Section { title: string; @@ -61,8 +63,30 @@ export function RecruitingGroupCarousel({ sections }: Props) { isDragging = false; }; + const handlePrevClick = () => { + if (scrollRef.current) { + const container = scrollRef.current; + const cardWidth = cardRefs.current[0]?.offsetWidth || 0; + container.scrollLeft -= cardWidth + 20; + } + }; + + const handleNextClick = () => { + if (scrollRef.current) { + const container = scrollRef.current; + const cardWidth = cardRefs.current[0]?.offsetWidth || 0; + container.scrollLeft += cardWidth + 20; + } + }; + return ( + + 이전 + + + 다음 + ` color: ${({ roleType }) => { if (!roleType) return semanticColors.text.point.green; - + const role = roleType.toLowerCase(); - + if (role.includes('예술') || role.includes('art')) { return semanticColors.text.character.pink; } - if (role.includes('문학') || role.includes('literature')) { + if (role.includes('인문') || role.includes('humanities') || role.includes('철학')) { return semanticColors.text.character.mint; } if (role.includes('사회') || role.includes('sociology')) { @@ -83,7 +84,7 @@ export const MemberRole = styled.div<{ roleType?: string }>` if (role.includes('과학') || role.includes('science')) { return semanticColors.text.character.lavender; } - + return semanticColors.text.point.green; }}; font-size: ${typography.fontSize.xs}; diff --git a/src/components/members/MemberList.tsx b/src/components/members/MemberList.tsx index 9a1e5cdd..e9e6935a 100644 --- a/src/components/members/MemberList.tsx +++ b/src/components/members/MemberList.tsx @@ -78,6 +78,7 @@ const ProfileImageWithSrc = styled.img` height: 36px; border-radius: 50%; background-color: var(--color-grey-400); + border: 1px solid #888; flex-shrink: 0; object-fit: cover; `; diff --git a/src/components/memory/RecordItem/PollRecord.tsx b/src/components/memory/RecordItem/PollRecord.tsx index 5058606a..54b08d97 100644 --- a/src/components/memory/RecordItem/PollRecord.tsx +++ b/src/components/memory/RecordItem/PollRecord.tsx @@ -91,8 +91,9 @@ const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUp return { ...opt, percentage: updatedItem.percentage, + count: updatedItem.count, isVoted: updatedItem.isVoted, - isHighest: updatedItem.percentage === Math.max(...response.data.voteItems.map(item => item.percentage)) + isHighest: updatedItem.count === Math.max(...response.data.voteItems.map(item => item.count)) }; } return opt; @@ -142,8 +143,17 @@ const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUp } }; - // 아무도 투표하지 않았는지 확인 (모든 옵션이 0%인지 확인) - const hasVotes = currentOptions.some(option => option.percentage > 0); + // 아무도 투표하지 않았는지 확인 (모든 옵션이 0표인지 확인) + const hasVotes = currentOptions.some(option => option.count > 0); + + // 전체 투표수 계산 + const totalVotes = currentOptions.reduce((sum, option) => sum + option.count, 0); + + // 각 옵션의 퍼센트 계산 (애니메이션용) + const getPercentage = (count: number) => { + if (totalVotes === 0) return 0; + return (count / totalVotes) * 100; + }; return ( @@ -162,7 +172,7 @@ const PollRecord = ({ content, pollOptions, postId, shouldBlur = false, onVoteUp > {hasVotes && ( - {option.percentage}% + {option.count}표 )} diff --git a/src/components/memory/RecordItem/RecordItem.styled.ts b/src/components/memory/RecordItem/RecordItem.styled.ts index 10b52443..52d2efce 100644 --- a/src/components/memory/RecordItem/RecordItem.styled.ts +++ b/src/components/memory/RecordItem/RecordItem.styled.ts @@ -3,7 +3,7 @@ import { colors, typography, semanticColors } from '../../../styles/global/globa export const Container = styled.div<{ shouldBlur?: boolean }>` background-color: none; - filter: ${({ shouldBlur }) => (shouldBlur ? 'blur(2px)' : 'none')}; + filter: ${({ shouldBlur }) => (shouldBlur ? 'blur(3px)' : 'none')}; transition: filter 0.3s ease; position: relative; `; diff --git a/src/components/search/GroupSearchResult.tsx b/src/components/search/GroupSearchResult.tsx index 62696737..4861ba66 100644 --- a/src/components/search/GroupSearchResult.tsx +++ b/src/components/search/GroupSearchResult.tsx @@ -6,7 +6,7 @@ import { Filter } from '../common/Filter'; import type { SearchRoomItem } from '@/api/rooms/getSearchRooms'; const FILTER = ['마감임박순', '인기순']; -const CATEGORIES = ['문학', '과학·IT', '사회과학', '인문학', '예술'] as const; +const CATEGORIES = ['전체', '문학', '과학·IT', '사회과학', '인문학', '예술'] as const; type ResultType = 'searching' | 'searched'; @@ -60,12 +60,12 @@ const GroupSearchResult = ({ {showTabs && ( {CATEGORIES.map(tab => { - const selected = tab === currentCategory; + const selected = tab === currentCategory || (tab === '전체' && currentCategory === ''); return ( onChangeCategory(selected ? '' : tab)} + onClick={() => onChangeCategory(tab === '전체' ? '' : tab)} aria-pressed={selected} > {tab} diff --git a/src/hooks/useInfiniteCarousel.ts b/src/hooks/useInfiniteCarousel.ts index 47c04504..fa53ed14 100644 --- a/src/hooks/useInfiniteCarousel.ts +++ b/src/hooks/useInfiniteCarousel.ts @@ -68,12 +68,19 @@ export function useInfiniteCarousel(groups: Group[], options?: { scaleAmount?: n handleScroll(); }; - const timer = setTimeout(initializeScroll, 0); + const timer = setTimeout(initializeScroll, 100); + + const handleResize = () => { + setTimeout(initializeScroll, 50); + }; + container.addEventListener('scroll', handleScroll, { passive: true }); + window.addEventListener('resize', handleResize); return () => { clearTimeout(timer); container.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleResize); }; }, [infiniteGroups.length, handleScroll, middleIndex]); diff --git a/src/pages/group/Group.tsx b/src/pages/group/Group.tsx index 5757722b..21d1b4bd 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -11,12 +11,14 @@ import { MyGroupModal } from '@/components/group/MyGroupModal'; import CompletedGroupModal from '@/components/group/CompletedGroupModal'; import { useNavigate } from 'react-router-dom'; import makegroupfab from '../../assets/common/makegroupfab.svg'; +import searchChar from '../../assets/common/searchChar.svg'; import { getRoomsByCategory, type RoomItem } from '@/api/rooms/getRoomsByCategory'; +import { colors, typography } from '@/styles/global/global'; const convertRoomItemToGroup = ( room: RoomItem, category: string, - listType: 'deadline' | 'popular', + listType: 'deadline' | 'popular' | 'recent', ): GroupType => ({ id: `${room.roomId}-${category}-${listType}`, title: room.roomName, @@ -32,6 +34,7 @@ const Group = () => { const [isMyGroupModalOpen, setIsMyGroupModalOpen] = useState(false); const [isCompletedGroupModalOpen, setIsCompletedGroupModalOpen] = useState(false); const [sections, setSections] = useState([ + { title: '최근 생성된 독서 모임방', groups: [] }, { title: '마감 임박한 독서 모임방', groups: [] }, { title: '인기 있는 독서 모임방', groups: [] }, ]); @@ -41,6 +44,7 @@ const Group = () => { const categories = ['문학', '인문학', '사회과학', '과학·IT', '예술']; const deadlineRoomsData: GroupType[] = []; const popularRoomsData: GroupType[] = []; + const recentRoomsData: GroupType[] = []; for (const category of categories) { const response = await getRoomsByCategory(category); @@ -51,18 +55,24 @@ const Group = () => { 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); } } setSections([ + { title: '최근 생성된 독서 모임방', groups: recentRoomsData }, { title: '마감 임박한 독서 모임방', groups: deadlineRoomsData }, { title: '인기 있는 독서 모임방', groups: popularRoomsData }, ]); } catch (error) { console.error('방 목록 조회 오류:', error); setSections([ + { title: '최근 생성된 독서 모임방', groups: [] }, { title: '마감 임박한 독서 모임방', groups: [] }, { title: '인기 있는 독서 모임방', groups: [] }, ]); @@ -83,6 +93,14 @@ const Group = () => { navigate('/group/search'); }; + const handleAllRoomsClick = () => { + navigate('/group/search', { + state: { + allRooms: true, + }, + }); + }; + return ( {isMyGroupModalOpen && } @@ -91,6 +109,10 @@ const Group = () => { + + 전체 모임방을 한 눈에 들러보세요! + 검색 캐릭터 이미지 + @@ -110,5 +132,24 @@ const Wrapper = styled.div` min-height: 100vh; margin: 0 auto; padding-top: 56px; - background-color: #121212; + background-color: ${colors.black.main}; +`; + +const AllRoomsButton = styled.div` + 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; + > img { + position: absolute; + right: 5%; + top: -2px; + } `; diff --git a/src/pages/groupDetail/GroupDetail.styled.ts b/src/pages/groupDetail/GroupDetail.styled.ts index dd576b5b..b2d8f54c 100644 --- a/src/pages/groupDetail/GroupDetail.styled.ts +++ b/src/pages/groupDetail/GroupDetail.styled.ts @@ -276,4 +276,9 @@ export const BottomButton = styled.button` border: none; z-index: 10; cursor: pointer; + + &:disabled { + background-color: ${colors.grey[300]}; + cursor: not-allowed; + } `; diff --git a/src/pages/groupDetail/GroupDetail.tsx b/src/pages/groupDetail/GroupDetail.tsx index 806cb12d..94a2e7f5 100644 --- a/src/pages/groupDetail/GroupDetail.tsx +++ b/src/pages/groupDetail/GroupDetail.tsx @@ -99,19 +99,17 @@ const GroupDetail = () => { } } catch (error: unknown) { console.error('방 상세 정보 조회 실패:', error); - - // 모집기간이 만료된 방인 경우 - 진행중인 방으로 리다이렉트 + if (error instanceof Error && error.message === '모집기간이 만료된 방입니다.') { navigate(`/group/detail/joined/${roomId}`, { replace: true }); return; } - - // 방 접근 권한이 없는 경우 - 모임 홈으로 리다이렉트 + if (error instanceof Error && error.message === '방 접근 권한이 없습니다.') { navigate('/group', { replace: true }); return; } - + setError('방 정보를 불러오는데 실패했습니다.'); } finally { setIsLoading(false); @@ -359,7 +357,10 @@ const GroupDetail = () => { )} - + = recruitCount)} + > {roomData.isHost ? '모집 마감하기' : isJoining ? '참여 취소하기' : '참여하기'} diff --git a/src/pages/groupSearch/GroupSearch.tsx b/src/pages/groupSearch/GroupSearch.tsx index 064987e4..ce8199a2 100644 --- a/src/pages/groupSearch/GroupSearch.tsx +++ b/src/pages/groupSearch/GroupSearch.tsx @@ -2,6 +2,7 @@ import TitleHeader from '@/components/common/TitleHeader'; import { Modal, Overlay } from '@/components/group/Modal.styles'; import leftArrow from '../../assets/common/leftArrow.svg'; import SearchBar from '@/components/search/SearchBar'; +import rightChevron from '../../assets/common/right-Chevron.svg'; import { useState, useEffect, useCallback, useRef } from 'react'; import RecentSearchTabs from '@/components/search/RecentSearchTabs'; import GroupSearchResult from '@/components/search/GroupSearchResult'; @@ -10,13 +11,14 @@ import { deleteRecentSearch } from '@/api/recentsearch/deleteRecentSearch'; import { getSearchRooms, type SearchRoomItem } from '@/api/rooms/getSearchRooms'; import styled from '@emotion/styled'; import { colors, typography } from '@/styles/global/global'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; type SortKey = 'deadline' | 'memberCount'; type SearchStatus = 'idle' | 'searching' | 'searched'; const GroupSearch = () => { const navigate = useNavigate(); + const location = useLocation(); const [searchTerm, setSearchTerm] = useState(''); const [searchStatus, setSearchStatus] = useState('idle'); @@ -54,7 +56,6 @@ const GroupSearch = () => { })(); }, []); - // searchStatus가 'idle'로 변경될 때 최근 검색어 새로고침 useEffect(() => { if (searchStatus === 'idle') { fetchRecentSearches(); @@ -71,9 +72,13 @@ const GroupSearch = () => { }; const searchFirstPage = useCallback( - async (term: string, sortKey: SortKey, status: 'searching' | 'searched') => { - if (!term.trim()) return; - + async ( + term: string, + sortKey: SortKey, + status: 'searching' | 'searched', + categoryParam: string, + isAllCategory: boolean = false, + ) => { setIsLoading(true); setError(null); setRooms([]); @@ -82,7 +87,14 @@ const GroupSearch = () => { try { const isFinalized = status === 'searched'; - const res = await getSearchRooms(term.trim(), sortKey, undefined, isFinalized, category); + const res = await getSearchRooms( + term.trim(), + sortKey, + undefined, + isFinalized, + categoryParam, + isAllCategory, + ); if (res.isSuccess) { const { roomList, nextCursor: nc, isLast: last } = res.data; @@ -98,9 +110,20 @@ const GroupSearch = () => { setIsLoading(false); } }, - [category], + [], ); + useEffect(() => { + if (location.state?.allRooms) { + navigate(location.pathname, { replace: true }); + + setSearchTerm(''); + setSearchStatus('searched'); + setShowTabs(true); + setCategory(''); + } + }, [location.state?.allRooms, navigate, location.pathname]); + const handleChange = (value: string) => { setSearchTerm(value); if (searchTimeoutId) clearTimeout(searchTimeoutId); @@ -120,7 +143,7 @@ const GroupSearch = () => { setSearchStatus('searching'); setShowTabs(false); const id = setTimeout(() => { - searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching'); + searchFirstPage(trimmed, toSortKey(selectedFilter), 'searching', category); }, 300); setSearchTimeoutId(id); }; @@ -135,7 +158,6 @@ const GroupSearch = () => { setSearchStatus('searched'); setShowTabs(true); - searchFirstPage(term, toSortKey(selectedFilter), 'searched'); }; const handleRecentSearchClick = (recent: string) => { @@ -146,32 +168,71 @@ const GroupSearch = () => { setSearchTerm(recent); setSearchStatus('searched'); setShowTabs(true); - searchFirstPage(recent.trim(), toSortKey(selectedFilter), 'searched'); }; + const handleAllRoomsClick = () => { + if (searchTimeoutId) { + clearTimeout(searchTimeoutId); + setSearchTimeoutId(null); + } + setSearchTerm(''); + setSearchStatus('searched'); + setShowTabs(true); + setCategory(''); + }; + + const searchStatusRef = useRef(searchStatus); + const categoryRef = useRef(category); + const selectedFilterRef = useRef(selectedFilter); + const searchTermRef = useRef(searchTerm); + + useEffect(() => { + searchStatusRef.current = searchStatus; + categoryRef.current = category; + selectedFilterRef.current = selectedFilter; + searchTermRef.current = searchTerm; + }); + useEffect(() => { + if (searchStatus !== 'searched') return; + const term = searchTerm.trim(); - if (!term) return; + const isAllCategory = !term && category === ''; + + searchFirstPage(term, toSortKey(selectedFilter), 'searched', category, isAllCategory); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedFilter, category, searchStatus, searchTerm]); + + useEffect(() => { + const term = searchTerm.trim(); + if (!term || searchStatus !== 'searching') return; if (searchTimeoutId) { clearTimeout(searchTimeoutId); setSearchTimeoutId(null); } - setSearchStatus('searching'); - searchFirstPage(term, toSortKey(selectedFilter), 'searching'); - }, [selectedFilter, category, searchTerm, searchFirstPage, toSortKey, searchTimeoutId]); + + const id = setTimeout(() => { + const currentCategory = categoryRef.current; + searchFirstPage(term, toSortKey(selectedFilter), 'searching', currentCategory); + }, 300); + setSearchTimeoutId(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, searchStatus, selectedFilter]); const loadMore = useCallback(async () => { if (!searchTerm.trim() || !nextCursor || isLast || isLoadingMore) return; try { setIsLoadingMore(true); const isFinalized = searchStatus === 'searched'; + const isAllCategory = !searchTerm.trim() && category === ''; const res = await getSearchRooms( searchTerm.trim(), toSortKey(selectedFilter), nextCursor, isFinalized, category, + isAllCategory, ); if (res.isSuccess) { const { roomList, nextCursor: nc, isLast: last } = res.data; @@ -187,16 +248,8 @@ const GroupSearch = () => { } finally { setIsLoadingMore(false); } - }, [ - searchTerm, - nextCursor, - isLast, - isLoadingMore, - selectedFilter, - toSortKey, - searchStatus, - category, - ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, nextCursor, isLast, isLoadingMore, selectedFilter, searchStatus, category]); const lastRoomElementCallback = useCallback( (node: HTMLDivElement | null) => { @@ -288,18 +341,24 @@ const GroupSearch = () => { )} ) : ( - 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} - /> + <> + 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} + /> + +

전체 모임방 둘러보기

+ 전체 모임방 버튼 +
+ )} @@ -316,3 +375,14 @@ const LoadingMessage = styled.div` color: ${colors.white}; font-size: ${typography.fontSize.base}; `; + +const AllRoomsButton = styled.div` + display: flex; + justify-content: space-between; + padding: 30px 20px; + background-color: transparent; + color: ${colors.grey[100]}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + cursor: pointer; +`; diff --git a/src/pages/memory/Memory.tsx b/src/pages/memory/Memory.tsx index fd413215..3f429032 100644 --- a/src/pages/memory/Memory.tsx +++ b/src/pages/memory/Memory.tsx @@ -31,14 +31,18 @@ const convertPostToRecord = (post: Post): Record => { isWriter: post.isWriter, isLiked: post.isLiked, isLocked: post.isLocked, // 블러 처리 여부 추가 - pollOptions: post.voteItems.map((item, index) => ({ - id: item.voteItemId.toString(), - text: item.itemName, - percentage: item.percentage, - isHighest: index === 0, - voteItemId: item.voteItemId, - isVoted: item.isVoted, - })), + pollOptions: post.voteItems.map((item) => { + const maxCount = Math.max(...post.voteItems.map(v => v.count || 0)); + return { + id: item.voteItemId.toString(), + text: item.itemName, + percentage: item.percentage, + count: item.count || 0, + isHighest: (item.count || 0) === maxCount && maxCount > 0, + voteItemId: item.voteItemId, + isVoted: item.isVoted, + }; + }), }; }; diff --git a/src/types/memory.ts b/src/types/memory.ts index f8041c31..44639d85 100644 --- a/src/types/memory.ts +++ b/src/types/memory.ts @@ -3,6 +3,7 @@ export interface VoteItem { voteItemId: number; itemName: string; percentage: number; + count: number; isVoted: boolean; } @@ -82,6 +83,7 @@ export interface PollOption { id: string; text: string; percentage: number; + count: number; isHighest: boolean; voteItemId: number; // 투표 API에 필요한 ID isVoted: boolean; // 현재 사용자가 투표했는지 여부 diff --git a/src/types/record.ts b/src/types/record.ts index 878e68a9..db0c0995 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -35,6 +35,7 @@ export interface VoteItemResult { voteItemId: number; // 투표 아이템 ID itemName: string; // 투표 옵션 이름 percentage: number; // 득표율 + count: number; // 득표수 isVoted: boolean; // 현재 사용자가 투표했는지 여부 }