Skip to content
Snippets Groups Projects
Commit 3e65d8ca authored by 석찬 윤's avatar 석찬 윤
Browse files

Merge branch 'develop' into 'main'

feat: 번개모임 페이지 개발

See merge request !49
parents fd670c2a 373d95fb
No related branches found
No related tags found
1 merge request!49feat: 번개모임 페이지 개발
Pipeline #11017 passed
// src/api/meeting.js
// 기본 API URL // 기본 API URL
const BASE_URL = process.env.REACT_APP_BASE_URL; const BASE_URL = process.env.REACT_APP_BASE_URL;
// 번개 모임 조회 /**
export const fetchMyMeetings = async (page = 0, size = 20) => { * 모든 미팅 불러오기
try { * @param {number} page - 페이지 번호 (기본값: 0)
* @param {number} size - 페이지 크기 (기본값: 20)
* @returns {Promise<Object>} - 미팅 데이터
*/
export const getAllMeetings = async (page = 0, size = 20) => {
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/meeting?page=${page}&size=${size}`,// 내가 참여한 번개 모임 목록 조회할거면 my `${BASE_URL}/api/meeting?page=${page}&size=${size}`,
{ {
method: "GET", method: "GET",
credentials: "include", // 세션 기반 인증을 위해 필요
headers: {
"Content-Type": "application/json",
},
} }
); );
console.log("번개 모임 조회", response);
if (!response.ok) { if (!response.ok) {
throw new Error(`Error: ${response.status}`); 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; // 서버에서 제공된 데이터 반환 if (!response.ok) {
} catch (error) { throw new Error("Failed to fetch meeting details");
console.error("Error fetching my meetings:", error);
throw error;
} }
return (await response.json()).data;
}; };
// 번개 모임 생성 /**
* 미팅 생성
* @param {Object} meetingData - 미팅 생성에 필요한 데이터
* @returns {Promise<Object>} - 생성된 미팅 데이터
*/
export const createMeeting = async (meetingData) => { export const createMeeting = async (meetingData) => {
const response = await fetch(`${BASE_URL}/api/meeting`, { const response = await fetch(`${BASE_URL}/api/meeting`, {
method: "POST", method: "POST",
credentials: "include", // 세션 기반 인증
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
...@@ -47,24 +54,91 @@ export const createMeeting = async (meetingData) => { ...@@ -47,24 +54,91 @@ export const createMeeting = async (meetingData) => {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to create meeting: ${response.status}`); throw new Error("Failed to create meeting");
} }
return await response.json(); return await response.json();
}; };
// 번개 모임 참가 /**
* 미팅 참가하기
* @param {number} meetingId - 참가할 미팅 ID
* @returns {Promise<Object>} - 참가 결과 메시지
*/
export const joinMeeting = async (meetingId) => { export const joinMeeting = async (meetingId) => {
const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}/join`, { const response = await fetch(`${BASE_URL}/api/meeting/${meetingId}/join`, {
method: "POST", method: "POST",
credentials: "include", // 세션 기반 인증
headers: {
"Content-Type": "application/json",
},
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to join meeting: ${response.status}`); throw new Error("Failed to join meeting");
}
return await response.json();
};
/**
* 내가 참가한 미팅 불러오기
* @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(); return await response.json();
......
...@@ -18,7 +18,15 @@ const cardVariants = cva("w-full rounded-xl shadow-lg p-4 overflow-hidden", { ...@@ -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 { const {
title, title,
timeIdxStart, timeIdxStart,
...@@ -27,13 +35,18 @@ export default function Card({ meeting, theme = "black", onJoin, onClick }) { ...@@ -27,13 +35,18 @@ export default function Card({ meeting, theme = "black", onJoin, onClick }) {
creatorName, creatorName,
time_idx_deadline, time_idx_deadline,
type, type,
isParticipant,
isScheduleConflict,
} = meeting; } = meeting;
const variantClass = cardVariants({ const variantClass = cardVariants({
theme: type === "CLOSE" ? "gray" : theme, theme:
!isParticipant && !isScheduleConflict && type === "OPEN" ? "mix" : theme,
}); });
// 시간 변환 // 시간 변환
const userName = localStorage.getItem("nickname");
const startTime = convertIndexToTime(timeIdxStart); const startTime = convertIndexToTime(timeIdxStart);
const endTime = convertIndexToTime(timeIdxEnd); const endTime = convertIndexToTime(timeIdxEnd);
const deadlineTime = convertIndexToTime(time_idx_deadline); const deadlineTime = convertIndexToTime(time_idx_deadline);
...@@ -53,15 +66,62 @@ export default function Card({ meeting, theme = "black", onJoin, onClick }) { ...@@ -53,15 +66,62 @@ export default function Card({ meeting, theme = "black", onJoin, onClick }) {
<Label size="sm" theme="black"> <Label size="sm" theme="black">
주최자: {creatorName} 주최자: {creatorName}
</Label> </Label>
<div className="flex justify-between mt-4"> <div className="flex items-end justify-between mt-4">
<span className={`text-sm ${type === "OPEN" ? "text-green-500" : "text-red-500"}`}> <div className="flex gap-2">
{type === "OPEN" ? "참여 가능" : "참여 마감"} <Label
</span> className={`${type === "OPEN" ? "text-green-500" : "text-red-500"}`}
{type === "OPEN" && ( size="sm"
<Button size="sm" theme="mix" onClick={onJoin}> 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> </Button>
)} )}
</>
) : null}
</div> </div>
</div> </div>
); );
......
import React, { useState } from "react"; import React, { useState } from "react";
import { createMeeting } from "../api/meeting"; import Modal from "./Modal";
import Button from "./Button"; import Button from "./Button";
import { createMeeting } from "../api/meeting";
import { convertTimeToIndex } from "../utils/time";
import { days } from "../constants/schedule";
const CreateMeetingModal = ({ onClose }) => { const CreateMeetingModal = ({ isOpen, onClose }) => {
const [formData, setFormData] = useState({ const [title, setTitle] = useState("");
title: "", const [description, setDescription] = useState("");
description: "", const [location, setLocation] = useState("");
location: "", const [dayStart, setDayStart] = useState("");
time_idx_start: 0, const [hourStart, setHourStart] = useState("0");
time_idx_end: 0, const [minuteStart, setMinuteStart] = useState("0");
max_num: 5, const [dayEnd, setDayEnd] = useState("");
type: "OPEN", // 기본값 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 minutes = ["0", "15", "30", "45"];
const { name, value } = e.target; const hours = Array.from({ length: 24 }, (_, i) => i.toString());
setFormData({ ...formData, [name]: value }); const maxParticipants = Array.from({ length: 20 }, (_, i) =>
}; (i + 1).toString()
);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await createMeeting(formData); // API 호출 const time_idx_start = convertTimeToIndex(
alert("번개 모임이 성공적으로 생성되었습니다!"); dayStart,
onClose(); // 모달 닫기 parseInt(hourStart),
window.location.reload(); // 새로고침하여 번개 목록 업데이트 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) { } catch (error) {
console.error("Error creating meeting:", error); console.error("Failed to create meeting:", error);
alert("번개 모임 생성에 실패했습니다."); alert("모임 생성에 실패했습니다.");
} }
}; };
return ( return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <Modal isOpen={isOpen} onClose={onClose}>
<div className="bg-white rounded-lg p-6 w-11/12 max-w-md"> <h2 className="mb-4 text-2xl font-bold">번개 모임 만들기</h2>
<h2 className="text-lg font-bold mb-4">번개 모임 만들기</h2> <hr />
<div className="space-y-4"> <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 <input
name="title" type="text"
value={formData.title}
onChange={handleChange}
placeholder="제목"
className="w-full p-2 border rounded" 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 <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" className="w-full p-2 border rounded"
/> placeholder="모임 설명을 입력하세요"
<input value={description}
name="time_idx_start" onChange={(e) => setDescription(e.target.value)}
type="number" ></textarea>
value={formData.time_idx_start} </div>
onChange={handleChange} <div>
placeholder="시작 시간 인덱스" <label className="block mb-2 font-semibold">
className="w-full p-2 border rounded" 장소
/> <span className="relative ml-1 bottom-1 label-1 text-warning">
<input *
name="time_idx_end" </span>
type="number" </label>
value={formData.time_idx_end}
onChange={handleChange}
placeholder="종료 시간 인덱스"
className="w-full p-2 border rounded"
/>
<input <input
name="max_num" type="text"
type="number"
value={formData.max_num}
onChange={handleChange}
placeholder="최대 인원"
className="w-full p-2 border rounded" 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 <select
name="type" value={dayStart}
value={formData.type} onChange={(e) => setDayStart(e.target.value)}
onChange={handleChange} className="p-2 border rounded"
className="w-full p-2 border rounded"
> >
<option value="OPEN">OPEN</option> {days.map((day) => (
<option value="CLOSE">CLOSE</option> <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> </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>
<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>
<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
value={maxNum}
onChange={(e) => setMaxNum(e.target.value)}
className="w-full p-2 border rounded"
>
{maxParticipants.map((num) => (
<option key={num} value={num}>
{num}
</option>
))}
</select>
</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> </div>
</Modal>
); );
}; };
......
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;
...@@ -4,10 +4,10 @@ const Modal = ({ isOpen, onClose, children }) => { ...@@ -4,10 +4,10 @@ const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white w-full max-w-lg p-6 rounded-lg relative"> <div className="relative w-full max-w-lg p-6 overflow-scroll bg-white rounded-lg h-3/4">
<button <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} onClick={onClose}
> >
&times; &times;
......
...@@ -3,27 +3,60 @@ import useAuthStore from "../store/authStore"; ...@@ -3,27 +3,60 @@ import useAuthStore from "../store/authStore";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Card from "../components/Card"; import Card from "../components/Card";
import Button from "../components/Button"; import Button from "../components/Button";
import { fetchMyMeetings, joinMeeting } from "../api/meeting";
import CreateMeetingModal from "../components/CreateMeetingModal"; import CreateMeetingModal from "../components/CreateMeetingModal";
import MeetingDetailModal from "../components/MeetingDetailModal";
import {
getAllMeetings,
getMyMeetings,
getMeetingDetails,
joinMeeting,
deleteMeeting,
closeMeeting,
leaveMeeting,
} from "../api/meeting";
const MeetingPage = () => { const MeetingPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기 const { fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기
const [activeTab, setActiveTab] = useState("all"); // 현재 활성화된 탭 (전체/내 번개)
const [meetings, setMeetings] = useState([]); const [meetings, setMeetings] = useState([]);
const [myMeetings, setMyMeetings] = useState([]);
const [meetingPage, setMeetingPage] = useState(0); const [meetingPage, setMeetingPage] = useState(0);
const [myMeetingPage, setMyMeetingPage] = useState(0);
const [meetingHasNext, setMeetingHasNext] = useState(true); const [meetingHasNext, setMeetingHasNext] = useState(true);
const [myMeetingHasNext, setMyMeetingHasNext] = useState(true);
const [meetingIsLoading, setMeetingIsLoading] = useState(false); const [meetingIsLoading, setMeetingIsLoading] = useState(false);
const [myMeetingIsLoading, setMyMeetingIsLoading] = useState(false);
const [hasError, setHasError] = 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(() => { 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; if (!meetingHasNext || meetingIsLoading || hasError) return;
try { try {
setMeetingIsLoading(true); setMeetingIsLoading(true);
const data = await fetchMyMeetings(meetingPage, 20); // API 호출 const data = await getAllMeetings(meetingPage, 20);
setMeetings((prev) => [...prev, ...data.content]); // 기존 데이터에 추가 setMeetings((prev) => [...prev, ...data.content]);
setMeetingHasNext(data.hasNext); setMeetingHasNext(data.hasNext);
setMeetingPage((prev) => prev + 1); setMeetingPage((prev) => prev + 1);
} catch (error) { } catch (error) {
...@@ -34,56 +67,177 @@ const MeetingPage = () => { ...@@ -34,56 +67,177 @@ const MeetingPage = () => {
} }
}; };
fetchMeetings(); if (activeTab === "all") {
// eslint-disable-next-line react-hooks/exhaustive-deps fetchAllMeetings();
}, [meetingPage, meetingHasNext, meetingIsLoading]); }
}, [activeTab, meetingPage, meetingHasNext, meetingIsLoading, hasError]);
useEffect(() => { useEffect(() => {
const fetchUserSession = async () => { const fetchMyMeetings = async () => {
if (!myMeetingHasNext || myMeetingIsLoading || hasError) return;
try { try {
const userInfo = localStorage.getItem("user"); setMyMeetingIsLoading(true);
if (!userInfo) { const data = await getMyMeetings(myMeetingPage, 20);
alert("로그인이 필요한 페이지입니다."); setMyMeetings((prev) => [...prev, ...data.content]);
navigate("/login"); setMyMeetingHasNext(data.hasNext);
} setMyMeetingPage((prev) => prev + 1);
await fetchSession(); // 세션 정보 가져오기
} catch (error) { } catch (error) {
console.error("Failed to fetch session:", error); setHasError(true);
console.error("Failed to fetch my meetings:", error);
} finally {
setMyMeetingIsLoading(false);
} }
}; };
fetchUserSession(); if (activeTab === "my") {
}, [fetchSession, navigate]); // 페이지 마운트 시 실행 fetchMyMeetings();
}
}, [
activeTab,
myMeetingPage,
myMeetingHasNext,
myMeetingIsLoading,
hasError,
]);
const handleCreateMeeting = () => { const handleCreateMeeting = () => {
setShowModal(true); // 모달 열기 setShowCreateModal(true); // 모달 열기
}; };
const handleJoinMeeting = async (meetingId) => { const handleJoinButtonClick = async (e, meetingId) => {
e.stopPropagation();
try { try {
await joinMeeting(meetingId); await joinMeeting(meetingId);
alert("번개 모임에 성공적으로 참가했습니다!"); alert("번개 모임에 성공적으로 참가했습니다!");
// 참가 후 UI를 업데이트할 필요가 있다면 추가 // 참가 상태 업데이트
setMeetings((prev) =>
prev.map((meeting) =>
meeting.id === meetingId
? { ...meeting, isParticipant: true }
: meeting
)
);
} catch (error) { } catch (error) {
alert("번개 모임 참가에 실패했습니다."); alert("번개 모임 참가에 실패했습니다.");
console.error("Error joining meeting:", error); 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 ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* 헤더 */} {/* 헤더 */}
<header className="flex items-center justify-between px-6 py-4 bg-gradient-purple text-white"> <header className="flex items-center justify-between px-6 py-4 text-black border-b-2 rounded-t-xl border-grayscale-300">
<h1 className="text-xl font-bold">번개 모임</h1> <h1 className="heading-1">번개 모임</h1>
<Button size="sm" theme="pink" onClick={handleCreateMeeting}> <Button size="lg" theme="mix" onClick={handleCreateMeeting}>
번개 만들기 번개 만들기
</Button> </Button>
</header> </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"> <main className="p-6">
{activeTab === "all" && (
<>
{meetings.length === 0 && !meetingIsLoading && ( {meetings.length === 0 && !meetingIsLoading && (
<p className="text-center text-gray-500">참여 중인 번개 모임이 없습니다.</p> <p className="text-center text-gray-500">
현재 조회되는 번개 모임이 없습니다.
</p>
)} )}
<div className="grid grid-cols-1 gap-6 tablet:grid-cols-2 desktop:grid-cols-3"> <div className="grid grid-cols-1 gap-6 tablet:grid-cols-2 desktop:grid-cols-3">
{meetings.map((meeting) => ( {meetings.map((meeting) => (
...@@ -91,34 +245,67 @@ const MeetingPage = () => { ...@@ -91,34 +245,67 @@ const MeetingPage = () => {
key={meeting.id} key={meeting.id}
meeting={meeting} meeting={meeting}
theme="white" theme="white"
onClick={() => navigate(`/meetings/${meeting.id}`)} // 상세 페이지로 이동 onClick={() => handleCardClick(meeting.id)}
onJoin={() => handleJoinMeeting(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> </div>
{meetingIsLoading && <p className="text-center text-gray-500">로딩 중...</p>} {meetingIsLoading && (
<p className="text-center text-gray-500">로딩 중...</p>
)}
{!meetingHasNext && meetings.length > 0 && ( {!meetingHasNext && meetings.length > 0 && (
<p className="mt-4 text-sm text-center text-gray-400"> <p className="mt-4 text-sm text-center text-gray-400">
더 이상 불러올 번개 모임이 없습니다. 더 이상 불러올 번개 모임이 없습니다.
</p> </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> </main>
{/* 모달 */} {/* 모달 */}
{showModal && ( <CreateMeetingModal
<CreateMeetingModal onClose={() => setShowModal(false)} /> // 모달 닫기 핸들러 isOpen={showCreateModal}
)} onClose={() => setShowCreateModal(false)}
/>
<MeetingDetailModal
isOpen={showDetailModal}
onClose={() => setShowDetailModal(false)}
meeting={selectedMeeting}
/>
</div> </div>
); );
}; };
export default MeetingPage; 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
...@@ -11,8 +11,6 @@ import { ...@@ -11,8 +11,6 @@ import {
} from "../api/friend"; } from "../api/friend";
import Button from "../components/Button"; import Button from "../components/Button";
import LogoIcon from "../components/icons/LogoIcon"; import LogoIcon from "../components/icons/LogoIcon";
import { fetchMyMeetings } from "../api/meeting";
// import Card from "../components/Card";
import ChattingList from "../components/ChattingList"; import ChattingList from "../components/ChattingList";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
...@@ -26,11 +24,6 @@ const MyPage = () => { ...@@ -26,11 +24,6 @@ const MyPage = () => {
const [page, setPage] = useState(0); // 친구 목록 페이지 const [page, setPage] = useState(0); // 친구 목록 페이지
const [hasNext, setHasNext] = useState(true); // 페이지네이션 상태 const [hasNext, setHasNext] = useState(true); // 페이지네이션 상태
const [isLoading, setIsLoading] = useState(false); 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 [hasError, setHasError] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
...@@ -55,29 +48,6 @@ const MyPage = () => { ...@@ -55,29 +48,6 @@ const MyPage = () => {
fetchUserSession(); fetchUserSession();
}, [fetchSession, navigate]); // 페이지 마운트 시 실행 }, [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(() => { useEffect(() => {
const fetchSentRequests = async () => { const fetchSentRequests = async () => {
...@@ -224,18 +194,9 @@ const MyPage = () => { ...@@ -224,18 +194,9 @@ const MyPage = () => {
{/* 번개 모임 탭 */} {/* 번개 모임 탭 */}
{activeTab === "chatting" && ( {activeTab === "chatting" && (
<div className="p-4"> <div className="p-4">
{meetings.length === 0 && !isLoading && (
<p className="text-center">참여 중인 채팅방이 없습니다.</p>
)}
<div className="w-full"> <div className="w-full">
<ChattingList /> <ChattingList />
</div> </div>
{isLoading && <p className="text-center">로딩 중...</p>}
{!hasNext && meetings.length > 0 && (
<p className="text-sm text-center text-gray-500">
더 이상 불러올 번개 모임이 없습니다.
</p>
)}
</div> </div>
)} )}
......
...@@ -23,3 +23,9 @@ export const convertIndexToTime = (timeIndex) => { ...@@ -23,3 +23,9 @@ export const convertIndexToTime = (timeIndex) => {
.padStart(2, "0")}`; .padStart(2, "0")}`;
return `${day} ${time}`; 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;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment