diff --git a/src/api/client.js b/src/api/client.js index 4917c0b3..3f608a75 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -91,7 +91,7 @@ const deleteCookie = (name) => { }; // 강제 로그아웃 처리 (refresh 실패 시) -const handleRefreshFailure = async () => { +const handleRefreshFailure = async (skipModal = false) => { try { // 1. 로그아웃 API 호출 (204 응답 확인) const logoutResponse = await axios.post( @@ -120,13 +120,21 @@ const handleRefreshFailure = async () => { deleteCookie("RefreshToken"); deleteCookie("refresh_token"); - // 4. 로그인 만료 모달 표시를 위한 커스텀 이벤트 발생 - const event = new CustomEvent('showSessionExpiredModal', { - detail: { - message: "로그인 시간이 만료되었습니다. 재로그인해주세요" - } - }); - window.dispatchEvent(event); + // 4. 로그인 만료 모달 표시를 위한 커스텀 이벤트 발생 (사용자가 직접 로그아웃한 경우 제외) + // 사용자가 직접 로그아웃한 경우 localStorage에 플래그가 설정되어 있음 + const isManualLogout = localStorage.getItem("manualLogout") === "true"; + if (!skipModal && !isManualLogout) { + const event = new CustomEvent('showSessionExpiredModal', { + detail: { + message: "로그인 시간이 만료되었습니다. 재로그인해주세요" + } + }); + window.dispatchEvent(event); + } + // 플래그 제거 + if (isManualLogout) { + localStorage.removeItem("manualLogout"); + } // 5. 로그인 페이지로 리다이렉트 (모달 닫힌 후 이동) // 모달에서 처리하도록 주석 처리 diff --git a/src/api/club.js b/src/api/club.js new file mode 100644 index 00000000..d40b1c42 --- /dev/null +++ b/src/api/club.js @@ -0,0 +1,49 @@ +import client from "./client"; + + +export async function getClubList(pageable) { + const response = await client.get(`/api/v1/clubs`, { + params: pageable, + }); + return response; + } + + export async function postClubMemberJoin(clubId) { + const response = await client.post(`/api/v1/clubs/${clubId}/join`); + return response; + } + + export async function getMyClubList(pageable) { + const response = await client.get("/api/v1/clubs/my", { + params: pageable, + }); + return response; + } + + export async function getClubMemberList(clubId, pageable) { + const response = await client.get(`/api/v1/clubs/${clubId}/members`, { + params: pageable, + }); + return response; + } + + export async function getClubJoinPendingList(clubId, pageable) { + const response = await client.get(`/api/v1/clubs/${clubId}/pending`, { + params: pageable, + }); + return response; + } + + export async function patchClubMemberJoinDecision(clubId, studentId, decision) { + const response = await client.patch(`/api/v1/clubs/${clubId}/members/${studentId}`, null, { + params: { + decision: decision, + }, + }); + return response; + } + + export async function deleteClubWithdraw(clubId) { + const response = await client.delete(`/api/v1/clubs/${clubId}/withdraw`); + return response; + } \ No newline at end of file diff --git a/src/api/mypage.js b/src/api/mypage.js index 75c2db97..659126ef 100644 --- a/src/api/mypage.js +++ b/src/api/mypage.js @@ -68,7 +68,6 @@ export async function getNickNameVerify(nickname) { } export async function getInquiryList(pageable) { - const accessToken = localStorage.getItem("accessToken"); const response = await client.get("/api/v1/inquiry/my", { params: pageable, }); @@ -76,7 +75,6 @@ export async function getInquiryList(pageable) { } export async function deleteInquiry(inquiryId) { - const accessToken = localStorage.getItem("accessToken"); const response = await client.delete(`/api/v1/inquiry/${inquiryId}`, { }); return response; diff --git a/src/assets/images/clubIcon.svg b/src/assets/images/clubIcon.svg new file mode 100644 index 00000000..db7fef60 --- /dev/null +++ b/src/assets/images/clubIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/expelIcon.svg b/src/assets/images/expelIcon.svg new file mode 100644 index 00000000..ef30bc21 --- /dev/null +++ b/src/assets/images/expelIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/outIcon.svg b/src/assets/images/outIcon.svg index 398261b3..f9d9d22f 100644 --- a/src/assets/images/outIcon.svg +++ b/src/assets/images/outIcon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/components/chat/chatMessage.jsx b/src/components/chat/chatMessage.jsx index 81b88ef4..9480cc53 100644 --- a/src/components/chat/chatMessage.jsx +++ b/src/components/chat/chatMessage.jsx @@ -366,7 +366,7 @@ export default function ChatMessage({ chatNickname, roomId, opponentProfileImage outIcon + outIcon 나가기 @@ -377,7 +377,7 @@ export default function ChatMessage({ chatNickname, roomId, opponentProfileImage className="bg-red-600 rounded-md p-2 text-white flex items-center gap-2 text-sm" onClick={() => handleDeleteChatRoom(roomId)} > - outIcon + outIcon 나가기 diff --git a/src/components/header.jsx b/src/components/header.jsx index fa8fc212..9927ddfb 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -120,7 +120,7 @@ useEffect(() => { setShowFeedAlertModal(true); return false; } - if (roleType !== "STUDENT" && roleType !== "ADMIN") { + if (roleType !== "STUDENT" && roleType !== "CLUB") { setShowFeedAlertModal(true); return false; } @@ -164,6 +164,9 @@ useEffect(() => { }; const toggleLogin = () => { + // 사용자가 직접 로그아웃했음을 표시하는 플래그 설정 + localStorage.setItem("manualLogout", "true"); + UserStore.getState().clearUser(); localStorage.removeItem("isLogin"); localStorage.removeItem("userType"); @@ -401,7 +404,7 @@ const DesktopHeader = () => ( 공고문 작성하기 )} - {roleType === "STUDENT" && ( + {(roleType === "STUDENT" || roleType === "CLUB") && ( - + {roleType === "MEMBER" && ( + + )} + {(roleType === "STUDENT" || roleType === "CLUB") && ( + + )} + ) : ( +
+ + + +
+ )} + + +
+ {clubMemberListLoading ? ( +
로딩 중...
+ ) : clubMemberListError ? ( +
오류가 발생했습니다.
+ ) : clubMemberList?.data?.result?.content && clubMemberList.data.result.content.length > 0 ? ( +
+ {clubMemberList.data.result.content.map((member) => { + const isSelected = selectedMembers.includes(member.memberId); + return ( +
{ + if (isEditMode) { + const memberId = member.memberId; + setSelectedMembers(prev => { + if (prev.includes(memberId)) { + return prev.filter(id => id !== memberId); + } else { + return [...prev, memberId]; + } + }); + } + }} + > +
+
+ 프로필 이미지 { + e.target.src = basicLogoImg; + }} + onClick={(e) => { + if (!isEditMode) { + e.stopPropagation(); + navigate(`/profileDetail/${member.memberId}`); + } + }} + /> + {member.temperature && ( +

스프 온도 {member.temperature}°C

+ )} +
+
+

{ + if (!isEditMode) { + e.stopPropagation(); + navigate(`/profileDetail/${member.memberId}`); + } + }} + > + {member.nickname || '익명'} +

+ {member.intro && ( +

{member.intro}

+ )} + {member.categoryDtoList && member.categoryDtoList.length > 0 && ( +
+ {member.categoryDtoList.map((category, index) => { + const categoryName = getLastCategoryName(category); + return categoryName ? ( + + {categoryName} + + ) : null; + })} +
+ )} + +
+ +
+ {isEditMode && isSelected && ( + expel + )} +
+ ); + })} +
+ ) : ( +
동아리원이 없습니다.
+ )} +
+ + +
+

동아리원 신청

+
+ {clubJoinPendingListLoading ? ( +
로딩 중...
+ ) : clubJoinPendingListError ? ( +
오류가 발생했습니다.
+ ) : clubJoinPendingList?.data?.result?.content && clubJoinPendingList.data.result.content.length > 0 ? ( +
+ {clubJoinPendingList.data.result.content.map((member) => ( +
+
+
+ 프로필 이미지 { + e.target.src = basicLogoImg; + }} + onClick={() => navigate(`/profileDetail/${member.memberId}`)} + /> + {member.temperature && ( +

스프 온도 {member.temperature}°C

+ )} +
+
+

navigate(`/profileDetail/${member.memberId}`)} + > + {member.nickname || '익명'} +

+ {member.intro && ( +

{member.intro}

+ )} + {member.categoryDtoList && member.categoryDtoList.length > 0 && ( +
+ {member.categoryDtoList.map((category, index) => { + const categoryName = getLastCategoryName(category); + return categoryName ? ( + + {categoryName} + + ) : null; + })} +
+ )} +
+
+
+ + +
+
+ )) +} +
) : ( +
신청한 회원이 없습니다.
+ )} +
+
+ + + )} + {roleType === 'STUDENT' && ( + <> +

내 동아리

+
+
+
+ {myClubListLoading ? ( +
로딩 중...
+ ) : myClubListError ? ( +
오류가 발생했습니다.
+ ) : myClubList?.data?.result?.content && myClubList.data.result.content.length > 0 ? ( + myClubList.data.result.content.map((club) => ( +
+
+ 동아리 이미지 +
+
+

navigate(`/profileDetail/${club.clubId}`)} + >{club.clubName}

+

|

+

인원 {club.memberCount}

+
+

{club.clubIntro}

+
+
+ +
+ )) + ) : ( +
내 동아리가 없습니다.
+ )} +
+
+
+ + )} + {modalState.isOpen && ( + + )} + + ); +} \ No newline at end of file diff --git a/src/components/studentProfile/profileDetail.jsx b/src/components/studentProfile/profileDetail.jsx index 3145a8c5..192dbd41 100644 --- a/src/components/studentProfile/profileDetail.jsx +++ b/src/components/studentProfile/profileDetail.jsx @@ -2,11 +2,13 @@ import { useNavigate, useParams } from "react-router-dom"; import backArrow from "../../assets/images/backArrow.svg"; import starOn from "../../assets/images/starOn.svg"; import starOff from "../../assets/images/starOff.svg"; -import BasicImg4 from "../../assets/images/BasicProfileImg4.png"; +import spoonMark from "../../assets/images/spoonMark.svg"; +import basicLogoImg from "../../assets/images/basiclogoimg.png"; import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { getProfileDetail } from "../../api/profile"; import { getFavorite, postFavorite, deleteFavorite } from "../../api/favorite"; +import { getClubMemberList, postClubMemberJoin } from "../../api/club"; import { UserStore } from "../../store/userStore"; import AlertModal from "../alertModal"; import SEO from "../seo"; @@ -14,6 +16,244 @@ import DeclareButton from "../declare/declareButton"; import { FAVORITE_ERRORS } from "../../constants/user"; import { handleApiError } from "../../utils/apiErrorHandler"; +// 학생 계정 프로필 UI 컴포넌트 +const StudentProfileUI = ({ + userData, + userWorks, + star, + isAnimating, + showIntroSkeleton, + handleFavorite, + handleDeclareClick, + onWorkClick, + S3_BUCKET_URL, + basicLogoImg +}) => { + return ( +
+
+ {/* 프로필 이미지 */} + 프로필 이미지 { + e.target.src = basicLogoImg; + }} + /> + +
+
+ {/* 닉네임 */} + {userData?.nickname ? ( +
{userData.nickname}
+ ) : ( +
+ )} + +
+ {/* 즐겨찾기 버튼 - 로그인한 사용자이고 본인이 아닐 때만 */} + {UserStore.getState().memberId && UserStore.getState().memberId !== userData?.id && ( + + )} + +
+
+ + {/* 자기소개 */} + {userData?.intro ? ( +
{userData.intro}
+ ) : showIntroSkeleton ? ( +
+ ) : ( +
+ )} + + {/* 개인 URL */} + {userData?.personalUrl ? ( +
{userData.personalUrl}
+ ) : showIntroSkeleton ? ( +
+ ) : ( +
+ )} +
+
+
+ {userWorks && userWorks.length > 0 ? ( +
+ {userWorks.map((data) => ( + onWorkClick(data.feedId)} + alt="작품 이미지" + /> + ))} +
+ ) : ( +
+
등록된 피드가 없습니다.
+
+ )} +
+ ); +}; + +// 동아리 계정 프로필 UI 컴포넌트 +const ClubProfileUI = ({ + userData, + userWorks, + star, + isAnimating, + showIntroSkeleton, + handleFavorite, + handleDeclareClick, + onWorkClick, + S3_BUCKET_URL, + basicLogoImg, + handleClubMemberJoin, + clubMemberList +}) => { + return ( +
+
+
+ {/* 프로필 이미지 */} + 프로필 이미지 { + e.target.src = basicLogoImg; + }} + /> + +
+
+ {/* 닉네임 */} + {userData?.nickname ? ( +
+ spoonMark +
{userData.nickname}
+
+ ) : ( +
+ )} + +
+ {/* 즐겨찾기 버튼 - 로그인한 사용자이고 본인이 아닐 때만 */} + {UserStore.getState().memberId && UserStore.getState().memberId !== userData?.id && ( + + )} + +
+
+ + {/* 자기소개 */} + {userData?.intro ? ( +
{userData.intro}
+ ) : showIntroSkeleton ? ( +
+ ) : ( +
+ )} + + {/* 개인 URL */} + {userData?.personalUrl ? ( +
{userData.personalUrl}
+ ) : showIntroSkeleton ? ( +
+ ) : ( +
+ )} +
+
+
+ {userWorks && userWorks.length > 0 ? ( +
+ {userWorks.map((data) => ( + onWorkClick(data.feedId)} + alt="작품 이미지" + /> + ))} +
+ ) : ( +
+
등록된 피드가 없습니다.
+
+ )} +
+{/* 동아리원 파트 */} +
+

