diff --git a/src/api/meeting.js b/src/api/meeting.js index fdb00cdbfe26f8adb9709ecb36827c1294e841eb..ff6fb6d8f039b60cd54a28036a8557dd24b9d9e8 100644 --- a/src/api/meeting.js +++ b/src/api/meeting.js @@ -1,45 +1,52 @@ -// src/api/meeting.js - // 기본 API URL const BASE_URL = process.env.REACT_APP_BASE_URL; -// 번개 모임 조회 -export const fetchMyMeetings = async (page = 0, size = 20) => { - try { - const response = await fetch( - `${BASE_URL}/api/meeting?page=${page}&size=${size}`,// 내가 참여한 번개 모임 목록 조회할거면 my - { - method: "GET", - credentials: "include", // 세션 기반 인증을 위해 필요 - headers: { - "Content-Type": "application/json", - }, - } - ); - console.log("번개 모임 조회", response); - - if (!response.ok) { - throw new Error(`Error: ${response.status}`); +/** + * 모든 미팅 불러오기 + * @param {number} page - 페이지 번호 (기본값: 0) + * @param {number} size - 페이지 크기 (기본값: 20) + * @returns {Promise<Object>} - 미팅 데이터 + */ +export const getAllMeetings = async (page = 0, size = 20) => { + const response = await fetch( + `${BASE_URL}/api/meeting?page=${page}&size=${size}`, + { + method: "GET", } + ); + + if (!response.ok) { + throw new Error("Failed to fetch meetings"); + } - const result = await response.json(); + return (await response.json()).data; +}; - if (!result.success) { - throw new Error("Failed to fetch meetings."); - } +/** + * 모임 상세 조회 + * @param {number} meetingId - 조회할 모임 ID + * @returns {Promise<Object>} - 모임 상세 데이터 + */ +export const getMeetingDetails = async (meetingId) => { + const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}`, { + method: "GET", + }); - return result.data; // 서버에서 제공된 데이터 반환 - } catch (error) { - console.error("Error fetching my meetings:", error); - throw error; + if (!response.ok) { + throw new Error("Failed to fetch meeting details"); } + + return (await response.json()).data; }; -// 번개 모임 생성 +/** + * 미팅 생성 + * @param {Object} meetingData - 미팅 생성에 필요한 데이터 + * @returns {Promise<Object>} - 생성된 미팅 데이터 + */ export const createMeeting = async (meetingData) => { const response = await fetch(`${BASE_URL}/api/meeting`, { method: "POST", - credentials: "include", // 세션 기반 인증 headers: { "Content-Type": "application/json", }, @@ -47,25 +54,92 @@ export const createMeeting = async (meetingData) => { }); if (!response.ok) { - throw new Error(`Failed to create meeting: ${response.status}`); + throw new Error("Failed to create meeting"); } return await response.json(); }; -// 번개 모임 참가 +/** + * 미팅 참가하기 + * @param {number} meetingId - 참가할 미팅 ID + * @returns {Promise<Object>} - 참가 결과 메시지 + */ export const joinMeeting = async (meetingId) => { const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}/join`, { method: "POST", - credentials: "include", // 세션 기반 인증 - headers: { - "Content-Type": "application/json", - }, }); if (!response.ok) { - throw new Error(`Failed to join meeting: ${response.status}`); + throw new Error("Failed to join meeting"); } return await response.json(); -}; \ No newline at end of file +}; + +/** + * 내가 참가한 미팅 불러오기 + * @param {number} page - 페이지 번호 (기본값: 0) + * @param {number} size - 페이지 크기 (기본값: 20) + * @returns {Promise<Object>} - 참가한 미팅 데이터 + */ +export const getMyMeetings = async (page = 0, size = 20) => { + const response = await fetch( + `${BASE_URL}/api/meeting/my?page=${page}&size=${size}`, + { + method: "GET", + } + ); + + if (!response.ok) { + throw new Error("Failed to fetch my meetings"); + } + + return (await response.json()).data; +}; + +/** + * 모임 탈퇴 + * @param {number} meetingId - 탈퇴할 모임 ID + * @returns {Promise<Object>} - 탈퇴 결과 메시지 + */ +export const leaveMeeting = async (meetingId) => { + const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}/leave`, { + method: "POST", + }); + + if (!response.ok) { + throw new Error("Failed to leave meeting"); + } + + return await response.json(); +}; + +/** + * 모임 마감 + * @param {number} meetingId - 마감할 모임 ID + * @returns {Promise<Object>} - 마감 결과 메시지 및 업데이트된 모임 데이터 + */ +export const closeMeeting = async (meetingId) => { + const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}/close`, { + method: "PUT", + }); + + if (!response.ok) { + throw new Error("Failed to close meeting"); + } + + return await response.json(); +}; + +export const deleteMeeting = async (meetingId) => { + const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Failed to delete meeting"); + } + + return await response.json(); +}; diff --git a/src/components/Card.jsx b/src/components/Card.jsx index bad2f8774f0694edd95aa2e8b3d54728b7b1f9c3..e35bb539a76420242ed026287cd77562e4b78cc0 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -18,7 +18,15 @@ const cardVariants = cva("w-full rounded-xl shadow-lg p-4 overflow-hidden", { }, }); -export default function Card({ meeting, theme = "black", onJoin, onClick }) { +export default function Card({ + meeting, + theme = "black", + onClick, + onJoin, + onDelete, + onClose, + onLeave, +}) { const { title, timeIdxStart, @@ -27,13 +35,19 @@ export default function Card({ meeting, theme = "black", onJoin, onClick }) { creatorName, time_idx_deadline, type, + isParticipant, + isScheduleConflict, } = meeting; const variantClass = cardVariants({ - theme: type === "CLOSE" ? "gray" : theme, + theme: + !isParticipant && !isScheduleConflict && type === "OPEN" ? "mix" : theme, }); // 시간 변환 + // const userName = localStorage.getItem("nickname"); + const userName = "윤석찬"; + const startTime = convertIndexToTime(timeIdxStart); const endTime = convertIndexToTime(timeIdxEnd); const deadlineTime = convertIndexToTime(time_idx_deadline); @@ -53,15 +67,62 @@ export default function Card({ meeting, theme = "black", onJoin, onClick }) { <Label size="sm" theme="black"> 주최자: {creatorName} </Label> - <div className="flex justify-between mt-4"> - <span className={`text-sm ${type === "OPEN" ? "text-green-500" : "text-red-500"}`}> - {type === "OPEN" ? "참여 가능" : "참여 마감"} - </span> - {type === "OPEN" && ( - <Button size="sm" theme="mix" onClick={onJoin}> - 참가하기 - </Button> - )} + <div className="flex items-end justify-between mt-4"> + <div className="flex gap-2"> + <Label + className={`${type === "OPEN" ? "text-green-500" : "text-red-500"}`} + size="sm" + theme="graysolid" + > + {type} + </Label> + {isParticipant && ( + <Label size="sm" theme="graysolid"> + 참여중 + </Label> + )} + {isScheduleConflict && ( + <Label className="text-warning" size="sm" theme="graysolid"> + 시간표 충돌 + </Label> + )} + </div> + + {type === "OPEN" ? ( + <> + {isParticipant ? ( + <> + {creatorName === userName ? ( + <div className="flex gap-2"> + <Button size="sm" theme="black" onClick={onClose}> + 마감 + </Button> + <Button size="sm" theme="pink" onClick={onDelete}> + 삭제 + </Button> + </div> + ) : ( + <> + <Button size="sm" theme="white" onClick={onLeave}> + 나가기 + </Button> + </> + )} + </> + ) : ( + <Button + size="sm" + theme="white" + state={ + isParticipant || isScheduleConflict ? "disable" : "default" + } + onClick={onJoin} + > + 참가하기 + </Button> + )} + </> + ) : null} </div> </div> ); diff --git a/src/components/CreateMeetingModal.jsx b/src/components/CreateMeetingModal.jsx index 82be28b1b36be5c889daf32a17a91211efe94c61..d24db87127c7661818e0090c30224671e042cf1d 100644 --- a/src/components/CreateMeetingModal.jsx +++ b/src/components/CreateMeetingModal.jsx @@ -1,106 +1,271 @@ import React, { useState } from "react"; -import { createMeeting } from "../api/meeting"; +import Modal from "./Modal"; import Button from "./Button"; +import { createMeeting } from "../api/meeting"; +import { convertTimeToIndex } from "../utils/time"; +import { days } from "../constants/schedule"; -const CreateMeetingModal = ({ onClose }) => { - const [formData, setFormData] = useState({ - title: "", - description: "", - location: "", - time_idx_start: 0, - time_idx_end: 0, - max_num: 5, - type: "OPEN", // 기본값 - }); +const CreateMeetingModal = ({ isOpen, onClose }) => { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [location, setLocation] = useState(""); + const [dayStart, setDayStart] = useState("월"); + const [hourStart, setHourStart] = useState("0"); + const [minuteStart, setMinuteStart] = useState("0"); + const [dayEnd, setDayEnd] = useState("월"); + const [hourEnd, setHourEnd] = useState("0"); + const [minuteEnd, setMinuteEnd] = useState("0"); + const [dayDeadline, setDayDeadline] = useState("월"); + const [hourDeadline, setHourDeadline] = useState("0"); + const [minuteDeadline, setMinuteDeadline] = useState("0"); + const [maxNum, setMaxNum] = useState("1"); - const handleChange = (e) => { - const { name, value } = e.target; - setFormData({ ...formData, [name]: value }); - }; + const minutes = ["0", "15", "30", "45"]; + const hours = Array.from({ length: 24 }, (_, i) => i.toString()); + const maxParticipants = Array.from({ length: 20 }, (_, i) => + (i + 1).toString() + ); const handleSubmit = async () => { try { - await createMeeting(formData); // API 호출 - alert("번개 모임이 성공적으로 생성되었습니다!"); - onClose(); // 모달 닫기 - window.location.reload(); // 새로고침하여 번개 목록 업데이트 + const time_idx_start = convertTimeToIndex( + dayStart, + parseInt(hourStart), + parseInt(minuteStart) + ); + const time_idx_end = convertTimeToIndex( + dayEnd, + parseInt(hourEnd), + parseInt(minuteEnd) + ); + const time_idx_deadline = convertTimeToIndex( + dayDeadline, + parseInt(hourDeadline), + parseInt(minuteDeadline) + ); + + const meetingData = { + title, + description, + location, + time_idx_start, + time_idx_end, + time_idx_deadline, + type: "OPEN", // 기본값 + max_num: parseInt(maxNum), + }; + + await createMeeting(meetingData); + alert("모임이 성공적으로 생성되었습니다!"); + onClose(); } catch (error) { - console.error("Error creating meeting:", error); - alert("번개 모임 생성에 실패했습니다."); + console.error("Failed to create meeting:", error); + alert("모임 생성에 실패했습니다."); } }; return ( - <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> - <div className="bg-white rounded-lg p-6 w-11/12 max-w-md"> - <h2 className="text-lg font-bold mb-4">번개 모임 만들기</h2> - <div className="space-y-4"> + <Modal isOpen={isOpen} onClose={onClose}> + <h2 className="mb-4 text-2xl font-bold">번개 모임 만들기</h2> + <hr /> + <div className="mt-4 space-y-4"> + <div> + <label className="block mb-2 font-semibold"> + 모임 이름 + <span className="relative ml-1 bottom-1 label-1 text-warning"> + * + </span> + </label> <input - name="title" - value={formData.title} - onChange={handleChange} - placeholder="제목" + type="text" className="w-full p-2 border rounded" + placeholder="모임 이름을 입력하세요" + value={title} + onChange={(e) => setTitle(e.target.value)} + required /> + </div> + <div> + <label className="block mb-2 font-semibold"> + 모임 설명 + <span className="relative ml-1 bottom-1 label-1 text-warning"> + * + </span> + </label> <textarea - name="description" - value={formData.description} - onChange={handleChange} - placeholder="설명" className="w-full p-2 border rounded" - /> - <input - name="location" - value={formData.location} - onChange={handleChange} - placeholder="장소" - className="w-full p-2 border rounded" - /> - <input - name="time_idx_start" - type="number" - value={formData.time_idx_start} - onChange={handleChange} - placeholder="시작 시간 인덱스" - className="w-full p-2 border rounded" - /> - <input - name="time_idx_end" - type="number" - value={formData.time_idx_end} - onChange={handleChange} - placeholder="종료 시간 인덱스" - className="w-full p-2 border rounded" - /> + placeholder="모임 설명을 입력하세요" + value={description} + onChange={(e) => setDescription(e.target.value)} + ></textarea> + </div> + <div> + <label className="block mb-2 font-semibold"> + 장소 + <span className="relative ml-1 bottom-1 label-1 text-warning"> + * + </span> + </label> <input - name="max_num" - type="number" - value={formData.max_num} - onChange={handleChange} - placeholder="최대 인원" + type="text" className="w-full p-2 border rounded" + placeholder="장소를 입력하세요" + value={location} + onChange={(e) => setLocation(e.target.value)} + required /> + </div> + <div> + <label className="block mb-2 font-semibold"> + 시작 시간 + <span className="relative ml-1 bottom-1 label-1 text-warning"> + * + </span> + </label> + <div className="flex items-center gap-2"> + <select + value={dayStart} + onChange={(e) => setDayStart(e.target.value)} + className="p-2 border rounded" + > + {days.map((day) => ( + <option key={day} value={day}> + {day} + </option> + ))} + </select> + <select + value={hourStart} + onChange={(e) => setHourStart(e.target.value)} + className="p-2 border rounded" + > + {hours.map((hour) => ( + <option key={hour} value={hour}> + {hour}시 + </option> + ))} + </select> + <select + value={minuteStart} + onChange={(e) => setMinuteStart(e.target.value)} + className="p-2 border rounded" + > + {minutes.map((minute) => ( + <option key={minute} value={minute}> + {minute}분 + </option> + ))} + </select> + </div> + </div> + <div> + <label className="block mb-2 font-semibold"> + 종료 시간 + <span className="relative ml-1 bottom-1 label-1 text-warning"> + * + </span> + </label> + <div className="flex items-center gap-2"> + <select + value={dayEnd} + onChange={(e) => setDayEnd(e.target.value)} + className="p-2 border rounded" + > + {days.map((day) => ( + <option key={day} value={day}> + {day} + </option> + ))} + </select> + <select + value={hourEnd} + onChange={(e) => setHourEnd(e.target.value)} + className="p-2 border rounded" + > + {hours.map((hour) => ( + <option key={hour} value={hour}> + {hour}시 + </option> + ))} + </select> + <select + value={minuteEnd} + onChange={(e) => setMinuteEnd(e.target.value)} + className="p-2 border rounded" + > + {minutes.map((minute) => ( + <option key={minute} value={minute}> + {minute}분 + </option> + ))} + </select> + </div> + </div> + <div> + <label className="block mb-2 font-semibold"> + 참가 마감 시간 (선택) + </label> + <div className="flex items-center gap-2"> + <select + value={dayDeadline} + onChange={(e) => setDayDeadline(e.target.value)} + className="p-2 border rounded" + > + {days.map((day) => ( + <option key={day} value={day}> + {day} + </option> + ))} + </select> + <select + value={hourDeadline} + onChange={(e) => setHourDeadline(e.target.value)} + className="p-2 border rounded" + > + {hours.map((hour) => ( + <option key={hour} value={hour}> + {hour}시 + </option> + ))} + </select> + <select + value={minuteDeadline} + onChange={(e) => setMinuteDeadline(e.target.value)} + className="p-2 border rounded" + > + {minutes.map((minute) => ( + <option key={minute} value={minute}> + {minute}분 + </option> + ))} + </select> + </div> + </div> + <div> + <label className="block mb-2 font-semibold">최대 참가 인원</label> <select - name="type" - value={formData.type} - onChange={handleChange} + value={maxNum} + onChange={(e) => setMaxNum(e.target.value)} className="w-full p-2 border rounded" > - <option value="OPEN">OPEN</option> - <option value="CLOSE">CLOSE</option> + {maxParticipants.map((num) => ( + <option key={num} value={num}> + {num}명 + </option> + ))} </select> - <div className="flex justify-between"> - <Button size="sm" theme="pink" onClick={handleSubmit}> - 생성하기 - </Button> - <Button size="sm" theme="gray" onClick={onClose}> - 닫기 - </Button> - </div> </div> </div> - </div> + <div className="flex justify-end gap-4 mt-6"> + <Button size="md" theme="white" onClick={onClose}> + 취소 + </Button> + <Button size="md" theme="mix" onClick={handleSubmit}> + 생성 + </Button> + </div> + </Modal> ); }; -export default CreateMeetingModal; \ No newline at end of file +export default CreateMeetingModal; diff --git a/src/components/MeetingDetailModal.jsx b/src/components/MeetingDetailModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..dd43cb768971efd848ada0fbaadfda915d563518 --- /dev/null +++ b/src/components/MeetingDetailModal.jsx @@ -0,0 +1,60 @@ +import React from "react"; +import Modal from "./Modal"; +import { convertIndexToTime } from "../utils/time"; +import Label from "./Label"; + +const MeetingDetailModal = ({ isOpen, onClose, meeting }) => { + if (!meeting) return null; + + const { + title, + description, + location, + timeIdxStart, + timeIdxEnd, + time_idx_deadline, + type, + creatorName, + participants, + } = meeting; + + const startTime = convertIndexToTime(timeIdxStart); + const endTime = convertIndexToTime(timeIdxEnd); + const deadlineTime = convertIndexToTime(time_idx_deadline); + + return ( + <Modal isOpen={isOpen} onClose={onClose}> + <h1 className="my-2 heading-1">번개모임 상세정보</h1> + <hr /> + <div className="flex items-center gap-3 mt-4"> + <h2 className="heading-3">{title}</h2> + <Label + className={`${type === "OPEN" ? "text-green-500" : "text-red-500"}`} + size="sm" + theme="graysolid" + > + {type} + </Label> + </div> + <p className="mb-4 text-gray-700 body-1">{description}</p> + <div className="space-y-2"> + <p>장소: {location}</p> + <p> + 시간: {startTime} ~ {endTime} + </p> + <p>참가 마감 시간: {deadlineTime}</p> + <p>주최자: {creatorName}</p> + <p>참가자 목록:</p> + <ul className="pl-6 list-disc"> + {participants.map((participant) => ( + <li key={participant.userId}> + {participant.name} ({participant.email}) + </li> + ))} + </ul> + </div> + </Modal> + ); +}; + +export default MeetingDetailModal; diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index b1b78afa3e6c047c7bfb9c88466b367cc750e3ff..115142d87e211849195fd7e81d5b3cd40fad0c0f 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -4,10 +4,10 @@ const Modal = ({ isOpen, onClose, children }) => { if (!isOpen) return null; return ( - <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> - <div className="bg-white w-full max-w-lg p-6 rounded-lg relative"> + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"> + <div className="relative w-full max-w-lg p-6 overflow-scroll bg-white rounded-lg h-3/4"> <button - className="absolute font-bold top-2 right-3 text-gray-600 hover:text-black" + className="absolute font-bold text-gray-600 top-2 right-3 hover:text-black" onClick={onClose} > × diff --git a/src/pages/MeetingPage.jsx b/src/pages/MeetingPage.jsx index 0a670484f07cd22b3cdfe5270dac183ab268d4ac..82a936eaa76f567b0b7b96d7f6e1d8825a11b1ee 100644 --- a/src/pages/MeetingPage.jsx +++ b/src/pages/MeetingPage.jsx @@ -3,27 +3,60 @@ import useAuthStore from "../store/authStore"; import { useNavigate } from "react-router-dom"; import Card from "../components/Card"; import Button from "../components/Button"; -import { fetchMyMeetings, joinMeeting } from "../api/meeting"; import CreateMeetingModal from "../components/CreateMeetingModal"; +import MeetingDetailModal from "../components/MeetingDetailModal"; +import { + getAllMeetings, + getMyMeetings, + getMeetingDetails, + joinMeeting, + deleteMeeting, + closeMeeting, + leaveMeeting, +} from "../api/meeting"; const MeetingPage = () => { const navigate = useNavigate(); const { fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기 + const [activeTab, setActiveTab] = useState("all"); // 현재 활성화된 탭 (전체/내 번개) const [meetings, setMeetings] = useState([]); + const [myMeetings, setMyMeetings] = useState([]); const [meetingPage, setMeetingPage] = useState(0); + const [myMeetingPage, setMyMeetingPage] = useState(0); const [meetingHasNext, setMeetingHasNext] = useState(true); + const [myMeetingHasNext, setMyMeetingHasNext] = useState(true); const [meetingIsLoading, setMeetingIsLoading] = useState(false); + const [myMeetingIsLoading, setMyMeetingIsLoading] = useState(false); const [hasError, setHasError] = useState(false); - const [showModal, setShowModal] = useState(false); // 모달 상태 관리 + const [showCreateModal, setShowCreateModal] = useState(false); // 모임 생성 모달 + const [showDetailModal, setShowDetailModal] = useState(false); // 모임 상세 모달 + const [selectedMeeting, setSelectedMeeting] = useState(); // 선택된 모임 useEffect(() => { - const fetchMeetings = async () => { + const fetchUserSession = async () => { + try { + const userInfo = localStorage.getItem("user"); + if (!userInfo) { + alert("로그인이 필요한 페이지입니다."); + navigate("/login"); + } + await fetchSession(); // 세션 정보 가져오기 + } catch (error) { + console.error("Failed to fetch session:", error); + } + }; + + fetchUserSession(); + }, [fetchSession, navigate]); // 페이지 마운트 시 실행 + + useEffect(() => { + const fetchAllMeetings = async () => { if (!meetingHasNext || meetingIsLoading || hasError) return; try { setMeetingIsLoading(true); - const data = await fetchMyMeetings(meetingPage, 20); // API 호출 - setMeetings((prev) => [...prev, ...data.content]); // 기존 데이터에 추가 + const data = await getAllMeetings(meetingPage, 20); + setMeetings((prev) => [...prev, ...data.content]); setMeetingHasNext(data.hasNext); setMeetingPage((prev) => prev + 1); } catch (error) { @@ -34,91 +67,245 @@ const MeetingPage = () => { } }; - fetchMeetings(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [meetingPage, meetingHasNext, meetingIsLoading]); + if (activeTab === "all") { + fetchAllMeetings(); + } + }, [activeTab, meetingPage, meetingHasNext, meetingIsLoading, hasError]); useEffect(() => { - const fetchUserSession = async () => { + const fetchMyMeetings = async () => { + if (!myMeetingHasNext || myMeetingIsLoading || hasError) return; + try { - const userInfo = localStorage.getItem("user"); - if (!userInfo) { - alert("로그인이 필요한 페이지입니다."); - navigate("/login"); - } - await fetchSession(); // 세션 정보 가져오기 + setMyMeetingIsLoading(true); + const data = await getMyMeetings(myMeetingPage, 20); + setMyMeetings((prev) => [...prev, ...data.content]); + setMyMeetingHasNext(data.hasNext); + setMyMeetingPage((prev) => prev + 1); } catch (error) { - console.error("Failed to fetch session:", error); + setHasError(true); + console.error("Failed to fetch my meetings:", error); + } finally { + setMyMeetingIsLoading(false); } }; - fetchUserSession(); - }, [fetchSession, navigate]); // 페이지 마운트 시 실행 + if (activeTab === "my") { + fetchMyMeetings(); + } + }, [ + activeTab, + myMeetingPage, + myMeetingHasNext, + myMeetingIsLoading, + hasError, + ]); const handleCreateMeeting = () => { - setShowModal(true); // 모달 열기 + setShowCreateModal(true); // 모달 열기 }; - const handleJoinMeeting = async (meetingId) => { + const handleJoinButtonClick = async (e, meetingId) => { + e.stopPropagation(); try { await joinMeeting(meetingId); alert("번개 모임에 성공적으로 참가했습니다!"); - // 참가 후 UI를 업데이트할 필요가 있다면 추가 + // 참가 상태 업데이트 + setMeetings((prev) => + prev.map((meeting) => + meeting.id === meetingId + ? { ...meeting, isParticipant: true } + : meeting + ) + ); } catch (error) { alert("번개 모임 참가에 실패했습니다."); console.error("Error joining meeting:", error); } }; + const handleCardClick = async (meetingId) => { + try { + const meetingDetail = await getMeetingDetails(meetingId); + setSelectedMeeting(meetingDetail); + setShowDetailModal(true); + } catch (error) { + alert("번개 모임 상세 정보 불러오기 실패했습니다."); + console.error("Error fetching meeting detail:", error); + } + }; + + const handleDeleteButtonClick = async (e, meetingId) => { + e.stopPropagation(); + try { + await deleteMeeting(meetingId); + alert("번개 모임을 삭제했습니다!"); + setMeetings((prev) => prev.filter((meeting) => meeting.id !== meetingId)); + setMyMeetings((prev) => + prev.filter((meeting) => meeting.id !== meetingId) + ); + } catch (error) { + alert("번개 모임 삭제에 실패했습니다."); + console.error("Error deleting meeting:", error); + } + }; + + const handleLeaveButtonClick = async (e, meetingId) => { + e.stopPropagation(); + try { + await leaveMeeting(meetingId); + alert("번개 모임을 나갔습니다!"); + setMeetings((prev) => + prev.map((meeting) => + meeting.id === meetingId + ? { ...meeting, isParticipant: false } + : meeting + ) + ); + setMyMeetings((prev) => + prev.map((meeting) => + meeting.id === meetingId + ? { ...meeting, isParticipant: false } + : meeting + ) + ); + } catch (error) { + alert("번개 모임 나가기에 실패했습니다."); + console.error("Error leaving meeting:", error); + } + }; + + const handleCloseButtonClick = async (e, meetingId) => { + e.stopPropagation(); + try { + await closeMeeting(meetingId); + alert("번개 모임을 마감했습니다!"); + setMeetings((prev) => + prev.map((meeting) => + meeting.id === meetingId ? { ...meeting, type: "CLOSE" } : meeting + ) + ); + setMyMeetings((prev) => + prev.map((meeting) => + meeting.id === meetingId ? { ...meeting, type: "CLOSE" } : meeting + ) + ); + } catch (error) { + alert("번개 모임 마감에 실패했습니다."); + console.error("Error closing meeting:", error); + } + }; + return ( <div className="min-h-screen bg-gray-50"> {/* 헤더 */} - <header className="flex items-center justify-between px-6 py-4 bg-gradient-purple text-white"> - <h1 className="text-xl font-bold">번개 모임</h1> - <Button size="sm" theme="pink" onClick={handleCreateMeeting}> + <header className="flex items-center justify-between px-6 py-4 text-black border-b-2 rounded-t-xl border-grayscale-300"> + <h1 className="heading-1">번개 모임</h1> + <Button size="lg" theme="mix" onClick={handleCreateMeeting}> 번개 만들기 </Button> </header> + {/* 탭 네비게이션 */} + <div className="flex justify-center py-2 space-x-6 border-b"> + <button + className={`px-4 py-2 text-sm font-medium ${ + activeTab === "all" + ? "text-secondary-500 border-b-2 border-secondary-500" + : "text-gray-600" + }`} + onClick={() => setActiveTab("all")} + > + 전체 번개 모임 + </button> + <button + className={`px-4 py-2 text-sm font-medium ${ + activeTab === "my" + ? "text-tertiary-500 border-b-2 border-tertiary-500" + : "text-gray-600" + }`} + onClick={() => setActiveTab("my")} + > + 나의 번개 모임 + </button> + </div> + {/* 번개 모임 리스트 */} <main className="p-6"> - {meetings.length === 0 && !meetingIsLoading && ( - <p className="text-center text-gray-500">참여 중인 번개 모임이 없습니다.</p> + {activeTab === "all" && ( + <> + {meetings.length === 0 && !meetingIsLoading && ( + <p className="text-center text-gray-500"> + 현재 조회되는 번개 모임이 없습니다. + </p> + )} + <div className="grid grid-cols-1 gap-6 tablet:grid-cols-2 desktop:grid-cols-3"> + {meetings.map((meeting) => ( + <Card + key={meeting.id} + meeting={meeting} + theme="white" + onClick={() => handleCardClick(meeting.id)} + onJoin={(e) => handleJoinButtonClick(e, meeting.id)} + onClose={(e) => handleCloseButtonClick(e, meeting.id)} + onDelete={(e) => handleDeleteButtonClick(e, meeting.id)} + onLeave={(e) => handleLeaveButtonClick(e, meeting.id)} + /> + ))} + </div> + {meetingIsLoading && ( + <p className="text-center text-gray-500">로딩 중...</p> + )} + {!meetingHasNext && meetings.length > 0 && ( + <p className="mt-4 text-sm text-center text-gray-400"> + 더 이상 불러올 번개 모임이 없습니다. + </p> + )} + </> )} - <div className="grid grid-cols-1 gap-6 tablet:grid-cols-2 desktop:grid-cols-3"> - {meetings.map((meeting) => ( - <Card - key={meeting.id} - meeting={meeting} - theme="white" - onClick={() => navigate(`/meetings/${meeting.id}`)} // 상세 페이지로 이동 - onJoin={() => handleJoinMeeting(meeting.id)} // 참가하기 클릭 시 처리 - /> - ))} - </div> - {meetingIsLoading && <p className="text-center text-gray-500">로딩 중...</p>} - {!meetingHasNext && meetings.length > 0 && ( - <p className="mt-4 text-sm text-center text-gray-400"> - 더 이상 불러올 번개 모임이 없습니다. - </p> + + {activeTab === "my" && ( + <> + {myMeetings.length === 0 && !myMeetingIsLoading && ( + <p className="text-center text-gray-500"> + 현재 조회되는 나의 번개 모임이 없습니다. + </p> + )} + <div className="grid grid-cols-1 gap-6 tablet:grid-cols-2 desktop:grid-cols-3"> + {myMeetings.map((meeting) => ( + <Card + key={meeting.id} + meeting={meeting} + theme="white" + onClick={() => handleCardClick(meeting.id)} + onJoin={(e) => handleJoinButtonClick(e, meeting.id)} + /> + ))} + </div> + {myMeetingIsLoading && ( + <p className="text-center text-gray-500">로딩 중...</p> + )} + {!myMeetingHasNext && myMeetings.length > 0 && ( + <p className="mt-4 text-sm text-center text-gray-400"> + 더 이상 불러올 나의 번개 모임이 없습니다. + </p> + )} + </> )} </main> {/* 모달 */} - {showModal && ( - <CreateMeetingModal onClose={() => setShowModal(false)} /> // 모달 닫기 핸들러 - )} + <CreateMeetingModal + isOpen={showCreateModal} + onClose={() => setShowCreateModal(false)} + /> + <MeetingDetailModal + isOpen={showDetailModal} + onClose={() => setShowDetailModal(false)} + meeting={selectedMeeting} + /> </div> ); }; export default MeetingPage; - -// -// black: "bg-black text-white", -// white: "bg-white text-black", -// pink: "bg-gradient-pink text-white", -// purple: "bg-gradient-purple text-white", -// indigo: "bg-gradient-indigo text-white", -// mix: "bg-gradient-mix text-white", -// gray: "bg-gray-300 text-gray-500" \ No newline at end of file diff --git a/src/pages/Mypage.jsx b/src/pages/Mypage.jsx index 107e0eb51db3ab856105021926da19214e27d76c..865abd47f86613d8671527f6377b7e90b4ec6bf3 100644 --- a/src/pages/Mypage.jsx +++ b/src/pages/Mypage.jsx @@ -11,8 +11,6 @@ import { } from "../api/friend"; import Button from "../components/Button"; import LogoIcon from "../components/icons/LogoIcon"; -import { fetchMyMeetings } from "../api/meeting"; -// import Card from "../components/Card"; import ChattingList from "../components/ChattingList"; import { useNavigate } from "react-router-dom"; @@ -26,11 +24,6 @@ const MyPage = () => { const [page, setPage] = useState(0); // 친구 목록 페이지 const [hasNext, setHasNext] = useState(true); // 페이지네이션 상태 const [isLoading, setIsLoading] = useState(false); - - const [meetings, setMeetings] = useState([]); - const [meetingPage, setMeetingPage] = useState(0); - const [meetingHasNext, setMeetingHasNext] = useState(true); - const [meetingIsLoading, setMeetingIsLoading] = useState(false); const [hasError, setHasError] = useState(false); const navigate = useNavigate(); @@ -55,29 +48,6 @@ const MyPage = () => { fetchUserSession(); }, [fetchSession, navigate]); // 페이지 마운트 시 실행 - // 번개 모임 가져오기 - useEffect(() => { - const fetchMeetings = async () => { - if (!meetingHasNext || meetingIsLoading || hasError) return; - - try { - setMeetingIsLoading(true); - const data = await fetchMyMeetings(meetingPage, 20); - setMeetings((prev) => [...prev, ...data.content]); - setMeetingHasNext(data.meetingHasNext); - setMeetingPage((prev) => prev + 1); - } catch (error) { - setHasError(true); - console.error("Failed to fetch meetings:", error); - } finally { - setIsLoading(false); - } - }; - - if (activeTab === "chatting") fetchMeetings(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTab, meetingPage, meetingHasNext, meetingIsLoading]); - // 보낸 친구 요청 조회 useEffect(() => { const fetchSentRequests = async () => { @@ -224,18 +194,9 @@ const MyPage = () => { {/* 번개 모임 탭 */} {activeTab === "chatting" && ( <div className="p-4"> - {meetings.length === 0 && !isLoading && ( - <p className="text-center">참여 중인 채팅방이 없습니다.</p> - )} <div className="w-full"> <ChattingList /> </div> - {isLoading && <p className="text-center">로딩 중...</p>} - {!hasNext && meetings.length > 0 && ( - <p className="text-sm text-center text-gray-500"> - 더 이상 불러올 번개 모임이 없습니다. - </p> - )} </div> )} diff --git a/src/utils/time.js b/src/utils/time.js index 15faa366895cf38ff69fe7548157f24d536b6c32..ab32f55c62085c69a534eb0326db21fb8378c43e 100644 --- a/src/utils/time.js +++ b/src/utils/time.js @@ -23,3 +23,9 @@ export const convertIndexToTime = (timeIndex) => { .padStart(2, "0")}`; return `${day} ${time}`; }; + +export const convertTimeToIndex = (day, hour, minute) => { + const dayIndex = days.indexOf(day); + const timeIndex = Math.floor((hour * 60 + minute) / 15); + return dayIndex * 24 * 4 + timeIndex; +};