From 041dfeaba4e5dc35fac9f4b26c78f25acc2eb41f Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:27:20 +0900 Subject: [PATCH 01/11] feat: getMyRooms API --- src/api/rooms/getMyRooms.ts | 44 +++++++ src/components/group/MyGroupModal.tsx | 179 +++++++++++++------------- 2 files changed, 136 insertions(+), 87 deletions(-) create mode 100644 src/api/rooms/getMyRooms.ts 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/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index dc036d90..8f0b796c 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -1,96 +1,65 @@ -import { useState } from 'react'; +import { useState, useEffect, useCallback } 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'; 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 getRoomType = useCallback((): RoomType => { + if (selected === '진행중') return 'playing'; + if (selected === '모집중') return 'recruiting'; + return 'playingAndRecruiting'; + }, [selected]); + + const fetchRooms = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + const roomType = getRoomType(); + const response = await getMyRooms(roomType, null); + console.log(response); + if (response.isSuccess) { + setRooms(response.data.roomList); + } else { + setError(response.message); + } + } catch (error) { + console.error('방 목록 조회 실패:', error); + setError('방 목록을 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }, [getRoomType]); + + const convertRoomToGroup = (room: Room): Group => { + return { + id: room.roomId.toString(), + title: room.roomName, + participants: room.memberCount, + maximumParticipants: room.recruitCount, + coverUrl: room.bookImageUrl, + deadLine: 0, + isOnGoing: room.type === 'playing' || room.type === 'playingAndRecruiting', + }; + }; - const filtered = selected - ? dummyMyGroups.filter(g => (selected === '진행중' ? g.isOnGoing : !g.isOnGoing)) - : dummyMyGroups; + useEffect(() => { + fetchRooms(); + }, [fetchRooms]); + + const convertedGroups = rooms.map(convertRoomToGroup); return ( @@ -113,14 +82,23 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { - {filtered.map(group => ( - - ))} + {isLoading ? ( + 로딩 중... + ) : error ? ( + {error} + ) : convertedGroups.length > 0 ? ( + convertedGroups.map(group => ( + + )) + ) : ( + + {selected === '진행중' + ? '진행중인 모임방이 없습니다.' + : selected === '모집중' + ? '모집중인 모임방이 없습니다.' + : '참여한 모임방이 없습니다.'} + + )} @@ -158,3 +136,30 @@ 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: var(--font-size-regular); +`; + +const ErrorMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: #ff6b6b; + font-size: var(--font-size-regular); +`; + +const EmptyMessage = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: #999; + font-size: var(--font-size-regular); +`; From 33fab28f2d866fc965114c0761989cfe6f52a9bd Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:10:51 +0900 Subject: [PATCH 02/11] =?UTF-8?q?design:=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=EB=A7=81=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/group/MyGroupModal.tsx | 120 ++++++++++++++++---------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/src/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index 8f0b796c..668e06c5 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; import styled from '@emotion/styled'; import TitleHeader from '../common/TitleHeader'; import leftArrow from '../../assets/common/leftArrow.svg'; @@ -6,6 +6,7 @@ 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; @@ -17,47 +18,51 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const getRoomType = useCallback((): RoomType => { - if (selected === '진행중') return 'playing'; - if (selected === '모집중') return 'recruiting'; - return 'playingAndRecruiting'; - }, [selected]); - - const fetchRooms = useCallback(async () => { - try { - setIsLoading(true); - setError(null); - const roomType = getRoomType(); - const response = await getMyRooms(roomType, null); - console.log(response); - if (response.isSuccess) { - setRooms(response.data.roomList); - } else { - setError(response.message); - } - } catch (error) { - console.error('방 목록 조회 실패:', error); - setError('방 목록을 불러오는데 실패했습니다.'); - } finally { - setIsLoading(false); - } - }, [getRoomType]); - 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); + console.log(response); + + if (response.isSuccess) { + setRooms(response.data.roomList); + } else { + setError(response.message); + } + } catch (error) { + console.error('방 목록 조회 실패:', error); + setError('방 목록을 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }; + fetchRooms(); - }, [fetchRooms]); + }, [selected]); const convertedGroups = rooms.map(convertRoomToGroup); return ( @@ -91,13 +96,22 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { )) ) : ( - - {selected === '진행중' - ? '진행중인 모임방이 없습니다.' - : selected === '모집중' - ? '모집중인 모임방이 없습니다.' - : '참여한 모임방이 없습니다.'} - + + + {selected === '진행중' + ? '진행중인 모임방이 없어요' + : selected === '모집중' + ? '모집중인 모임방이 없어요' + : '참여중인 모임방이 없어요'} + + + {selected === '진행중' + ? '진행중인 모임방에 참여해보세요!' + : selected === '모집중' + ? '모집중인 모임방에 참여해보세요!' + : '첫 번째 모임방에 참여해보세요!'} + + )} @@ -113,13 +127,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; `; @@ -143,7 +156,7 @@ const LoadingMessage = styled.div` align-items: center; padding: 40px 20px; color: #fff; - font-size: var(--font-size-regular); + font-size: ${typography.fontSize.base}; `; const ErrorMessage = styled.div` @@ -152,14 +165,31 @@ const ErrorMessage = styled.div` align-items: center; padding: 40px 20px; color: #ff6b6b; - font-size: var(--font-size-regular); + font-size: ${typography.fontSize.base}; `; -const EmptyMessage = styled.div` +const EmptyState = styled.div` + flex: 1; + min-height: 78vh; display: flex; + flex-direction: column; justify-content: center; align-items: center; padding: 40px 20px; - color: #999; - font-size: var(--font-size-regular); + 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]}; `; From e5ca390333110dc84b25954356a326f12f428df2 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:23:33 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20getMyRooms=20CompletedGroupModal?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/group/CompletedGroupModal.tsx | 185 +++++++++++-------- src/components/group/MyGroupModal.tsx | 1 - 2 files changed, 104 insertions(+), 82 deletions(-) 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/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index 668e06c5..3731fd7d 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -46,7 +46,6 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { : 'playingAndRecruiting'; const response = await getMyRooms(roomType, null); - console.log(response); if (response.isSuccess) { setRooms(response.data.roomList); From b136ba43e5729391a2ab1a68c570c9933eb0f02c Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:37:12 +0900 Subject: [PATCH 04/11] feat: getRoomDetail --- src/api/rooms/getRoomDetail.ts | 49 ++++++++++ src/pages/group/Group.tsx | 2 +- src/pages/groupDetail/GroupDetail.tsx | 134 ++++++++++++++++++++------ 3 files changed, 155 insertions(+), 30 deletions(-) create mode 100644 src/api/rooms/getRoomDetail.ts 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/pages/group/Group.tsx b/src/pages/group/Group.tsx index 00adab86..f3fe2e7d 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -129,7 +129,7 @@ const Group = () => { const closeCompletedGroupModal = () => setIsCompletedGroupModalOpen(false); const handleSearchBarClick = () => { - navigate('/groupsearch'); + navigate('/group/search'); }; return ( diff --git a/src/pages/groupDetail/GroupDetail.tsx b/src/pages/groupDetail/GroupDetail.tsx index e7d6d5eb..78fc0a39 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,126 @@ 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)); + + if (diffDays < 0) return '모집 종료'; + if (diffDays === 0) return 'D-DAY'; + return `D-${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 +155,7 @@ const GroupDetail = () => { 모임 활동기간 - {activityPeriod.start} ~ {activityPeriod.end} + {progressStartDate} ~ {progressEndDate} @@ -87,33 +163,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 +197,10 @@ const GroupDetail = () => { 이런 모임방은 어때요? - {recommendations.map(group => ( + {recommendRooms.map(room => ( Date: Thu, 14 Aug 2025 23:26:13 +0900 Subject: [PATCH 05/11] feat: getRoomsByCategory API --- src/api/rooms/getRoomsByCategory.ts | 33 +++++++ src/pages/group/Group.tsx | 137 +++++++++++++--------------- 2 files changed, 95 insertions(+), 75 deletions(-) create mode 100644 src/api/rooms/getRoomsByCategory.ts 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/pages/group/Group.tsx b/src/pages/group/Group.tsx index f3fe2e7d..13b1b13a 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -6,11 +6,12 @@ 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[] = [ { @@ -42,85 +43,70 @@ const dummyMyGroups: GroupType[] = [ }, ]; -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); @@ -131,6 +117,7 @@ const Group = () => { const handleSearchBarClick = () => { navigate('/group/search'); }; + return ( {isMyGroupModalOpen && } From 10babe49eec6177f535af23933f52c8b731ce20d Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:29:53 +0900 Subject: [PATCH 06/11] feat: RecruitingGroupBox EmpryState --- src/components/group/RecruitingGroupBox.tsx | 38 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/components/group/RecruitingGroupBox.tsx b/src/components/group/RecruitingGroupBox.tsx index 2b8c134b..a5ca9df5 100644 --- a/src/components/group/RecruitingGroupBox.tsx +++ b/src/components/group/RecruitingGroupBox.tsx @@ -2,6 +2,7 @@ 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'; interface Props { groups: Group[]; @@ -26,9 +27,14 @@ export function RecruitingGroupBox({ groups, title }: Props) { ))} - {filtered.map(group => ( - - ))} + {filtered.length > 0 ? ( + filtered.map(group => ) + ) : ( + + 모임방이 아직 없어요. + 해당 장르의 모임방이 생기면 보여줄게요! + + )} ); @@ -95,3 +101,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; +`; From a79d94c2d511a899c249fe4af915b0b607ce6e6f Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:44:05 +0900 Subject: [PATCH 07/11] feat: getJoinedRooms API --- src/api/rooms/getJoinedRooms.ts | 34 ++++++++ src/components/group/MyGroupBox.tsx | 127 ++++++++++++++++++++++++---- src/pages/group/Group.tsx | 32 +------ 3 files changed, 144 insertions(+), 49 deletions(-) create mode 100644 src/api/rooms/getJoinedRooms.ts 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/components/group/MyGroupBox.tsx b/src/components/group/MyGroupBox.tsx index 2a74a48d..33798ad4 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 { useState, useEffect } from 'react'; +import { getJoinedRooms, type JoinedRoomItem } from '@/api/rooms/getJoinedRooms'; export interface Group { id: number | string; @@ -16,12 +18,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,22 +68,38 @@ 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) => ( + + ))} + + + ) : ( + + 가입한 모임방이 없어요 + + )} ); } @@ -109,3 +161,42 @@ const Dot = styled.div<{ active: boolean }>` background: ${({ active }) => (active ? 'var(--color-white)' : `var(--color-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: var(--color-grey-300); + font-size: var(--font-size-medium02); + margin: 0; +`; + +const ErrorContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; +`; + +const ErrorText = styled.p` + color: var(--color-red); + font-size: var(--font-size-medium02); + margin: 0; +`; + +const EmptyContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; +`; + +const EmptyText = styled.p` + color: var(--color-grey-300); + font-size: var(--font-size-medium02); + margin: 0; +`; diff --git a/src/pages/group/Group.tsx b/src/pages/group/Group.tsx index 13b1b13a..2d401992 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -13,36 +13,6 @@ 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 convertRoomItemToGroup = ( room: RoomItem, category: string, @@ -124,7 +94,7 @@ const Group = () => { {isCompletedGroupModalOpen && } - + From 0c069c1856cbf7fdabfad0a93aac65d03580105d Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:49:44 +0900 Subject: [PATCH 08/11] =?UTF-8?q?design:=20global=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/group/MyGroupBox.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/group/MyGroupBox.tsx b/src/components/group/MyGroupBox.tsx index 33798ad4..a040cba0 100644 --- a/src/components/group/MyGroupBox.tsx +++ b/src/components/group/MyGroupBox.tsx @@ -4,6 +4,7 @@ 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; @@ -105,7 +106,7 @@ export function MyGroupBox({ onMyGroupsClick }: MyGroupProps) { } const Container = styled.div` - background-color: var(--color-main-black); + background-color: ${colors.black.main}; position: relative; width: 100%; overflow-x: hidden; @@ -119,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; `; @@ -158,7 +159,7 @@ 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; `; @@ -170,8 +171,8 @@ const LoadingContainer = styled.div` `; const LoadingText = styled.p` - color: var(--color-grey-300); - font-size: var(--font-size-medium02); + color: ${colors.grey[300]}; + font-size: ${typography.fontSize.base}; margin: 0; `; @@ -183,8 +184,8 @@ const ErrorContainer = styled.div` `; const ErrorText = styled.p` - color: var(--color-red); - font-size: var(--font-size-medium02); + color: ${colors.red}; + font-size: ${typography.fontSize.base}; margin: 0; `; @@ -196,7 +197,7 @@ const EmptyContainer = styled.div` `; const EmptyText = styled.p` - color: var(--color-grey-300); - font-size: var(--font-size-medium02); + color: ${colors.grey[300]}; + font-size: ${typography.fontSize.base}; margin: 0; `; From a3770c2d2cabb5024f13d094e84c67eb6fc65099 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:08:16 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20groupCard=20click=20event=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/group/GroupCard.tsx | 5 ++-- src/components/group/RecruitingGroupBox.tsx | 27 ++++++++++++++++++++- src/pages/group/CreateGroup.tsx | 3 +-- src/pages/index.tsx | 2 +- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/group/GroupCard.tsx b/src/components/group/GroupCard.tsx index 1396b271..8c5192d4 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} diff --git a/src/components/group/RecruitingGroupBox.tsx b/src/components/group/RecruitingGroupBox.tsx index a5ca9df5..cfa97b9a 100644 --- a/src/components/group/RecruitingGroupBox.tsx +++ b/src/components/group/RecruitingGroupBox.tsx @@ -3,6 +3,8 @@ 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[]; @@ -13,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} @@ -28,7 +46,14 @@ export function RecruitingGroupBox({ groups, title }: Props) { {filtered.length > 0 ? ( - filtered.map(group => ) + filtered.map(group => ( + handleGroupCardClick(group.id)} + /> + )) ) : ( 모임방이 아직 없어요. 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/index.tsx b/src/pages/index.tsx index 62347f89..5d8ab67f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -52,7 +52,7 @@ const Router = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> From 26edc6e678f15443d3b5bc0b5373d19f5095fbd8 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:24:05 +0900 Subject: [PATCH 10/11] design: min size add --- src/components/group/GroupCard.tsx | 4 +++- src/pages/groupDetail/GroupDetail.tsx | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/group/GroupCard.tsx b/src/components/group/GroupCard.tsx index 8c5192d4..c238c7bf 100644 --- a/src/components/group/GroupCard.tsx +++ b/src/components/group/GroupCard.tsx @@ -53,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/pages/groupDetail/GroupDetail.tsx b/src/pages/groupDetail/GroupDetail.tsx index 78fc0a39..596974b7 100644 --- a/src/pages/groupDetail/GroupDetail.tsx +++ b/src/pages/groupDetail/GroupDetail.tsx @@ -76,9 +76,10 @@ const GroupDetail = () => { 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 'D-DAY'; - return `D-${diffDays}`; + if (diffDays === 0) return '오늘 마감'; + return `${diffDays}일 남음`; }; useEffect(() => { From c2bd7ba197a2d9f81e29ac877024989c68487bec Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:27:38 +0900 Subject: [PATCH 11/11] =?UTF-8?q?design:=20BookDetails=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/groupDetail/GroupDetail.styled.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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']}; } `;