동아리 멤버

+
+ {clubMemberList && clubMemberList.length > 0 ? ( + clubMemberList.map((member) => ( +
+ 동아리원 프로필 이미지 { + e.target.src = basicLogoImg; + }} + /> +
+

{member.nickname || '익명'}

+ {member.intro && ( +

{member.intro}

+ )} +
+
+ )) + ) : ( +
동아리원이 없습니다.
+ )} + +
+ +
+
+ ); +}; + export default function ProfileDetail({}) { const { id } = useParams(); const navigate = useNavigate(); @@ -26,6 +266,9 @@ export default function ProfileDetail({}) { const [errorModal, setErrorModal] = useState(false); const [errorDescription, setErrorDescription] = useState("잘못된 접근"); const [errorAction, setErrorAction] = useState("redirect"); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [clubMemberList, setClubMemberList] = useState([]); const fromMemberId = UserStore.getState().memberId; const S3_BUCKET_URL = import.meta.env.VITE_S3_BUCKET_URL; @@ -38,6 +281,8 @@ export default function ProfileDetail({}) { queryKey: ["profileDetail"], queryFn: async () => { const data = await getProfileDetail(id); + // console.log('프로필 상세 API 응답:', data); + // console.log('memberResDto:', data.result.memberResDto); setUserData(data.result.memberResDto); setUserWorks(data.result.feedSimpleResDtoPage.content) @@ -148,6 +393,64 @@ export default function ProfileDetail({}) { // 여기에 신고 API 호출 }; + // 계정 타입 확인 - 여러 가능한 필드명 확인 + // console.log('userData 전체:', userData); + + // 여러 가능한 필드명에서 계정 타입 찾기 + const accountType = userData?.roleType || + userData?.role || + userData?.userType || + userData?.accountType || + userData?.memberType || + "STUDENT"; // 기본값은 STUDENT + const isStudentAccount = accountType === "STUDENT"; + const isClubAccount = accountType === "CLUB"; + // console.log('최종 계정 타입:', accountType, 'isStudentAccount:', isStudentAccount, 'isClubAccount:', isClubAccount); + + const handleClubMemberJoin = async () => { + if (!UserStore.getState().roleType !== "STUDENT") { + setErrorDescription("학생 계정만 동아리 멤버 신청을 할 수 있습니다."); + setErrorAction("reload"); + setErrorModal(true); + return; + } + try { + const response = await postClubMemberJoin(userData.id); + if (response.data && response.data.code === 200) { + setSuccessMessage(response.data.result || "동아리 지원이 완료되었습니다."); + setShowSuccessModal(true); + } + } catch (error) { + console.error("동아리 멤버 신청 에러:", error); + setErrorDescription(error.response?.data?.message || "동아리 멤버 신청 중 오류가 발생했습니다."); + setErrorAction("redirect"); + setErrorModal(true); + } + } + + const handleClubMemberList = async () => { + try { + const pageable = { page: 0, size: 10 }; + const response = await getClubMemberList(userData.id, pageable); + if (response.data && response.data.code === 200) { + setClubMemberList(response.data.result?.content || []); + } + } + catch (error) { + console.error("동아리 멤버 목록 조회 에러:", error); + setErrorDescription(error.response?.data?.message || "동아리 멤버 목록 조회 중 오류가 발생했습니다."); + setErrorAction("redirect"); + setErrorModal(true); + } + } + + // 동아리 계정일 때 동아리원 목록 조회 + useEffect(() => { + if (isClubAccount && userData?.id) { + handleClubMemberList(); + } + }, [isClubAccount, userData?.id]); + return ( <> @@ -158,7 +461,7 @@ export default function ProfileDetail({}) { subTitle="스프" /> )} -
+
-
- - -
- {/* 프로필 이미지 */} - 프로필 이미지 { - e.target.src = BasicImg4; - }} - /> - -
-
- {/* 닉네임 */} - {userData?.nickname ? ( -
{userData.nickname}
- ) : ( -
- )} - -
-{/* 즐겨찾기 버튼 - 로그인한 사용자이고 본인이 아닐 때만 */} -{UserStore.getState().memberId && UserStore.getState().memberId !== userData?.id && ( - - )} - -
- -
- - {/* 자기소개 */} - {userData?.intro ? ( -
{userData.intro}
- ) : showIntroSkeleton ? ( -
- ) : ( -
- )} - - {/* 개인 URL */} - {userData?.personalUrl ? ( -
{userData.personalUrl}
- ) : showIntroSkeleton ? ( -
- ) : ( -
- )} - -
-
-
- {userWorks && userWorks.length > 0 ? ( -
- {userWorks.map((data) => ( - onWorkClick(data.feedId)} - alt="작품 이미지" - /> - ))} -
- ) : ( -
-
등록된 피드가 없습니다.
-
- )} -
+ + {/* 계정 타입에 따라 다른 UI 렌더링 */} + {isStudentAccount && ( + + )} + + {isClubAccount && ( + + )} {showLoginModal && ( )} + {showSuccessModal && ( + { + setShowSuccessModal(false); + }} + /> + )}
); diff --git a/src/pages/mypage.jsx b/src/pages/mypage.jsx index 84df2e41..8ef61eef 100644 --- a/src/pages/mypage.jsx +++ b/src/pages/mypage.jsx @@ -1,16 +1,19 @@ import React, { useState } from 'react'; +import SEO from '../components/seo'; // import { useNavigate } from 'react-router-dom'; import profileIcon from '../assets/images/profileIcon.svg' import starIcon from '../assets/images/starIcon.svg' import applyIcon from '../assets/images/applyIcon.svg' import feedIcon from '../assets/images/feedIcon.svg' import inquiryIcon from '../assets/images/inquiryIcon.svg' +import clubIcon from '../assets/images/clubIcon.svg' import ProfileEditContent from '../components/mypage/ProfileEditContent'; import ApplicationsContent from '../components/mypage/ApplicationsContent'; import FavoritesContent from '../components/mypage/FavoritesContent'; import InquiryContent from '../components/mypage/inquiryContent'; import CompanyApplicants from '../components/companyMyPage/companyApplicants'; +import ClubList from '../components/mypage/clubList'; import { UserStore } from '../store/userStore'; import MyFeed from '../components/mypage/myFeed'; @@ -42,6 +45,8 @@ export default function MyPage() { return ; case 'inquiry': return ; + case 'clubList': + return ; default: return ; } @@ -81,6 +86,11 @@ export default function MyPage() { label: '내 피드', icon: feedIcon }); + roleSpecificMenus.push({ + id: 'clubList', + label: '내 동아리', + icon: clubIcon + }); } // MEMBER: 기업 지원 내역 보여줌 @@ -113,12 +123,27 @@ export default function MyPage() { } + if (roleType === 'CLUB') { + roleSpecificMenus.push({ + id: 'myFeed', + label: '내 피드', + icon: feedIcon + }); + roleSpecificMenus.push({ + id: 'clubList', + label: '동아리 목록', + icon: clubIcon + }); + } + return [...baseMenus, ...roleSpecificMenus]; }; const menuItems = renderMenuItems(); return ( + <> +
{/* 모바일 메뉴 버튼 */}
+ ); } \ No newline at end of file