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

Merge branch 'feat/#11' into 'develop'

[#11] 마이페이지 개발

See merge request !19
parents 25ec37c6 08650b16
Branches
No related tags found
10 merge requests!30fix: 친구목록 조회 오류 해결,!29[#15]시간 인덱스 관련 최적화,!27[#14] FCM 토큰 포함 로그인 로직 수정,!26[#14] FCM 토큰 포함 로그인 로직 수정,!25[#14] FCM 토큰 포함 로그인 로직 수정,!24[#13] 스케줄 페이지 스타일 개선,!23fix: 로그인 store 로직 수정,!22hotfix: 로그아웃 로직 및 홈페이지 하단 버튼 로직 변경,!20[#11] 마이페이지 개발,!19[#11] 마이페이지 개발
Pipeline #10906 passed
// 기본 API URL
const BASE_URL = process.env.REACT_APP_BASE_URL;
/**
* 친구 요청 보내기
* @param {Object} requestData - 요청 데이터 (userId, email)
* @returns {Promise<Object>} - 생성된 친구 요청 데이터
*/
export const sendFriendRequest = async (requestData) => {
const response = await fetch(`${BASE_URL}/api/friend/request`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestData),
});
if (!response.ok) {
throw new Error("Failed to send friend request");
}
return await response.json();
};
/**
* 받은 친구 요청 조회
* @returns {Promise<Object[]>} - 받은 친구 요청 리스트
*/
export const getReceivedFriendRequests = async () => {
const response = await fetch(`${BASE_URL}/api/friend/requests/received`);
if (!response.ok) {
throw new Error("Failed to fetch received friend requests");
}
return (await response.json()).data;
};
/**
* 보낸 친구 요청 조회
* @returns {Promise<Object[]>} - 보낸 친구 요청 리스트
*/
export const getSentFriendRequests = async () => {
const response = await fetch(`${BASE_URL}/api/friend/requests/sent`);
if (!response.ok) {
throw new Error("Failed to fetch sent friend requests");
}
return (await response.json()).data;
};
/**
* 친구 요청 수락
* @param {number} requestId - 친구 요청 ID
* @returns {Promise<Object>} - 수락된 친구 요청 데이터
*/
export const acceptFriendRequest = async (requestId) => {
const response = await fetch(
`${BASE_URL}/api/friend/request/${requestId}/accept`,
{
method: "POST",
}
);
if (!response.ok) {
throw new Error("Failed to accept friend request");
}
return (await response.json()).data;
};
/**
* 친구 요청 거절
* @param {number} requestId - 친구 요청 ID
* @returns {Promise<Object>} - 거절된 친구 요청 데이터
*/
export const rejectFriendRequest = async (requestId) => {
const response = await fetch(
`${BASE_URL}/api/friend/request/${requestId}/reject`,
{
method: "POST",
}
);
if (!response.ok) {
throw new Error("Failed to reject friend request");
}
return (await response.json()).data;
};
/**
* 친구 목록 조회
* @param {number} page - 페이지 번호
* @param {number} size - 페이지 크기
* @returns {Promise<Object>} - 친구 목록 데이터
*/
export const getAllFriends = async (page = 0, size = 10) => {
const response = await fetch(
`${BASE_URL}/api/friend/all?page=${page}&size=${size}`
);
if (!response.ok) {
throw new Error("Failed to fetch friends list");
}
return (await response.json()).data.content;
};
/**
* 친구 삭제
* @param {number} friendId - 삭제할 친구 ID
* @returns {Promise<Object>} - 삭제 결과 데이터
*/
export const deleteFriend = async (friendId) => {
const response = await fetch(`${BASE_URL}/api/friends/${friendId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to delete friend");
}
return await response.json();
};
// 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/my?page=${page}&size=${size}`,
{
method: "GET",
credentials: "include", // 세션 기반 인증을 위해 필요
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error("Failed to fetch meetings.");
}
return result.data; // 서버에서 제공된 데이터 반환
} catch (error) {
console.error("Error fetching my meetings:", error);
throw error;
}
};
import { cn } from "../libs/index"; import { cn } from "../libs/index";
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
import React from "react"; import React from "react";
import Label from "./Label";
const cardVariants = cva("w-[20rem] rounded-xl shadow-lg p-4 overflow-hidden", { // Card 스타일 변수를 정의합니다.
const cardVariants = cva("w-full rounded-xl shadow-lg p-4 overflow-hidden", {
variants: { variants: {
theme: { theme: {
black: "bg-black text-white", black: "bg-black text-white",
...@@ -11,35 +13,46 @@ const cardVariants = cva("w-[20rem] rounded-xl shadow-lg p-4 overflow-hidden", { ...@@ -11,35 +13,46 @@ const cardVariants = cva("w-[20rem] rounded-xl shadow-lg p-4 overflow-hidden", {
purple: "bg-gradient-purple text-white", purple: "bg-gradient-purple text-white",
indigo: "bg-gradient-indigo text-white", indigo: "bg-gradient-indigo text-white",
mix: "bg-gradient-mix text-white", mix: "bg-gradient-mix text-white",
gray: "bg-gray-300 text-gray-500",
}, },
}, },
}); });
export default function Card({ export default function Card({ meeting, theme = "black", onClick }) {
image, const {
title, title,
content, timeIdxStart,
time, timeIdxEnd,
headCount, location,
theme = "black", creatorName,
onClick, time_idx_deadline,
}) { type,
const variantClass = cardVariants({ theme }); } = meeting;
const variantClass = cardVariants({
theme: type === "CLOSE" ? "gray" : theme,
});
return ( return (
<div className={cn(variantClass)} onClick={onClick}> <div className={cn(variantClass)} onClick={onClick}>
{image && (
<img
src={image}
alt={title}
className="object-contain w-full h-48 rounded-xl"
/>
)}
<div className="p-3">
<h3 className="mb-2 text-xl font-bold">{title}</h3> <h3 className="mb-2 text-xl font-bold">{title}</h3>
<p className="text-base">{content}</p> <div className="flex gap-2 mb-2">
<p className="text-base">{time}</p> <Label size="sm" theme="indigo">
<p className="mt-5 text-right">{headCount}</p> {location}
</Label>
<Label size="sm" theme="indigo">
시간: {timeIdxStart} ~ {timeIdxEnd}
</Label>
</div>
<Label size="sm" theme="indigo">
마감 시간: {time_idx_deadline}
</Label>
<div className="flex justify-between mt-2">
<span className="text-sm text-white">작성자: {creatorName}</span>
<span className="text-sm text-right">
{type === "OPEN" ? "참여 가능" : "참여 마감"}
</span>
</div> </div>
</div> </div>
); );
......
...@@ -9,7 +9,7 @@ const labelVariants = cva( ...@@ -9,7 +9,7 @@ const labelVariants = cva(
theme: { theme: {
indigo: "bg-secondary-900 text-white", indigo: "bg-secondary-900 text-white",
solid: "bg-primary-600 text-white", solid: "bg-primary-600 text-white",
lightsolid: "bg-primary-100 text-primary-600", lightsolid: "bg-primary-600 text-primary-100 border border-primary-100",
graysolid: "bg-grayscale-100 text-grayscale-900", graysolid: "bg-grayscale-100 text-grayscale-900",
ghost: "border border-grayscale-500 text-grayscale-900", ghost: "border border-grayscale-500 text-grayscale-900",
}, },
......
...@@ -89,7 +89,7 @@ export default function HeaderNav() { ...@@ -89,7 +89,7 @@ export default function HeaderNav() {
icon={<ChatIcon />} icon={<ChatIcon />}
onClick={navigateToChattingList} onClick={navigateToChattingList}
> >
번개채팅방 번개모임
</Button> </Button>
{user ? ( {user ? (
<Button <Button
......
import React from "react"; import React, { useState, useEffect } from "react";
import useAuthStore from "../store/authStore";
import {
getReceivedFriendRequests,
sendFriendRequest,
getAllFriends,
acceptFriendRequest,
rejectFriendRequest,
deleteFriend,
} 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 { useNavigate } from "react-router-dom";
const MyPage = () => { const MyPage = () => {
return <></>; const { user, fetchSession } = useAuthStore(); // Zustand 상태 및 메서드 가져오기
const [activeTab, setActiveTab] = useState("lightning"); // 현재 활성화된 탭
const [receivedRequests, setReceivedRequests] = useState([]);
const [sentRequests, setSentRequests] = useState([]);
const [friends, setFriends] = useState([]);
const [email, setEmail] = useState(""); // 친구 요청할 이메일
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 navigate = useNavigate();
// 탭 변경 함수
const switchTab = (tab) => setActiveTab(tab);
useEffect(() => {
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 fetchMeetings = async () => {
if (!meetingHasNext || meetingIsLoading) return;
try {
setMeetingIsLoading(true);
const data = await fetchMyMeetings(meetingPage, 20);
setMeetings((prev) => [...prev, ...data.content]);
setMeetingHasNext(data.meetingHasNext);
setMeetingPage((prev) => prev + 1);
} catch (error) {
console.error("Failed to fetch meetings:", error);
} finally {
setIsLoading(false);
}
};
if (activeTab === "lightning") fetchMeetings();
}, [activeTab, meetingPage, meetingHasNext, meetingIsLoading]);
// 받은 친구 요청 조회
useEffect(() => {
const fetchReceivedRequests = async () => {
try {
const data = await getReceivedFriendRequests();
setReceivedRequests(data);
} catch (error) {
console.error("Failed to fetch received requests:", error);
}
};
if (activeTab === "friends") fetchReceivedRequests();
}, [activeTab]);
// 친구 목록 무한스크롤 처리
useEffect(() => {
const fetchFriends = async () => {
if (!hasNext || isLoading) return;
try {
setIsLoading(true);
const data = await getAllFriends(page, 10);
setFriends((prev) => [...prev, ...data.content]);
setHasNext(data.hasNext);
setPage((prev) => prev + 1);
} catch (error) {
console.error("Failed to fetch friends:", error);
} finally {
setIsLoading(false);
}
};
if (activeTab === "friends") fetchFriends();
}, [page, hasNext, activeTab, isLoading]);
// 친구 요청 보내기
const handleSendRequest = async () => {
try {
const requestData = { email };
const response = await sendFriendRequest(requestData);
setSentRequests((prev) => [...prev, response.data]);
setEmail(""); // 입력 필드 초기화
} catch (error) {
console.error("Failed to send friend request:", error);
}
};
// 친구 요청 수락
const handleAcceptRequest = async (requestId) => {
try {
const response = await acceptFriendRequest(requestId);
setReceivedRequests((prev) =>
prev.filter((request) => request.id !== requestId)
);
setFriends((prev) => [response, ...prev]); // 친구 목록에 추가
} catch (error) {
console.error("Failed to accept request:", error);
}
};
// 친구 요청 거절
const handleRejectRequest = async (requestId) => {
try {
await rejectFriendRequest(requestId);
setReceivedRequests((prev) =>
prev.filter((request) => request.id !== requestId)
);
} catch (error) {
console.error("Failed to reject request:", error);
}
};
// 친구 삭제
const handleDeleteFriend = async (friendId) => {
try {
await deleteFriend(friendId);
setFriends((prev) => prev.filter((friend) => friend.id !== friendId));
} catch (error) {
console.error("Failed to delete friend:", error);
}
};
return (
<div className="w-full max-w-screen-lg min-h-screen mx-auto bg-white">
{/* 프로필 영역 */}
<div className="flex items-center px-6 py-4 border-b">
<div className="flex-shrink-0">
<LogoIcon className="w-20 h-20 rounded-full" />
</div>
<div className="flex-grow ml-6">
<h1 className="text-xl font-bold">{user?.name || "Guest"}</h1>
<p className="text-gray-600">{user?.email || "guest@example.com"}</p>
</div>
</div>
{/* 탭 네비게이션 */}
<div className="flex justify-center py-2 space-x-6 border-b">
<button
className={`px-4 py-2 text-sm font-medium ${
activeTab === "lightning"
? "text-secondary-500 border-b-2 border-secondary-500"
: "text-gray-600"
}`}
onClick={() => switchTab("lightning")}
>
번개 모임
</button>
<button
className={`px-4 py-2 text-sm font-medium ${
activeTab === "friends"
? "text-tertiary-500 border-b-2 border-tertiary-500"
: "text-gray-600"
}`}
onClick={() => switchTab("friends")}
>
친구
</button>
</div>
{/* 번개 모임 탭 */}
{activeTab === "lightning" && (
<div className="p-4">
{meetings.length === 0 && !isLoading && (
<p className="text-center">참여 중인 번개 모임이 없습니다.</p>
)}
<div className="grid grid-cols-1 gap-4 tablet:grid-cols-2 desktop:grid-cols-3">
{meetings.map((meeting) => (
<Card
key={meeting.id}
meeting={meeting}
theme="purple"
onClick={() => console.log("Clicked meeting:", meeting.id)}
/>
))}
</div>
{isLoading && <p className="text-center">로딩 중...</p>}
{!hasNext && meetings.length > 0 && (
<p className="text-sm text-center text-gray-500">
더 이상 불러올 번개 모임이 없습니다.
</p>
)}
</div>
)}
{/* 친구 탭 */}
{activeTab === "friends" && (
<div className="p-4 space-y-8">
{/* 친구 요청하기 */}
<div>
<h2 className="text-lg font-bold">친구 요청하기</h2>
<div className="flex items-center my-4 space-x-4">
<input
type="email"
className="flex-1 min-w-0 p-3 border rounded-full"
placeholder="친구 이메일 입력"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button
size="md"
theme="indigo"
className="min-w-[50px]"
onClick={handleSendRequest}
>
요청
</Button>
</div>
<div className="mt-4 space-y-4">
{sentRequests.map((request) => (
<div key={request.id} className="p-2 border rounded-lg">
<p className="text-sm">
{request.receiver.name} ({request.receiver.email})
</p>
<p className="text-xs text-gray-500">{request.status}</p>
</div>
))}
</div>
</div>
<hr />
{/* 받은 친구 요청 */}
<div>
<h2 className="mb-2 text-lg font-bold">받은 친구 요청</h2>
<div className="space-y-4">
{receivedRequests.map((request) => (
<div
key={request.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div>
<h3 className="font-semibold text-md">
{request.requester.name}
</h3>
<p className="text-[10px] tablet:text-sm text-gray-600">
{request.requester.email}
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
theme="indigo"
onClick={() => handleAcceptRequest(request.id)}
>
수락
</Button>
<Button
size="sm"
theme="pink"
onClick={() => handleRejectRequest(request.id)}
>
거절
</Button>
</div>
</div>
))}
</div>
</div>
<hr />
{/* 친구 목록 */}
<div>
<h2 className="mb-2 text-lg font-bold">친구 목록</h2>
<div className="space-y-4">
{friends.map((friend) => (
<div
key={friend.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div>
<h3 className="font-semibold text-md">
{friend.friendInfo.name}
</h3>
<p className="text-[10px] tablet:text-sm text-gray-600">
{friend.friendInfo.email}
</p>
</div>
<Button
size="sm"
theme="black"
onClick={() => handleDeleteFriend(friend.id)}
>
삭제
</Button>
</div>
))}
{isLoading && <p>로딩 중...</p>}
{!hasNext && (
<p className="text-center label-2">더 이상 친구가 없습니다.</p>
)}
</div>
</div>
</div>
)}
</div>
);
}; };
export default MyPage; export default MyPage;
...@@ -11,6 +11,7 @@ const useAuthStore = create((set) => ({ ...@@ -11,6 +11,7 @@ const useAuthStore = create((set) => ({
try { try {
const userInfo = await getSessionInfo(); const userInfo = await getSessionInfo();
set({ user: userInfo }); set({ user: userInfo });
localStorage.setItem("user", { user: userInfo });
} catch (error) { } catch (error) {
console.error("Failed to fetch session info:", error); console.error("Failed to fetch session info:", error);
set({ user: null }); set({ user: null });
...@@ -24,6 +25,7 @@ const useAuthStore = create((set) => ({ ...@@ -24,6 +25,7 @@ const useAuthStore = create((set) => ({
try { try {
await logout(); await logout();
set({ user: null }); set({ user: null });
localStorage.removeItem("user");
} catch (error) { } catch (error) {
console.error("Failed to logout:", error); console.error("Failed to logout:", error);
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment