diff --git a/src/api/rooms/getJoinedRooms.ts b/src/api/rooms/getJoinedRooms.ts new file mode 100644 index 00000000..fb104d1b --- /dev/null +++ b/src/api/rooms/getJoinedRooms.ts @@ -0,0 +1,34 @@ +import { apiClient } from '../index'; + +// 가입한 방 목록 응답 데이터 타입 +export interface JoinedRoomItem { + roomId: number; + bookImageUrl: string; + roomTitle: string; + memberCount: number; + userPercentage: number; +} + +export interface JoinedRoomsResponse { + isSuccess: boolean; + code: number; + message: string; + data: { + roomList: JoinedRoomItem[]; + nickname: string; + page: number; + size: number; + last: boolean; + first: boolean; + }; +} + +export const getJoinedRooms = async (page: number = 1): Promise => { + try { + const response = await apiClient.get(`/rooms/home/joined?page=${page}`); + return response.data; + } catch (error) { + console.error('가입한 방 목록 조회 API 오류:', error); + throw error; + } +}; diff --git a/src/api/rooms/getMyRooms.ts b/src/api/rooms/getMyRooms.ts new file mode 100644 index 00000000..445f0627 --- /dev/null +++ b/src/api/rooms/getMyRooms.ts @@ -0,0 +1,44 @@ +import { apiClient } from '../index'; + +export type RoomType = 'playingAndRecruiting' | 'recruiting' | 'playing' | 'expired'; + +// 방 데이터 타입 +export interface Room { + roomId: number; + bookImageUrl: string; + roomName: string; + recruitCount: number; + memberCount: number; + endDate: string; + type: string; +} + +// 내 방 조회 응답 타입 +export interface MyRoomsResponse { + isSuccess: boolean; + code: number; + message: string; + data: { + roomList: Room[]; + nextCursor: string; + isLast: boolean; + }; +} + +export const getMyRooms = async ( + type: RoomType = 'playingAndRecruiting', + cursor: string | null = null, +): Promise => { + try { + const params = new URLSearchParams(); + params.append('type', type); + if (cursor) { + params.append('cursor', cursor); + } + const response = await apiClient.get(`/rooms/my?${params.toString()}`); + return response.data; + } catch (error) { + console.error('내 방 조회 API 오류:', error); + throw error; + } +}; diff --git a/src/api/rooms/getRoomDetail.ts b/src/api/rooms/getRoomDetail.ts new file mode 100644 index 00000000..7a21b057 --- /dev/null +++ b/src/api/rooms/getRoomDetail.ts @@ -0,0 +1,49 @@ +import { apiClient } from '../index'; + +// 방 상세 정보 응답 타입 +export interface RoomDetailResponse { + isSuccess: boolean; + code: number; + message: string; + data: { + isHost: boolean; + isJoining: boolean; + roomId: number; + roomName: string; + roomImageUrl: string; + isPublic: boolean; + progressStartDate: string; + progressEndDate: string; + recruitEndDate: string; + category: string; + roomDescription: string; + memberCount: number; + recruitCount: number; + isbn: string; + bookImageUrl: string; + bookTitle: string; + authorName: string; + bookDescription: string; + publisher: string; + recommendRooms: RecommendRoom[]; + }; +} + +export interface RecommendRoom { + roomId: number; + roomImageUrl: string; + roomName: string; + memberCount: number; + recruitCount: number; + recruitEndDate: string; +} + +export const getRoomDetail = async (roomId: number): Promise => { + try { + const response = await apiClient.get(`/rooms/${roomId}/recruiting`); + return response.data; + } catch (error) { + console.error('방 상세 정보 조회 API 오류:', error); + throw error; + } +}; diff --git a/src/api/rooms/getRoomsByCategory.ts b/src/api/rooms/getRoomsByCategory.ts new file mode 100644 index 00000000..b2c55583 --- /dev/null +++ b/src/api/rooms/getRoomsByCategory.ts @@ -0,0 +1,33 @@ +import { apiClient } from '../index'; + +// 방 목록 응답 데이터 타입 +export interface RoomItem { + roomId: number; + bookImageUrl: string; + roomName: string; + recruitCount: number; + memberCount: number; + deadlineDate: string; +} + +export interface RoomsResponse { + isSuccess: boolean; + code: number; + message: string; + data: { + deadlineRoomList: RoomItem[]; + popularRoomList: RoomItem[]; + }; +} + +export const getRoomsByCategory = async (category: string): Promise => { + try { + const response = await apiClient.get( + `/rooms?category=${encodeURIComponent(category)}`, + ); + return response.data; + } catch (error) { + console.error('방 목록 조회 API 오류:', error); + throw error; + } +}; diff --git a/src/components/group/CompletedGroupModal.tsx b/src/components/group/CompletedGroupModal.tsx index b0954499..b2708cd1 100644 --- a/src/components/group/CompletedGroupModal.tsx +++ b/src/components/group/CompletedGroupModal.tsx @@ -1,86 +1,58 @@ +import { useState, useEffect } from 'react'; import styled from '@emotion/styled'; import leftArrow from '../../assets/common/leftArrow.svg'; import type { Group } from './MyGroupBox'; import { GroupCard } from './GroupCard'; import TitleHeader from '../common/TitleHeader'; import { Modal, Overlay } from './Modal.styles'; +import { getMyRooms, type Room } from '@/api/rooms/getMyRooms'; +import { colors, typography } from '@/styles/global/global'; interface CompletedGroupModalProps { onClose: () => void; } -const dummyCompletedGroups: Group[] = [ - { - id: '1', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 1, - genre: '문학', - }, - { - id: '2', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 2, - genre: '문학', - }, - { - id: '3', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 3, - genre: '문학', - }, - { - id: '4', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '문학', - }, - { - id: '5', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '과학·IT', - }, - { - id: '6', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '과학·IT', - }, -]; +const CompletedGroupModal = ({ onClose }: CompletedGroupModalProps) => { + const [rooms, setRooms] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); -const userName = '00'; + const convertRoomToGroup = (room: Room): Group => { + return { + id: room.roomId.toString(), + title: room.roomName, + userName: '', + participants: room.memberCount, + maximumParticipants: room.recruitCount, + coverUrl: room.bookImageUrl, + deadLine: 0, + isOnGoing: false, + }; + }; -const CompletedGroupModal = ({ onClose }: CompletedGroupModalProps) => { + useEffect(() => { + const fetchCompletedRooms = async () => { + try { + setIsLoading(true); + setError(null); + const response = await getMyRooms('expired', null); + if (response.isSuccess) { + setRooms(response.data.roomList); + } else { + setError(response.message); + } + } catch (error) { + console.error('완료된 방 목록 조회 실패:', error); + setError('완료된 방 목록을 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }; + + fetchCompletedRooms(); + }, []); + + const convertedGroups = rooms.map(convertRoomToGroup); return ( @@ -89,11 +61,20 @@ const CompletedGroupModal = ({ onClose }: CompletedGroupModalProps) => { leftIcon={뒤로 가기} onLeftClick={onClose} /> - {userName}님이 참여했던 모임방들을 확인해보세요. - - {dummyCompletedGroups.map(group => ( - - ))} + 00님이 참여했던 모임방들을 확인해보세요. + + {isLoading ? ( + 로딩 중... + ) : error ? ( + {error} + ) : convertedGroups.length > 0 ? ( + convertedGroups.map(group => ) + ) : ( + + 완료된 모임방이 없어요 + 아직 완료된 모임방이 없습니다. + + )} @@ -103,21 +84,63 @@ const CompletedGroupModal = ({ onClose }: CompletedGroupModalProps) => { export default CompletedGroupModal; const Text = styled.p` - font-size: var(--font-size-medium01); - font-weight: var(--font-weight-regular); - color: var(--color-white); + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + color: ${colors.white}; margin: 96px 20px 20px 20px; `; -const Content = styled.div` +const Content = styled.div<{ isEmpty?: boolean }>` display: grid; gap: 20px; - overflow-y: auto; + overflow-y: ${({ isEmpty }) => (isEmpty ? 'visible' : 'auto')}; padding: 0 20px; - + flex: 1; grid-template-columns: 1fr; @media (min-width: 584px) { grid-template-columns: 1fr 1fr; } `; + +const LoadingMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: ${colors.white}; + font-size: ${typography.fontSize.base}; +`; + +const ErrorMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: #ff6b6b; + font-size: ${typography.fontSize.base}; +`; + +const EmptyState = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: ${colors.grey[100]}; + text-align: center; + height: 100%; +`; + +const EmptyTitle = styled.p` + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + margin-bottom: 8px; + color: ${colors.white}; +`; + +const EmptySubText = styled.p` + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + color: ${colors.grey[100]}; +`; diff --git a/src/components/group/GroupCard.tsx b/src/components/group/GroupCard.tsx index 1396b271..c238c7bf 100644 --- a/src/components/group/GroupCard.tsx +++ b/src/components/group/GroupCard.tsx @@ -9,12 +9,13 @@ interface Props { isOngoing?: boolean; type?: 'main' | 'search' | 'modal'; isRecommend?: boolean; + onClick?: () => void; } export const GroupCard = forwardRef( - ({ group, isOngoing, type = 'main', isRecommend = false }, ref) => { + ({ group, isOngoing, type = 'main', isRecommend = false, onClick }, ref) => { return ( - + {group.title} @@ -52,7 +53,9 @@ const Card = styled.div<{ cardType: 'main' | 'search' | 'modal' }>` box-sizing: border-box; padding: ${({ cardType }) => (cardType === 'search' ? '24px 12px 12px 12px' : '12px')}; gap: 12px; - width: 100%; + min-width: 208px; + min-height: 80px; + padding: 12px; `; const Cover = styled.img<{ cardType: 'main' | 'search' | 'modal'; isRecommend?: boolean }>` diff --git a/src/components/group/MyGroupBox.tsx b/src/components/group/MyGroupBox.tsx index 2a74a48d..a040cba0 100644 --- a/src/components/group/MyGroupBox.tsx +++ b/src/components/group/MyGroupBox.tsx @@ -2,6 +2,9 @@ import { MyGroupCard } from './MyGroupCard'; import { useInfiniteCarousel } from '../../hooks/useInfiniteCarousel'; import styled from '@emotion/styled'; import rightChevron from '../../assets/common/right-Chevron.svg'; +import { useState, useEffect } from 'react'; +import { getJoinedRooms, type JoinedRoomItem } from '@/api/rooms/getJoinedRooms'; +import { colors, typography } from '@/styles/global/global'; export interface Group { id: number | string; @@ -16,12 +19,46 @@ export interface Group { isOnGoing?: boolean; } +const convertJoinedRoomToGroup = (room: JoinedRoomItem): Group => ({ + id: room.roomId, + title: room.roomTitle, + participants: room.memberCount, + coverUrl: room.bookImageUrl, + progress: room.userPercentage, +}); + interface MyGroupProps { - groups: Group[]; onMyGroupsClick: () => void; } -export function MyGroupBox({ groups, onMyGroupsClick }: MyGroupProps) { +export function MyGroupBox({ onMyGroupsClick }: MyGroupProps) { + const [groups, setGroups] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchJoinedRooms = async () => { + try { + setLoading(true); + setError(null); + const response = await getJoinedRooms(1); + + if (response.isSuccess) { + const convertedGroups = response.data.roomList.map(convertJoinedRoomToGroup); + setGroups(convertedGroups); + } + } catch (error) { + console.error('가입한 방 목록 조회 오류:', error); + setError('방 목록을 불러오는데 실패했습니다.'); + setGroups([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchJoinedRooms(); + }, []); + const { scrollRef, cardRefs, infiniteGroups, current } = useInfiniteCarousel(groups); return ( @@ -32,28 +69,44 @@ export function MyGroupBox({ groups, onMyGroupsClick }: MyGroupProps) { 내 모임방 버튼 - - {infiniteGroups.map((g, i) => ( - { - cardRefs.current[i] = el; - }} - /> - ))} - - - {groups.map((_, i) => ( - - ))} - + {loading ? ( + + 모임방을 불러오는 중... + + ) : error ? ( + + {error} + + ) : groups.length > 0 ? ( + <> + + {infiniteGroups.map((g, i) => ( + { + cardRefs.current[i] = el; + }} + /> + ))} + + + {groups.map((_, i) => ( + + ))} + + + ) : ( + + 가입한 모임방이 없어요 + + )} ); } const Container = styled.div` - background-color: var(--color-main-black); + background-color: ${colors.black.main}; position: relative; width: 100%; overflow-x: hidden; @@ -67,9 +120,9 @@ const Header = styled.div` const Title = styled.h2` flex: 1; - font-size: var(--font-size-large02); - font-weight: var(--font-weight-bold); - color: var(--color-white); + font-size: ${typography.fontSize.xl}; + font-weight: ${typography.fontWeight.bold}; + color: ${colors.white}; margin: 0; `; @@ -106,6 +159,45 @@ const Dot = styled.div<{ active: boolean }>` width: 4px; height: 4px; border-radius: 50%; - background: ${({ active }) => (active ? 'var(--color-white)' : `var(--color-grey-300)`)}; + background: ${({ active }) => (active ? colors.white : colors.grey[300])}; transition: background-color 0.3s; `; + +const LoadingContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; +`; + +const LoadingText = styled.p` + color: ${colors.grey[300]}; + font-size: ${typography.fontSize.base}; + margin: 0; +`; + +const ErrorContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; +`; + +const ErrorText = styled.p` + color: ${colors.red}; + font-size: ${typography.fontSize.base}; + margin: 0; +`; + +const EmptyContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; +`; + +const EmptyText = styled.p` + color: ${colors.grey[300]}; + font-size: ${typography.fontSize.base}; + margin: 0; +`; diff --git a/src/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index dc036d90..3731fd7d 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -1,96 +1,69 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import styled from '@emotion/styled'; import TitleHeader from '../common/TitleHeader'; import leftArrow from '../../assets/common/leftArrow.svg'; import type { Group } from './MyGroupBox'; import { GroupCard } from './GroupCard'; import { Modal, Overlay } from './Modal.styles'; +import { getMyRooms, type Room, type RoomType } from '@/api/rooms/getMyRooms'; +import { colors, typography } from '@/styles/global/global'; interface MyGroupModalProps { onClose: () => void; } -const dummyMyGroups: Group[] = [ - { - id: '1', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 1, - genre: '문학', - isOnGoing: true, - }, - { - id: '2', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 2, - genre: '문학', - isOnGoing: true, - }, - { - id: '3', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 3, - genre: '문학', - isOnGoing: true, - }, - { - id: '4', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '문학', - isOnGoing: true, - }, - { - id: '5', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '과학·IT', - isOnGoing: false, - }, - { - id: '6', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '과학·IT', - isOnGoing: false, - }, -]; - export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { const [selected, setSelected] = useState<'진행중' | '모집중' | ''>(''); + const [rooms, setRooms] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const convertRoomToGroup = (room: Room): Group => { + return { + id: room.roomId.toString(), + title: room.roomName, + userName: '', + participants: room.memberCount, + maximumParticipants: room.recruitCount, + coverUrl: room.bookImageUrl, + deadLine: 0, + genre: '', + isOnGoing: room.type === 'playing' || room.type === 'playingAndRecruiting', + }; + }; + + useEffect(() => { + const fetchRooms = async () => { + try { + setIsLoading(true); + setError(null); + + const roomType: RoomType = + selected === '진행중' + ? 'playing' + : selected === '모집중' + ? 'recruiting' + : 'playingAndRecruiting'; + + const response = await getMyRooms(roomType, null); + + if (response.isSuccess) { + setRooms(response.data.roomList); + } else { + setError(response.message); + } + } catch (error) { + console.error('방 목록 조회 실패:', error); + setError('방 목록을 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }; - const filtered = selected - ? dummyMyGroups.filter(g => (selected === '진행중' ? g.isOnGoing : !g.isOnGoing)) - : dummyMyGroups; + fetchRooms(); + }, [selected]); + + const convertedGroups = rooms.map(convertRoomToGroup); return ( @@ -113,14 +86,32 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { - {filtered.map(group => ( - - ))} + {isLoading ? ( + 로딩 중... + ) : error ? ( + {error} + ) : convertedGroups.length > 0 ? ( + convertedGroups.map(group => ( + + )) + ) : ( + + + {selected === '진행중' + ? '진행중인 모임방이 없어요' + : selected === '모집중' + ? '모집중인 모임방이 없어요' + : '참여중인 모임방이 없어요'} + + + {selected === '진행중' + ? '진행중인 모임방에 참여해보세요!' + : selected === '모집중' + ? '모집중인 모임방에 참여해보세요!' + : '첫 번째 모임방에 참여해보세요!'} + + + )} @@ -135,13 +126,12 @@ const TabContainer = styled.div` const Tab = styled.button<{ selected: boolean }>` white-space: nowrap; - padding: 6px 12px; - font-size: var(--font-size-small03); - font-weight: var(--font-weight-regular); + padding: 8px 12px; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; border: none; border-radius: 16px; - background: ${({ selected }) => - selected ? 'var(--color-purple-main)' : 'var(--color-darkgrey-main)'}; + background: ${({ selected }) => (selected ? colors.purple.main : colors.darkgrey.main)}; color: #fff; cursor: pointer; `; @@ -158,3 +148,47 @@ const Content = styled.div` grid-template-columns: 1fr 1fr; } `; + +const LoadingMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: #fff; + font-size: ${typography.fontSize.base}; +`; + +const ErrorMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: #ff6b6b; + font-size: ${typography.fontSize.base}; +`; + +const EmptyState = styled.div` + flex: 1; + min-height: 78vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px 20px; + margin-bottom: 70px; + color: ${colors.grey[100]}; + text-align: center; +`; + +const EmptyTitle = styled.p` + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + margin-bottom: 8px; + color: ${colors.white}; +`; + +const EmptySubText = styled.p` + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + color: ${colors.grey[100]}; +`; diff --git a/src/components/group/RecruitingGroupBox.tsx b/src/components/group/RecruitingGroupBox.tsx index 2b8c134b..cfa97b9a 100644 --- a/src/components/group/RecruitingGroupBox.tsx +++ b/src/components/group/RecruitingGroupBox.tsx @@ -2,6 +2,9 @@ import { useMemo, useState } from 'react'; import styled from '@emotion/styled'; import type { Group } from './MyGroupBox'; import { GroupCard } from './GroupCard'; +import { colors, typography } from '@/styles/global/global'; +import { useNavigate } from 'react-router-dom'; +import { getRoomDetail } from '@/api/rooms/getRoomDetail'; interface Props { groups: Group[]; @@ -12,9 +15,25 @@ const GENRE = ['문학', '과학·IT', '사회과학', '인문학', '예술']; export function RecruitingGroupBox({ groups, title }: Props) { const [selected, setSelected] = useState('문학'); + const navigate = useNavigate(); const filtered = useMemo(() => groups.filter(g => g.genre === selected), [groups, selected]); + const handleGroupCardClick = async (groupId: number | string) => { + try { + const roomId = typeof groupId === 'string' ? parseInt(groupId) : groupId; + + const response = await getRoomDetail(roomId); + + if (response.isSuccess) { + navigate(`/group/detail/${roomId}`); + } + } catch (error) { + console.error('방 상세 정보 조회 오류:', error); + navigate(`/group/${groupId}`); + } + }; + return ( {title} @@ -26,9 +45,21 @@ export function RecruitingGroupBox({ groups, title }: Props) { ))} - {filtered.map(group => ( - - ))} + {filtered.length > 0 ? ( + filtered.map(group => ( + handleGroupCardClick(group.id)} + /> + )) + ) : ( + + 모임방이 아직 없어요. + 해당 장르의 모임방이 생기면 보여줄게요! + + )} ); @@ -95,3 +126,29 @@ const Grid = styled.div` grid-template-columns: 1fr 1fr; } `; + +const EmptyContent = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + padding: 60px 20px; + grid-column: 1 / -1; +`; + +const EmptyMainText = styled.p` + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + text-align: center; + margin: 0; +`; + +const EmptySubText = styled.p` + color: ${colors.grey[100]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + text-align: center; + margin: 0; +`; diff --git a/src/pages/group/CreateGroup.tsx b/src/pages/group/CreateGroup.tsx index d6e975f7..0f9e84e0 100644 --- a/src/pages/group/CreateGroup.tsx +++ b/src/pages/group/CreateGroup.tsx @@ -137,9 +137,8 @@ const CreateGroup = () => { if (isSuccessful) { // 성공 시 모집 중인 방 상세 페이지로 이동 - navigate('/group/detail', { + navigate(`/group/detail/${response.data.roomId}`, { replace: true, - state: { roomId: response.data.roomId }, }); } else { alert(`방 생성에 실패했습니다: ${response.message} (코드: ${response.code})`); diff --git a/src/pages/group/Group.tsx b/src/pages/group/Group.tsx index 00adab86..2d401992 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -6,121 +6,77 @@ import { MyGroupBox } from '../../components/group/MyGroupBox'; import Blank from '@/components/common/Blank'; import styled from '@emotion/styled'; import { RecruitingGroupCarousel, type Section } from '@/components/group/RecruitingGroupCarousel'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; 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 { getRoomsByCategory, type RoomItem } from '@/api/rooms/getRoomsByCategory'; -const dummyMyGroups: GroupType[] = [ - { - id: '1', - title: '호르몬 체인지 완독하는 방', - participants: 22, - userName: 'hoho', - progress: 40, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - }, - { - id: '2', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - progress: 0, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - }, - { - id: '3', - title: '일본 소설 좋아하는 사람들', - userName: 'hoho3', - participants: 30, - progress: 100, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - }, -]; - -const dummyRecruitingGroups: GroupType[] = [ - { - id: '1', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 1, - genre: '문학', - }, - { - id: '2', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 2, - genre: '문학', - }, - { - id: '3', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 3, - genre: '문학', - }, - { - id: '4', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '문학', - }, - { - id: '5', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '과학·IT', - }, - { - id: '6', - title: '시집만 읽는 사람들 3월', - userName: 'hoho2', - participants: 15, - maximumParticipants: 30, - coverUrl: - 'https://marketplace.canva.com/EAF9zlwqylI/1/0/1003w/canva-%EB%B2%A0%EC%9D%B4%EC%A7%80-%EC%A3%BC%ED%99%A9-%EA%B7%80%EC%97%BD%EA%B3%A0-%EB%AF%B8%EB%8B%88%EB%A9%80%ED%95%9C-%EC%9D%BC%EB%9F%AC%EC%8A%A4%ED%8A%B8-e%EB%B6%81-%EC%9C%84%EB%A1%9C-%EC%A2%8B%EC%9D%80%EA%B8%80-%EC%B1%85%ED%91%9C%EC%A7%80-zrZ6hI8_IWo.jpg', - deadLine: 4, - genre: '과학·IT', - }, -]; - -const sections: Section[] = [ - { title: '마감 임박한 독서 모임방', groups: dummyRecruitingGroups }, - { title: '인기 있는 독서 모임방', groups: dummyRecruitingGroups }, - { title: '인플루언서·작가 독서 모임방', groups: dummyRecruitingGroups }, -]; +const convertRoomItemToGroup = ( + room: RoomItem, + category: string, + listType: 'deadline' | 'popular', +): GroupType => ({ + id: `${room.roomId}-${category}-${listType}`, + title: room.roomName, + participants: room.memberCount, + maximumParticipants: room.recruitCount, + coverUrl: room.bookImageUrl, + deadLine: Math.ceil( + (new Date(room.deadlineDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24), + ), + genre: category, +}); const Group = () => { const navigate = useNavigate(); const [isMyGroupModalOpen, setIsMyGroupModalOpen] = useState(false); const [isCompletedGroupModalOpen, setIsCompletedGroupModalOpen] = useState(false); + const [sections, setSections] = useState([ + { title: '마감 임박한 독서 모임방', groups: [] }, + { title: '인기 있는 독서 모임방', groups: [] }, + { title: '인플루언서·작가 독서 모임방', groups: [] }, + ]); + + const fetchAllRoomsData = async () => { + try { + const categories = ['문학', '인문학', '사회과학', '과학·IT', '예술']; + const deadlineRoomsData: GroupType[] = []; + const popularRoomsData: GroupType[] = []; + + for (const category of categories) { + const response = await getRoomsByCategory(category); + if (response.isSuccess) { + const deadlineGroups = response.data.deadlineRoomList.map(room => + convertRoomItemToGroup(room, category, 'deadline'), + ); + const popularGroups = response.data.popularRoomList.map(room => + convertRoomItemToGroup(room, category, 'popular'), + ); + deadlineRoomsData.push(...deadlineGroups); + popularRoomsData.push(...popularGroups); + } + } + + setSections([ + { title: '마감 임박한 독서 모임방', groups: deadlineRoomsData }, + { title: '인기 있는 독서 모임방', groups: popularRoomsData }, + { title: '인플루언서·작가 독서 모임방', groups: [] }, + ]); + } catch (error) { + console.error('방 목록 조회 오류:', error); + setSections([ + { title: '마감 임박한 독서 모임방', groups: [] }, + { title: '인기 있는 독서 모임방', groups: [] }, + { title: '인플루언서·작가 독서 모임방', groups: [] }, + ]); + } + }; + + useEffect(() => { + fetchAllRoomsData(); + }, []); const openMyGroupModal = () => setIsMyGroupModalOpen(true); const closeMyGroupModal = () => setIsMyGroupModalOpen(false); @@ -129,15 +85,16 @@ const Group = () => { const closeCompletedGroupModal = () => setIsCompletedGroupModalOpen(false); const handleSearchBarClick = () => { - navigate('/groupsearch'); + navigate('/group/search'); }; + return ( {isMyGroupModalOpen && } {isCompletedGroupModalOpen && } - + diff --git a/src/pages/groupDetail/GroupDetail.styled.ts b/src/pages/groupDetail/GroupDetail.styled.ts index c34757ea..92ec1f63 100644 --- a/src/pages/groupDetail/GroupDetail.styled.ts +++ b/src/pages/groupDetail/GroupDetail.styled.ts @@ -176,13 +176,14 @@ export const BookDetails = styled.div` font-weight: ${typography.fontWeight.medium}; gap: 20px; color: ${colors.white}; - margin: auto 0; + margin-top: 8px; `; export const BookIntro = styled.div` > p { margin-top: 4px; color: ${colors.grey[200]}; + font-size: ${typography.fontSize['2xs']}; } `; diff --git a/src/pages/groupDetail/GroupDetail.tsx b/src/pages/groupDetail/GroupDetail.tsx index e7d6d5eb..596974b7 100644 --- a/src/pages/groupDetail/GroupDetail.tsx +++ b/src/pages/groupDetail/GroupDetail.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Wrapper, TopBackground, @@ -27,51 +28,127 @@ import { } from './GroupDetail.styled'; import leftArrow from '../../assets/common/leftArrow.svg'; import moreIcon from '../../assets/common/more.svg'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { IconButton } from '@/components/common/IconButton'; -import { mockGroupDetail } from '../../mocks/groupDetail.mock'; import lockIcon from '../../assets/group/lock.svg'; import calendarIcon from '../../assets/group/calendar.svg'; import peopleIcon from '../../assets/common/darkPeople.svg'; import rightChevron from '../../assets/common/right-Chevron.svg'; import { GroupCard } from '@/components/group/GroupCard'; +import { + getRoomDetail, + type RoomDetailResponse, + type RecommendRoom, +} from '@/api/rooms/getRoomDetail'; +import type { Group } from '@/components/group/MyGroupBox'; const GroupDetail = () => { - const { - title, - isPrivate, - introduction, - activityPeriod, - members, - ddayText, - genre, - book, - recommendations, - } = mockGroupDetail; - + const { roomId } = useParams<{ roomId: string }>(); const navigate = useNavigate(); + const [roomData, setRoomData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const handleBackButton = () => { navigate(-1); }; const handleMoreButton = () => {}; + const convertRecommendRoomToGroup = (room: RecommendRoom): Group => { + return { + id: room.roomId.toString(), + title: room.roomName, + userName: '', + participants: room.memberCount, + maximumParticipants: room.recruitCount, + coverUrl: room.roomImageUrl, + deadLine: 0, + genre: '', + isOnGoing: true, + }; + }; + + const calculateDday = (recruitEndDate: string): string => { + const today = new Date(); + const endDate = new Date(recruitEndDate); + const diffTime = endDate.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + console.log(endDate); + if (diffDays < 0) return '모집 종료'; + if (diffDays === 0) return '오늘 마감'; + return `${diffDays}일 남음`; + }; + + useEffect(() => { + const fetchRoomDetail = async () => { + if (!roomId) return; + + try { + setIsLoading(true); + setError(null); + + const response = await getRoomDetail(Number(roomId)); + console.log(response); + + if (response.isSuccess) { + setRoomData(response.data); + } else { + setError(response.message); + } + } catch (error) { + console.error('방 상세 정보 조회 실패:', error); + setError('방 정보를 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }; + + fetchRoomDetail(); + }, [roomId]); + + if (isLoading) { + return
로딩 중...
; + } + + if (error || !roomData) { + return
에러: {error}
; + } + + const { + roomName, + isPublic, + roomDescription, + progressStartDate, + progressEndDate, + memberCount, + recruitCount, + recruitEndDate, + category, + bookTitle, + authorName, + bookDescription, + bookImageUrl, + recommendRooms, + } = roomData; + return ( - +
- {title} {isPrivate && 자물쇠 아이콘} + {roomName} {!isPublic && 자물쇠 아이콘}
소개글

- {introduction} + {roomDescription}
@@ -79,7 +156,7 @@ const GroupDetail = () => { 모임 활동기간 - {activityPeriod.start} ~ {activityPeriod.end} + {progressStartDate} ~ {progressEndDate} @@ -87,33 +164,33 @@ const GroupDetail = () => { 참여 중인 독서메이트 - {members.current} - / {members.max}명 + {memberCount} + / {recruitCount}명 - 모집 {ddayText} + 모집 {calculateDday(recruitEndDate)} - 장르 {genre} + 장르 {category}
-

{book.title}

+

{bookTitle}

- + -
{book.author}
+
{authorName}
도서 소개
-

{book.description}

+

{bookDescription}

@@ -121,10 +198,10 @@ const GroupDetail = () => { 이런 모임방은 어때요? - {recommendations.map(group => ( + {recommendRooms.map(room => ( { } /> } /> } /> - } /> + } /> } /> } /> } />