From a732204588a5bb1310c8b6af194a7fc994020e17 Mon Sep 17 00:00:00 2001 From: tldms0507 Date: Sun, 16 Nov 2025 17:36:22 +0900 Subject: [PATCH 1/7] =?UTF-8?q?FEAT=20:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8D=BC?= =?UTF-8?q?=EB=B8=94=EB=A6=AC=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/header.jsx | 30 +- .../studentProfile/profileDetail.jsx | 349 +++++++++++++----- 2 files changed, 280 insertions(+), 99 deletions(-) diff --git a/src/components/header.jsx b/src/components/header.jsx index fa8fc21..b763e46 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; } @@ -401,7 +401,7 @@ const DesktopHeader = () => ( 공고문 작성하기 )} - {roleType === "STUDENT" && ( + {(roleType === "STUDENT" || roleType === "CLUB") && ( - + {roleType === "MEMBER" && ( + + )} + {(roleType === "STUDENT" || roleType === "CLUB") && ( + + )} + )} + + + + + {/* 자기소개 */} + {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 +}) => { + 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="작품 이미지" + /> + ))} +
+ ) : ( +
+
등록된 피드가 없습니다.
+
+ )} +
+{/* 동아리원 파트 */} + {/*
+

동아리 멤버

+
+ 동아리원 프로필 이미지 +
+

동아리원 1

+

동아리원 1 소개

+
+
+
*/} +
+ ); +}; + export default function ProfileDetail({}) { const { id } = useParams(); const navigate = useNavigate(); @@ -38,6 +251,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 +363,19 @@ 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); return ( <> @@ -158,7 +386,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 && ( Date: Sun, 16 Nov 2025 17:40:22 +0900 Subject: [PATCH 2/7] =?UTF-8?q?DESIGN=20:=20=ED=94=BC=EB=93=9C=201:1?= =?UTF-8?q?=EB=B9=84=EC=9C=A8=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/studentProfile/profileDetail.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/studentProfile/profileDetail.jsx b/src/components/studentProfile/profileDetail.jsx index df15195..8242e5c 100644 --- a/src/components/studentProfile/profileDetail.jsx +++ b/src/components/studentProfile/profileDetail.jsx @@ -99,7 +99,7 @@ const StudentProfileUI = ({ onWorkClick(data.feedId)} alt="작품 이미지" /> @@ -200,7 +200,7 @@ const ClubProfileUI = ({ onWorkClick(data.feedId)} alt="작품 이미지" /> From 1d06fc8f8e990fad5575dd154bb21ffc21de38a5 Mon Sep 17 00:00:00 2001 From: tldms0507 Date: Sun, 16 Nov 2025 17:43:04 +0900 Subject: [PATCH 3/7] =?UTF-8?q?DESIGN=20:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A7=88=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/studentProfile/profileDetail.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/studentProfile/profileDetail.jsx b/src/components/studentProfile/profileDetail.jsx index 8242e5c..847c080 100644 --- a/src/components/studentProfile/profileDetail.jsx +++ b/src/components/studentProfile/profileDetail.jsx @@ -2,6 +2,7 @@ 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 spoonMark from "../../assets/images/spoonMark.svg"; import basicLogoImg from "../../assets/images/basiclogoimg.png"; import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; @@ -145,7 +146,10 @@ const ClubProfileUI = ({
{/* 닉네임 */} {userData?.nickname ? ( +
+ spoonMark
{userData.nickname}
+
) : (
)} From 8cac831989dd6c5d5b37cb085b30bee702d2e30c Mon Sep 17 00:00:00 2001 From: tldms0507 Date: Sun, 16 Nov 2025 17:44:17 +0900 Subject: [PATCH 4/7] =?UTF-8?q?REMOVE=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20=EC=A3=BC=EC=84=9D=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/studentProfile/profileDetail.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/studentProfile/profileDetail.jsx b/src/components/studentProfile/profileDetail.jsx index 847c080..0ce587e 100644 --- a/src/components/studentProfile/profileDetail.jsx +++ b/src/components/studentProfile/profileDetail.jsx @@ -255,8 +255,8 @@ export default function ProfileDetail({}) { queryKey: ["profileDetail"], queryFn: async () => { const data = await getProfileDetail(id); - console.log('프로필 상세 API 응답:', data); - console.log('memberResDto:', data.result.memberResDto); + // console.log('프로필 상세 API 응답:', data); + // console.log('memberResDto:', data.result.memberResDto); setUserData(data.result.memberResDto); setUserWorks(data.result.feedSimpleResDtoPage.content) @@ -379,7 +379,7 @@ export default function ProfileDetail({}) { "STUDENT"; // 기본값은 STUDENT const isStudentAccount = accountType === "STUDENT"; const isClubAccount = accountType === "CLUB"; - console.log('최종 계정 타입:', accountType, 'isStudentAccount:', isStudentAccount, 'isClubAccount:', isClubAccount); + // console.log('최종 계정 타입:', accountType, 'isStudentAccount:', isStudentAccount, 'isClubAccount:', isClubAccount); return ( <> From f4e0b23f0381dd06fe3b51598b3b760ef666adb8 Mon Sep 17 00:00:00 2001 From: tldms0507 Date: Mon, 17 Nov 2025 19:09:58 +0900 Subject: [PATCH 5/7] =?UTF-8?q?FEAT=20:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD/=EC=8A=B9=EC=9D=B8=20=EB=B0=8F=20=EA=B1=B0?= =?UTF-8?q?=EC=A0=88/=EC=A1=B0=ED=9A=8C=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/club.js | 49 +++ src/api/mypage.js | 2 - src/assets/images/clubIcon.svg | 3 + src/assets/images/expelIcon.svg | 10 + src/assets/images/outIcon.svg | 4 +- src/components/chat/chatMessage.jsx | 4 +- src/components/mypage/clubList.jsx | 400 ++++++++++++++++++ .../studentProfile/profileDetail.jsx | 45 +- src/pages/mypage.jsx | 21 + 9 files changed, 528 insertions(+), 10 deletions(-) create mode 100644 src/api/club.js create mode 100644 src/assets/images/clubIcon.svg create mode 100644 src/assets/images/expelIcon.svg create mode 100644 src/components/mypage/clubList.jsx diff --git a/src/api/club.js b/src/api/club.js new file mode 100644 index 0000000..05d95b3 --- /dev/null +++ b/src/api/club.js @@ -0,0 +1,49 @@ +import client from "./client"; + + +export async function getClubList(clubId, pageable) { + const response = await client.get(`/api/v1/clubs/${clubId}/members`, { + 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 75c2db9..659126e 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 0000000..db7fef6 --- /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 0000000..ef30bc2 --- /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 398261b..f9d9d22 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 81b88ef..9480cc5 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/mypage/clubList.jsx b/src/components/mypage/clubList.jsx new file mode 100644 index 0000000..36c7c5c --- /dev/null +++ b/src/components/mypage/clubList.jsx @@ -0,0 +1,400 @@ +import { getClubList, getMyClubList, getClubJoinPendingList, patchClubMemberJoinDecision, getClubMemberList, deleteClubWithdraw } from '../../api/club'; +import { useState } from 'react'; +import { UserStore } from '../../store/userStore'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import basicLogoImg from '../../assets/images/basiclogoimg.png'; +import expelIcon from '../../assets/images/expelIcon.svg'; +import AlertModal from '../alertModal'; +import outIcon from '../../assets/images/outIcon.svg'; +import { getLastCategoryName } from '../../utils/categoryUtils'; + + +export default function ClubList() { + const {memberId, roleType} = UserStore(); + const [currentPage, setCurrentPage] = useState(0); + const [totalPages, setTotalPages] = useState(1); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [isEditMode, setIsEditMode] = useState(false); + const [selectedMembers, setSelectedMembers] = useState([]); + const [modalState, setModalState] = useState({ + isOpen: false, + type: null, + title: "", + description: "", + confirmText: "확인", + FalseBtnText: undefined, + onConfirm: null, + onCancel: null + }); + const pageSize = 10; + const pageable = { + page: currentPage, + size: pageSize, + }; + const {data, isLoading, error} = useQuery({ + queryKey: ['clubList', memberId, currentPage], + queryFn: () => getClubList(memberId, pageable), + }); + + const {data: clubMemberList, isLoading: clubMemberListLoading, error: clubMemberListError} = useQuery({ + queryKey: ['clubMemberList', memberId, currentPage], + queryFn: () => getClubMemberList(memberId, pageable), + }); + // console.log(clubMemberList); + const {data: clubJoinPendingList, isLoading: clubJoinPendingListLoading, error: clubJoinPendingListError} = useQuery({ + queryKey: ['clubJoinPendingList', memberId, currentPage], + queryFn: () => getClubJoinPendingList(memberId, pageable), + }); + + const {mutate: handleClubMemberJoinDecision} = useMutation({ + mutationFn: ({ clubId, studentId, decision }) => patchClubMemberJoinDecision(clubId, studentId, decision), + onSuccess: (response, variables) => { + if (response.data && response.data.code === 200) { + const decisionText = variables.decision === 'APPROVE' ? '승인' : '거절'; + setModalState({ + isOpen: true, + type: 'simple', + title: "처리 완료", + description: response.data.result || `${decisionText}이 완료되었습니다.`, + confirmText: "확인", + FalseBtnText: undefined, + onConfirm: () => setModalState(prev => ({ ...prev, isOpen: false })), + onCancel: null + }); + // 목록 새로고침 + queryClient.invalidateQueries(['clubJoinPendingList', memberId, currentPage]); + } + }, + onError: (error) => { + console.error("동아리원 승인/거절 에러:", error); + setModalState({ + isOpen: true, + type: 'simple', + title: "오류 발생", + description: error.response?.data?.message || "처리 중 오류가 발생했습니다.", + confirmText: "확인", + FalseBtnText: undefined, + onConfirm: () => setModalState(prev => ({ ...prev, isOpen: false })), + onCancel: null + }); + } + }); + // console.log(data); + // console.log(clubJoinPendingList); + + const {data: myClubList, isLoading: myClubListLoading, error: myClubListError} = useQuery({ + queryKey: ['myClubList', memberId, currentPage], + queryFn: () => getMyClubList(pageable), + }); + // console.log(myClubList); + + + const handleClubMemberDelete = (selectedMembers) => { + console.log('선택된 멤버:', selectedMembers); + setIsEditMode(false); + setSelectedMembers([]); + } + + const {mutate: handleClubWithdraw} = useMutation({ + mutationFn: (clubId) => deleteClubWithdraw(clubId), + onSuccess: (response) => { + if (response.data && response.data.code === 200) { + setModalState({ + isOpen: true, + type: 'simple', + title: "탈퇴 완료", + description: "탈퇴가 완료되었습니다.", + confirmText: "확인", + FalseBtnText: undefined, + onConfirm: () => { + setModalState(prev => ({ ...prev, isOpen: false })); + // 목록 새로고침 + queryClient.invalidateQueries(['myClubList', memberId, currentPage]); + }, + onCancel: null + }); + } + }, + onError: (error) => { + console.error("동아리 탈퇴 에러:", error); + setModalState({ + isOpen: true, + type: 'simple', + title: "오류 발생", + description: error.response?.data?.message || "탈퇴 중 오류가 발생했습니다.", + confirmText: "확인", + FalseBtnText: undefined, + onConfirm: () => setModalState(prev => ({ ...prev, isOpen: false })), + onCancel: null + }); + } + }); + + const handleClubLeave = (clubId) => { + setModalState({ + isOpen: true, + type: 'confirm', + title: "탈퇴 확인", + description: "정말 동아리를 탈퇴하시겠습니까?", + confirmText: "탈퇴", + FalseBtnText: "취소", + onCancel: () => { + setModalState(prev => ({ ...prev, isOpen: false })); + }, + onConfirm: () => { + setModalState(prev => ({ ...prev, isOpen: false })); + handleClubWithdraw(clubId); + } + }); + } + return ( +
+ {roleType === 'CLUB' && ( + <> +

동아리원 관리

+
+
+
+

동아리원 목록

+ {!isEditMode ? ( + + ) : ( +
+ + + +
+ )} +
+ +
+ {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 0ce587e..cea91a2 100644 --- a/src/components/studentProfile/profileDetail.jsx +++ b/src/components/studentProfile/profileDetail.jsx @@ -8,6 +8,7 @@ import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { getProfileDetail } from "../../api/profile"; import { getFavorite, postFavorite, deleteFavorite } from "../../api/favorite"; +import { postClubMemberJoin } from "../../api/mypage"; import { UserStore } from "../../store/userStore"; import AlertModal from "../alertModal"; import SEO from "../seo"; @@ -126,7 +127,8 @@ const ClubProfileUI = ({ handleDeclareClick, onWorkClick, S3_BUCKET_URL, - basicLogoImg + basicLogoImg, + handleClubMemberJoin }) => { return (
@@ -217,8 +219,9 @@ const ClubProfileUI = ({ )}
{/* 동아리원 파트 */} - {/*
-

동아리 멤버

+
+

동아리 멤버

+
동아리원 프로필 이미지
@@ -226,7 +229,11 @@ const ClubProfileUI = ({

동아리원 1 소개

-
*/} + +
+ +
); }; @@ -243,6 +250,8 @@ 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 fromMemberId = UserStore.getState().memberId; const S3_BUCKET_URL = import.meta.env.VITE_S3_BUCKET_URL; @@ -381,6 +390,22 @@ export default function ProfileDetail({}) { const isClubAccount = accountType === "CLUB"; // console.log('최종 계정 타입:', accountType, 'isStudentAccount:', isStudentAccount, 'isClubAccount:', isClubAccount); + const handleClubMemberJoin = async () => { + 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); + } + } + + return ( <> {userData?.nickname && ( @@ -427,6 +452,7 @@ export default function ProfileDetail({}) { onWorkClick={onWorkClick} S3_BUCKET_URL={S3_BUCKET_URL} basicLogoImg={basicLogoImg} + handleClubMemberJoin={handleClubMemberJoin} /> )} {showLoginModal && ( @@ -464,6 +490,17 @@ export default function ProfileDetail({}) { }} /> )} + {showSuccessModal && ( + { + setShowSuccessModal(false); + }} + /> + )} ); diff --git a/src/pages/mypage.jsx b/src/pages/mypage.jsx index 84df2e4..1349365 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,22 @@ export default function MyPage() { } + if (roleType === 'CLUB') { + roleSpecificMenus.push({ + id: 'clubList', + label: '동아리 목록', + icon: clubIcon + }); + } + return [...baseMenus, ...roleSpecificMenus]; }; const menuItems = renderMenuItems(); return ( + <> +
{/* 모바일 메뉴 버튼 */}
+ ); } \ No newline at end of file From 41805e40bd3ea4026f709457d5d5a218a7b51b73 Mon Sep 17 00:00:00 2001 From: tldms0507 Date: Mon, 17 Nov 2025 19:20:16 +0900 Subject: [PATCH 6/7] =?UTF-8?q?FEAT=20:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EC=97=90=EC=84=9C=20=EB=8F=99=EC=95=84?= =?UTF-8?q?=EB=A6=AC=EB=A9=A4=EB=B2=84=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20&=20=EB=A9=A4=EB=B2=84=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EA=B1=B0=EB=B6=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/club.js | 4 +- src/components/mypage/clubList.jsx | 5 +- .../studentProfile/profileDetail.jsx | 67 ++++++++++++++++--- src/pages/mypage.jsx | 5 ++ 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/api/club.js b/src/api/club.js index 05d95b3..d40b1c4 100644 --- a/src/api/club.js +++ b/src/api/club.js @@ -1,8 +1,8 @@ import client from "./client"; -export async function getClubList(clubId, pageable) { - const response = await client.get(`/api/v1/clubs/${clubId}/members`, { +export async function getClubList(pageable) { + const response = await client.get(`/api/v1/clubs`, { params: pageable, }); return response; diff --git a/src/components/mypage/clubList.jsx b/src/components/mypage/clubList.jsx index 36c7c5c..9ba9618 100644 --- a/src/components/mypage/clubList.jsx +++ b/src/components/mypage/clubList.jsx @@ -33,10 +33,7 @@ export default function ClubList() { page: currentPage, size: pageSize, }; - const {data, isLoading, error} = useQuery({ - queryKey: ['clubList', memberId, currentPage], - queryFn: () => getClubList(memberId, pageable), - }); + const {data: clubMemberList, isLoading: clubMemberListLoading, error: clubMemberListError} = useQuery({ queryKey: ['clubMemberList', memberId, currentPage], diff --git a/src/components/studentProfile/profileDetail.jsx b/src/components/studentProfile/profileDetail.jsx index cea91a2..192dbd4 100644 --- a/src/components/studentProfile/profileDetail.jsx +++ b/src/components/studentProfile/profileDetail.jsx @@ -8,7 +8,7 @@ import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { getProfileDetail } from "../../api/profile"; import { getFavorite, postFavorite, deleteFavorite } from "../../api/favorite"; -import { postClubMemberJoin } from "../../api/mypage"; +import { getClubMemberList, postClubMemberJoin } from "../../api/club"; import { UserStore } from "../../store/userStore"; import AlertModal from "../alertModal"; import SEO from "../seo"; @@ -128,7 +128,8 @@ const ClubProfileUI = ({ onWorkClick, S3_BUCKET_URL, basicLogoImg, - handleClubMemberJoin + handleClubMemberJoin, + clubMemberList }) => { return (
@@ -219,16 +220,31 @@ const ClubProfileUI = ({ )}
{/* 동아리원 파트 */} -
+

동아리 멤버

-
- 동아리원 프로필 이미지 -
-

동아리원 1

-

동아리원 1 소개

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

{member.nickname || '익명'}

+ {member.intro && ( +

{member.intro}

+ )} +
+
+ )) + ) : ( +
동아리원이 없습니다.
+ )}
@@ -252,6 +268,7 @@ export default function ProfileDetail({}) { 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; @@ -391,6 +408,12 @@ export default function ProfileDetail({}) { // 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) { @@ -405,6 +428,29 @@ export default function ProfileDetail({}) { } } + 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 ( <> @@ -453,6 +499,7 @@ export default function ProfileDetail({}) { S3_BUCKET_URL={S3_BUCKET_URL} basicLogoImg={basicLogoImg} handleClubMemberJoin={handleClubMemberJoin} + clubMemberList={clubMemberList} /> )} {showLoginModal && ( diff --git a/src/pages/mypage.jsx b/src/pages/mypage.jsx index 1349365..8ef61ee 100644 --- a/src/pages/mypage.jsx +++ b/src/pages/mypage.jsx @@ -124,6 +124,11 @@ export default function MyPage() { } if (roleType === 'CLUB') { + roleSpecificMenus.push({ + id: 'myFeed', + label: '내 피드', + icon: feedIcon + }); roleSpecificMenus.push({ id: 'clubList', label: '동아리 목록', From e5f4d293b8f98571e25fdc3bdf852a8205239c01 Mon Sep 17 00:00:00 2001 From: tldms0507 Date: Tue, 18 Nov 2025 00:25:31 +0900 Subject: [PATCH 7/7] =?UTF-8?q?FEAT=20:=20=EC=A7=81=EC=A0=91=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=8B=9C=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EC=95=88=EB=9C=A8=EA=B2=8C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.js | 24 ++++++++++++++++-------- src/components/header.jsx | 3 +++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/api/client.js b/src/api/client.js index 4917c0b..3f608a7 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/components/header.jsx b/src/components/header.jsx index b763e46..9927ddf 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -164,6 +164,9 @@ useEffect(() => { }; const toggleLogin = () => { + // 사용자가 직접 로그아웃했음을 표시하는 플래그 설정 + localStorage.setItem("manualLogout", "true"); + UserStore.getState().clearUser(); localStorage.removeItem("isLogin"); localStorage.removeItem("userType");