From a66349fc43d9743762c1bf5036ae8921c8311662 Mon Sep 17 00:00:00 2001 From: Whaleinmilktea <109408848+Whaleinmilktea@users.noreply.github.com> Date: Tue, 23 May 2023 16:38:41 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=B8=EB=B6=80=EC=82=AC=EC=96=91=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20=EB=B0=8F=20CSS=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=A4=91=EC=9E=85=EB=8B=88=EB=8B=A4.=20(#?= =?UTF-8?q?277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 토큰 요청 리팩토링 * 리팩토링 수정 완료 * 병합 전 커밋 * 마이페이지 구현 및 테스트 완료 * 마이페이지 구현 완료 및 리스트 오류 해결 중 * 멤버 목록 조회 * 가입된 멤버조회 구현 * 마이페이지 / 스터디 리스트 페이지 테스트 완료 * 개인 일정만 구현하면 진짜진짜 끝... * 세부사양 구현 완료, 개인 일정 CRUD의 경우 엔드포인트가 확정되면 추후 개선 * 기능구현 완료, 디버깅 및 CSS 구현 중 * 기능구현 완료 및 CSS 구현 중 * 세부사양 구현 완료 및 CSS 수정 중 --- client/README.md | 1 - client/package-lock.json | 9 + client/package.json | 3 +- client/src/App.tsx | 1 - client/src/apis/CalendarApi.ts | 90 +++++ client/src/apis/EduApi.ts | 1 + client/src/apis/MemberApi.ts | 37 +- client/src/apis/StudyGroupApi.ts | 317 ++++++++++-------- client/src/apis/TokenRequestApi.ts | 5 + client/src/components/ProfileImg.tsx | 109 ++++-- client/src/components/WaitingList.tsx | 71 ++-- client/src/components/calendar/Calendar.tsx | 85 +---- .../components/modal/CheckPasswordModal.tsx | 72 ++++ .../components/modal/StudyInfoEditModal.tsx | 142 ++++++-- .../components/modal/UserInfoEditModal.tsx | 14 +- .../studyManage/CandidateManage.tsx | 65 ++++ .../components/studyManage/MemberManage.tsx | 101 ++++-- .../src/components/studyManage/StudyInfo.tsx | 0 client/src/pages/Profile.tsx | 3 +- client/src/pages/ProfileCalendar.tsx | 26 +- client/src/pages/ProfileInfo.tsx | 30 +- client/src/pages/ProfileStudyList.tsx | 65 ++++ client/src/pages/ProfileStudyManage.tsx | 90 ++++- client/src/test/TestPage.tsx | 12 - 24 files changed, 933 insertions(+), 416 deletions(-) delete mode 100644 client/README.md create mode 100644 client/src/apis/CalendarApi.ts create mode 100644 client/src/components/modal/CheckPasswordModal.tsx create mode 100644 client/src/components/studyManage/CandidateManage.tsx delete mode 100644 client/src/components/studyManage/StudyInfo.tsx delete mode 100644 client/src/test/TestPage.tsx diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 8b137891..00000000 --- a/client/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/client/package-lock.json b/client/package-lock.json index b44e90a6..a80029e0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -21,6 +21,7 @@ "dotenv": "^16.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.8.0", "react-modal": "^3.16.1", "react-router-dom": "^6.11.0", "recoil": "^0.7.7", @@ -4518,6 +4519,14 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz", + "integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/client/package.json b/client/package.json index 3658f523..09f0847a 100644 --- a/client/package.json +++ b/client/package.json @@ -23,6 +23,7 @@ "dotenv": "^16.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.8.0", "react-modal": "^3.16.1", "react-router-dom": "^6.11.0", "recoil": "^0.7.7", @@ -49,4 +50,4 @@ "msw": { "workerDirectory": "public" } -} \ No newline at end of file +} diff --git a/client/src/App.tsx b/client/src/App.tsx index 2c27c745..db853793 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,7 +13,6 @@ import ProfileCalendar from "./pages/ProfileCalendar"; import Redirect from "./pages/Redirect"; import useRefreshToken from "./hooks/useRefreshToken"; import Modal from "react-modal"; -import TestPage from "./test/TestPage"; import Home from "./pages/Home"; import HTestPage from "./pages/HTestPage"; diff --git a/client/src/apis/CalendarApi.ts b/client/src/apis/CalendarApi.ts new file mode 100644 index 00000000..7c4e1f7b --- /dev/null +++ b/client/src/apis/CalendarApi.ts @@ -0,0 +1,90 @@ +import { + StudyInfoDto, + getStudyGroupInfo, + getStudyGroupList, +} from "./StudyGroupApi"; + +// ====================== 개인이 속한 스터디의 스케줄을 가져오는 로직 =========================== +// 1. 개인이 속한 스터디 조회 +// 2. 조회 데이터의 id 추출 +// 3. id를 인자로 전달하여 각 스터디의 상세정보를 추출하고, 변수에 담기 +// 4. 변수에 담은 스터디 정보를 fullCalendar 라이브러리에 맞게 맵핑 +// 5. fullCalendar 라이브러리에 전달하여 이벤트 생성 +export interface Event { + id: string; + title: string; + daysOfWeek?: string[]; + startTime: string; + endTime: string; + startRecur: string; + endRecur: string; + description: string; + overlap: boolean; +} + +export const generateStudyEvents = async ( + isLoggedIn: boolean +): Promise => { + // 1. 개인이 속한 스터디 조회 + const myStudyGroups = await getStudyGroupList(); + console.log(myStudyGroups); + + // 2. 조회 데이터의 id 추출 + const studyGroupIds: number[] = []; + // members 배열에서 스터디 그룹의 ID 추출 + for (const member of myStudyGroups.data.members) { + studyGroupIds.push(member.id); + } + + // 3. id를 인자로 전달하여 각 스터디의 상세정보를 추출하고, 변수에 담기 + const studyGroupInfos: StudyInfoDto[] = []; + for (const id of studyGroupIds) { + const studyGroupInfo = await getStudyGroupInfo(id, isLoggedIn); + studyGroupInfos.push(studyGroupInfo); + } + + // 4. 변수에 담은 스터디 정보를 fullCalendar 라이브러리에 맞게 맵핑 + const events: Event[] = studyGroupInfos.map( + (studyGroupInfo: StudyInfoDto) => { + const mappedDaysOfWeek: string[] = studyGroupInfo.daysOfWeek.map( + (day: string) => { + switch (day) { + case "월": + return "1"; // "월" -> 1 + case "화": + return "2"; // "화" -> 2 + case "수": + return "3"; // "수" -> 3 + case "목": + return "4"; // "목" -> 4 + case "금": + return "5"; // "금" -> 5 + case "토": + return "6"; // "토" -> 6 + case "일": + return "0"; // "일" -> 0 + default: + return ""; // handle any other cases if necessary + } + } + ); + + const event: Event = { + id: studyGroupInfo.id.toString(), + title: studyGroupInfo.studyName, + daysOfWeek: mappedDaysOfWeek, + startTime: `${studyGroupInfo.studyTimeStart}:00`, + endTime: `${studyGroupInfo.studyTimeEnd}:00`, + startRecur: studyGroupInfo.studyPeriodStart, + endRecur: studyGroupInfo.studyPeriodEnd, + description: studyGroupInfo.introduction, + overlap: true, + }; + console.log(event); + return event; + } + ); + + // 5. fullCalendar 이벤트 배열 반환 + return events; +}; diff --git a/client/src/apis/EduApi.ts b/client/src/apis/EduApi.ts index 3d8163ab..218ed090 100644 --- a/client/src/apis/EduApi.ts +++ b/client/src/apis/EduApi.ts @@ -7,3 +7,4 @@ export const eduApi: AxiosInstance = axios.create({ export const socialLoginApi = `${ import.meta.env.VITE_APP_API_URL }/oauth2/authorization`; + diff --git a/client/src/apis/MemberApi.ts b/client/src/apis/MemberApi.ts index 93133912..8d79a1f5 100644 --- a/client/src/apis/MemberApi.ts +++ b/client/src/apis/MemberApi.ts @@ -18,8 +18,6 @@ export interface MemberInfoResponseDto { export const getMemberInfo = async (isLoggedIn: boolean) => { // * 로그인 상태가 아닌 경우 에러 발생 if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요."); - - try { // tokenRequestApi를 사용하여 /members 엔드포인트로 GET 요청 전송 const response = await tokenRequestApi.get( "/members" @@ -27,10 +25,6 @@ export const getMemberInfo = async (isLoggedIn: boolean) => { // 응답 데이터 추출 const data = response.data; return data; // 데이터 반환 - } catch (error) { - console.error("유저 정보를 불러오는데 실패했습니다.", error); - throw new Error("유저 정보를 불러오는데 실패했습니다."); // 실패 시 에러 발생 - } }; // =============== 유저 기본정보 업데이트(PATCH) =============== @@ -48,15 +42,10 @@ export const updateMember = async ( if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요."); // 입력 데이터가 없을 경우 에러 발생 if (!data) throw new Error("입력값을 확인해주세요."); - try { + // tokenRequestApi를 사용하여 /members 엔드포인트로 PATCH 요청 전송 console.log("전송되는 데이터:", data); - await tokenRequestApi.patch("/members", {data - : data}); - } catch (error) { - console.error("유저정보를 업데이트 하는데 실패했습니다.", error); - throw new Error("유저정보를 업데이트 하는데 실패했습니다."); // 실패 시 에러 발생 - } + await tokenRequestApi.patch("/members", data); }; // =============== 유저 프로필 사진 업데이트(PATCH) =============== @@ -67,12 +56,7 @@ export interface MemberProfileUpdateImageDto { export const updateMemberProfileImage = async ( data: MemberProfileUpdateImageDto ) => { - try { await tokenRequestApi.patch("/members/profile-image", data); - } catch (error) { - console.error("프로필 사진을 업로드하는데 실패했습니다.", error); - throw new Error("프로필 사진을 업로드하는데 실패했습니다."); - } }; // =============== 유저 자기소개 / 선호하는 사람 업데이트(PATCH) =============== @@ -82,22 +66,13 @@ export interface MemberDetailDto { } // TODO : Member의 자기소개, 선호하는 사람 수정 요청을 보내는 코드 export const updateMemberDetail = async (memberDetailDto: MemberDetailDto) => { - try { await tokenRequestApi.patch("/members/detail", memberDetailDto); - } catch (error) { - console.error("상세정보를 업데이트하는데 실패했습니다.", error); - throw new Error("상세정보를 업데이트하는데 실패했습니다."); - } }; // =============== 유저 탈퇴(DELETE) =============== // TODO : Member의 회원 탈퇴 요청을 보내는 코드 export const deleteMember = async () => { - try { await tokenRequestApi.delete("/members"); - } catch (error) { - throw new Error("회원탈퇴에 실패했습니다."); - } }; // =============== 유저 비밀번호 체크 =============== @@ -105,14 +80,10 @@ export interface MemberPasswordCheckDto { password: string; } // TODO : Member의 비밀번호를 확인하는 코드 -export const checkMemberPassword = async ( +export const checkMemberPassword = ( memberPasswordCheckDto: MemberPasswordCheckDto ) => { - try { - await tokenRequestApi.post("/members/password", memberPasswordCheckDto); - } catch (error) { - throw new Error("비밀번호가 일치하지 않습니다."); - } + tokenRequestApi.post("/members/password", memberPasswordCheckDto); }; // TODO : (Advance) Member의 수정된 사진을 S3에 업로드하는 코드 diff --git a/client/src/apis/StudyGroupApi.ts b/client/src/apis/StudyGroupApi.ts index 8f3eb3b7..a478dfd2 100644 --- a/client/src/apis/StudyGroupApi.ts +++ b/client/src/apis/StudyGroupApi.ts @@ -1,30 +1,62 @@ import axios from "axios"; -import { useRecoilValue } from "recoil"; -import { LogInState } from "../recoil/atoms/LogInState"; import tokenRequestApi from "./TokenRequestApi"; // ====================== 마이 스터디 리스트 조회 (GET) =========================== +export interface StudyGroup { + id: number; + title: string; + tagValues: string[]; +} export interface StudyGroupListDto { + leaders: StudyGroup[]; + members: StudyGroup[]; +} + +export const getStudyGroupList = async () => { + const response = await tokenRequestApi.get( + `/studygroup/myList?approved=true` + ); + return response; +}; + +// ====================== 가입 대기중인 스터디 리스트 조회 (GET) =========================== +export interface WaitingStudyGroupItemDto { id: number; title: string; - tagValues: string[]; } -export const getStudyGroupList = async (): Promise => { - try { - const response = await tokenRequestApi.get( - "/studygroup/myList?approved=false" - ); - const data = response.data; - return data; - } catch (error) { - console.log(error); - throw new Error("스터디 그룹 리스트를 불러오는데 실패했습니다."); - } +export interface WaitingStudyGroupListDto { + beStudys: WaitingStudyGroupItemDto[]; +} + +export const getWaitingStudyGroupList = async (): Promise< + WaitingStudyGroupItemDto[] +> => { + const response = await tokenRequestApi.get( + `/studygroup/myList?approved=false` + ); + const data = response.data; + return data; }; + +// ====================== 스터디원 가입 신청 철회 =========================== +// TODO : StudyGroup의 가입 신청을 철회하는 코드 +export async function cancelStudyGroupApplication( + id: number, + isLoggedIn: boolean +) { + if (!isLoggedIn) throw new Error("로그인 상태를 확인하세요"); + const response = await tokenRequestApi.delete(`/studygroup/${id}/join`); + console.log("해당 그룹에 가입신청을 철회합니다", response); +} + // ====================== 스터디 그룹 정보 조회 (GET) =========================== // TODO : StudyGroup의 정보를 조회할 때 데이터 타입 정의 +export interface StudyTags { + [key: string]: string[]; +} + export interface StudyInfoDto { id: number; studyName: string; @@ -39,21 +71,21 @@ export interface StudyInfoDto { platform: string; introduction: string; isRecruited: boolean; - tags: { - [key: string]: string[]; - }; - leader: { - id: number; - nickName: string; - }; + tags: StudyTags; + leaderNickName: string; + leader: boolean; } // TODO : StudyGroup의 정보를 조회하는 코드 -export async function getStudyGroupInfo(id: number) { - const response = await axios.get( - `${import.meta.env.VITE_APP_API_URL}/studygroup/${id}` - ); - return response.data; +export async function getStudyGroupInfo(id: number, isLoggedIn: boolean) { + if (!isLoggedIn) throw new Error("로그인 상태를 확인하세요"); + const response = await tokenRequestApi.get(`/studygroup/${id}`); + const studyInfo = response.data; + studyInfo.studyTimeStart = studyInfo.studyTimeStart + .split("T")[1] + .substring(0, 5); + studyInfo.studyTimeEnd = studyInfo.studyTimeEnd.split("T")[1].substring(0, 5); + return studyInfo; } // ====================== 스터디 그룹 수정 (PATCH) =========================== @@ -63,44 +95,47 @@ export interface StudyGroupUpdateDto { studyName: string; studyPeriodStart: string; studyPeriodEnd: string; - daysOfWeek: number[]; + daysOfWeek: string[]; studyTimeStart: string; studyTimeEnd: string; memberCountMin: number; memberCountMax: number; platform: string; introduction: string; - tags: { - [key: string]: string; - }; + tags: StudyTags; } -// TODO : StudyGroup의 정보를 수정하는 코드 -export async function updateStudyGroupInfo(data: StudyGroupUpdateDto) { - const isLoggedIn = useRecoilValue(LogInState); +export async function updateStudyGroupInfo( + data: StudyGroupUpdateDto, + isLoggedIn: boolean, + id: number +) { if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요"); - try { - const response = await tokenRequestApi.patch("/studygroup", data); - console.log("성공적으로 스터디 정보를 업데이트 했습니다", response.data); - } catch (error) { - console.error("스터디 정보를 업데이트 하는데 실패했습니다", error); - } + + // studyPeriodStart 및 studyPeriodEnd 값을 ISO 8601 형식으로 변환 + const formattedData = { + ...data, + studyPeriodStart: `${data.studyPeriodStart}T${data.studyTimeStart}:00`, + studyPeriodEnd: `${data.studyPeriodEnd}T${data.studyTimeEnd}:00`, + studyTimeStart: `${data.studyPeriodStart}T${data.studyTimeStart}:00`, + studyTimeEnd: `${data.studyPeriodEnd}T${data.studyTimeEnd}:00`, + }; + + console.log(formattedData); + + const response = await tokenRequestApi.patch( + `/studygroup/${id}`, + formattedData + ); + console.log("성공적으로 스터디 정보를 업데이트 했습니다", response.data); } // ====================== 스터디 그룹 삭제 (DELETE) =========================== // TODO : StudyGroup의 정보를 삭제하는 코드 -export async function deleteStudyGroupInfo(id: number) { - const isLoggedIn = useRecoilValue(LogInState); +export async function deleteStudyGroupInfo(id: number, isLoggedIn: boolean) { if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요"); - try { - const response = await tokenRequestApi.delete(`/studygroup/${id}`); - console.log("스터디가 삭제되었습니다.", response); - } catch (error) { - console.error( - "스터디 정보를 삭제하는데 실패했습니다. 권한을 확인하세요", - error - ); - } + const response = await tokenRequestApi.delete(`/studygroup/${id}`); + console.log("스터디가 삭제되었습니다.", response); } // ====================== 스터디 그룹 모집 상태 수정 (UPDATE) =========================== @@ -112,64 +147,58 @@ interface StudyGroupRecruitmentStatusUpdateDto { // TODO : StudyGroup의 모집 상태를 수정 export async function updateStudyGroupRecruitmentStatus( id: number, - data: StudyGroupRecruitmentStatusUpdateDto + data: StudyGroupRecruitmentStatusUpdateDto, + isLoggedIn: boolean ) { - const isLoggedIn = useRecoilValue(LogInState); if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요"); - try { - const response = await tokenRequestApi.patch(`/studygroup/${id}`, data); - console.log("스터디 모집 상태를 최신화하는데 성공했습니다", response); - } catch { - console.error("스터디 모집 상태를 최신화하는데 실패했습니다", Error); - } + const response = await tokenRequestApi.patch(`/studygroup/${id}`, data); + console.log("스터디 모집 상태를 최신화하는데 성공했습니다", response); } // ====================== 스터디 그룹장의 가입 승인/거절/강제 탈퇴 기능 =========================== // TODO : 스터디 그룹장이 가입을 승인/거절할 때 + 강제 탈퇴 시킬 때, 서버로 보내는 데이터 양식 -interface StudyGroupMemberApprovalDto { - nickname: string; +export interface StudyGroupMemberApprovalDto { + nickName: string; +} + +// TODO: 스터디 그룹장이 가입을 승인/거절할 때 + 강제 탈퇴 시킬 때, 서버로 보내는 데이터 양식 +export interface StudyGroupMemberApprovalDto { + nickName: string; } -// TODO : StudyGroup으로의 가입을 승인하는 코드 +// TODO: StudyGroup으로의 가입을 승인하는 코드 export async function approveStudyGroupApplication( id: number, - data: StudyGroupMemberApprovalDto + nickname: string, + isLoggedIn: boolean ) { - const isLoggedIn = useRecoilValue(LogInState); if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요"); - - try { - const response = await tokenRequestApi.post( - `/studygroup/${id}/candidate`, - data - ); - console.log("해당 회원의 가입을 허가합니다", response); - } catch (error) { - console.error("허가 요청이 실패했습니다", error); - } + const data: StudyGroupMemberApprovalDto = { + nickName: nickname, + }; + const response = await tokenRequestApi.post( + `/studygroup/${id}/candidate`, + data + ); + console.log("해당 회원의 가입을 허가합니다", response); } -// TODO : StudyGroup으로의 가입을 거절하는 코드 +// TODO: StudyGroup으로의 가입을 거절하는 코드 export async function rejectStudyGroupApplication( id: number, - //accessToken: string | null, - data: StudyGroupMemberApprovalDto + nickname: string ) { - //if (!accessToken) throw new Error("Access token is not defined."); - + const data: StudyGroupMemberApprovalDto = { + nickName: nickname, + }; const config = { data, }; - - try { - const response = await tokenRequestApi.delete( - `/studygroup/${id}/candidate`, - config - ); - console.log("가입 거절", response); - } catch (error) { - console.error("예기치 못한 오류로 요청을 처리할 수 없습니다", error); - } + const response = await tokenRequestApi.delete( + `/studygroup/${id}/candidate`, + config + ); + console.log("가입 거절", response); } // TODO : StudyGroup에서 강제 퇴출시키는 코드 @@ -180,57 +209,48 @@ export async function forceExitStudyGroup( const config = { data, }; - - try { - const response = await tokenRequestApi.delete( - `/studygroup/${id}/kick`, - config - ); - console.log("강제 탈퇴에 성공했습니다", response); - } catch (error) { - console.error("강제탈퇴에 실패했습니다. 권한을 확인하세요", error); - } + const response = await tokenRequestApi.delete( + `/studygroup/${id}/kick`, + config + ); + console.log("강제 탈퇴에 성공했습니다", response); } -// ====================== 스터디원 가입 신청 철회 =========================== -// TODO : StudyGroup의 가입 신청을 철회하는 코드 -export async function cancelStudyGroupApplication(id: number) { - const isLoggedIn = useRecoilValue(LogInState); - if (!isLoggedIn) throw new Error("Access token is not defined."); - - try { - const response = await tokenRequestApi.delete(`/studygroup/${id}/join`); - console.log("해당 그룹에 가입신청을 철회합니다", response); - } catch (error) { - console.error("가입 신청 철회에 실패했습니다", error); - } +// TODO : StudyGroup에서 특정 회원에게 스터디장의 권한을 위임하는 코드 +export async function delegateStudyGroupLeader( + id: number, + data: StudyGroupMemberApprovalDto +) { + const config = { + data, + }; + const response = await tokenRequestApi.patch( + `/studygroup/${id}/privileges`, + config + ); + console.log("스터디장 권한 위임에 성공했습니다", response); } // ====================== 회원의 가입 대기 리스트 =========================== // TODO 회원이 스터디에 가입하기 위해 대기하는 리스트를 조회할 때, 서버에서 받은 양식에 대한 타입 정의 -interface StudyGroupMemberWaitingListDto { +export interface StudyGroupMemberWaitingListDto { nickName: [string]; } // TODO 회원이 스터디에 가입하기 위해 대기하는 리스트를 조회하는 코드 -export async function getStudyGroupMemberWaitingList(id: number) { - const isLoggedIn = useRecoilValue(LogInState); +export async function getStudyGroupMemberWaitingList( + id: number, + isLoggedIn: boolean +) { if (!isLoggedIn) throw new Error("로그인 상태를 확인해주세요"); - try { - const response = await tokenRequestApi.get( - `/studygroup/${id}/member?join-false` - ); - console.log("성공적으로 대기 리스트를 호출했습니다", response); - return response.data; - } catch (error) { - console.error( - "예기치 못한 오류로 인해 데이터를 불러오는데 실패했습니다", - error - ); - } + const response = await tokenRequestApi.get( + `/studygroup/${id}/member?join=false` + ); + console.log("성공적으로 대기 리스트를 호출했습니다", response); + console.log(response.data); + return response.data; } - // ====================== 회원 리스트 =========================== // TODO 스터디 그룹에 가입된 회원 리스트 export interface StudyGroupMemberListDto { @@ -240,13 +260,40 @@ export interface StudyGroupMemberListDto { // TODO : StudyGroup에 가입된 멤버 리스트 export async function getStudyGroupMemberList(id: number, isLoggedIn: boolean) { if (!isLoggedIn) throw new Error("Access token is not defined."); - try { - const response = await axios.get( - `${import.meta.env.VITE_APP_API_URL}/studygroup/${id}/member?join=true` - ); - console.log("성공적으로 멤버 목록을 불러왔습니다", response); - return response.data as StudyGroupMemberListDto; - } catch (error) { - console.error("멤버 목록을 불러오는데 실패했습니다", error); - } + const response = await axios.get( + `${import.meta.env.VITE_APP_API_URL}/studygroup/${id}/member?join=true` + ); + if (response === undefined) return; + return response.data as StudyGroupMemberListDto; +} + +// ====================== 스터디에서 탈퇴 =========================== +// TODO : 스터디에서 탈퇴하는 코드 +export async function exitStudyGroup(id: number, isLoggedIn: boolean) { + if (!isLoggedIn) throw new Error("Access token is not defined."); + const response = await tokenRequestApi.delete(`/studygroup/${id}/member`); + console.log("스터디에서 탈퇴했습니다", response); +} + +// ====================== 스터디 그룹 모집상태 변경 =========================== +// TODO : 스터디의 모집 상태를 변경하는 코드 +interface StudyGroupRecruitmentStatusUpdateDto { + state: boolean; +} + +export async function changeStudyGroupRecruitmentStatus( + id: number, + isLoggedIn: boolean, +) { + if (!isLoggedIn) throw new Error("Access token is not defined."); + + const config = { + status : false, + }; + + const response = await tokenRequestApi.patch( + `/studygroup/${id}/status`, + config + ); + console.log("스터디 모집 상태를 변경했습니다", response); } diff --git a/client/src/apis/TokenRequestApi.ts b/client/src/apis/TokenRequestApi.ts index de1f88ac..a3f1c3d2 100644 --- a/client/src/apis/TokenRequestApi.ts +++ b/client/src/apis/TokenRequestApi.ts @@ -14,6 +14,7 @@ tokenRequestApi.interceptors.request.use( (config) => { config.headers = config.headers || {}; if (accessToken) { + console.log(accessToken) config.headers.authorization = `${accessToken}`; } return config; @@ -23,6 +24,7 @@ tokenRequestApi.interceptors.request.use( } ); + const extendAccessToken = async () => { const expirationTime = 4 * 60 * 1000; const timeToExpire = @@ -42,12 +44,15 @@ const extendAccessToken = async () => { const { authorization: newAccessToken } = response.headers; tokenRequestApi.setAccessToken(newAccessToken); console.log("accessToken 갱신됨"); + } catch (error) { console.error("accessToken 갱신 실패:", error); } }, timeToExpire); }; + + tokenRequestApi.setAccessToken = (token): void => { if (token) { accessToken = token; diff --git a/client/src/components/ProfileImg.tsx b/client/src/components/ProfileImg.tsx index 53ddc3de..580f49d5 100644 --- a/client/src/components/ProfileImg.tsx +++ b/client/src/components/ProfileImg.tsx @@ -1,65 +1,82 @@ -import { useState } from "react"; +import { useState, useRef } from "react"; import styled from "styled-components"; -import tokenRequestApi from "../apis/TokenRequestApi"; +import { updateMemberProfileImage } from "../apis/MemberApi"; interface Props { profileImage: string | undefined; } const ProfileImg = ({ profileImage }: Props) => { + const fileInputRef = useRef(null); const [imageUrl, setImageUrl] = useState(profileImage || ""); + const [isEditing, setIsEditing] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); - // TODO refator : if(undefiend || null === basic img) - - // TODO 프로필 이미지를 업로드하여 상태에 담아놓는 코드 - const checkImg = (e: React.ChangeEvent): any => { - const selectedFile: File | undefined = e.target.files?.[0]; - - if (selectedFile) { - // 파일의 유효성 검사 - const allowedTypes: string[] = ["image/png", "image/jpeg", "image/jpg"]; // 파일의 타입은 png, jpeg, jpg만 허용 - if (!allowedTypes.includes(selectedFile.type)) { - alert("프로필 이미지는 png, jpeg, jpg 파일만 업로드 가능합니다."); - return; - } - const maxSize = 3 * 1024 * 1024; // 파일의 사이즈는 3MB를 넘을 수 없음 - if (selectedFile.size > maxSize) { - alert("프로필 이미지는 3MB를 넘을 수 없습니다."); - return; - } + const checkImg = (): void => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + const handleFileChange = (e: React.ChangeEvent): void => { + const file: File | undefined = e.target.files?.[0]; + if (file) { + setSelectedFile(file); const reader: FileReader = new FileReader(); - reader.readAsDataURL(selectedFile); + reader.readAsDataURL(file); reader.onloadend = () => { const base64Image: string = reader.result as string; setImageUrl(base64Image); - console.log(base64Image); // ! 구현 완료 시 삭제되어야 할 코드 }; + setIsEditing(true); } }; - // TODO 업로드 된 이미지를 서버에 저장할 것을 요청하는 코드 - const updateImg = async () => { + const cancelUpload = (): void => { + setSelectedFile(null); + setIsEditing(false); + setImageUrl(profileImage || ""); + }; + + const updateImg = async (): Promise => { try { - await tokenRequestApi.patch("/members/profile-image", { - image: imageUrl, - }); - console.log("프로필 사진을 업로드하는데 성공했습니다."); - console.log(imageUrl); + await updateMemberProfileImage({ profileImage: imageUrl }); + alert("프로필 이미지가 변경되었습니다."); } catch (error) { - console.error("프로필 사진을 업로드하는데 실패했습니다.", error); - throw new Error("프로필 사진을 업로드하는데 실패했습니다."); + alert("프로필 이미지 변경에 실패하였습니다."); } }; - // TODO 리턴문 return ( - Profile image + {!isEditing ? ( + + ) : ( + Profile image + )} + - - + {isEditing && ( + <> + + Cancel + Upload + + + )} + {!isEditing && ( + Change Profile Image + )} ); }; @@ -68,4 +85,24 @@ export default ProfileImg; const ProfileImgWrapper = styled.div``; -const ProfileImgSection = styled.div``; +const ProfileImgSection = styled.div` + width: 150px; + height: 150px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + ` + +const ButtonGroup = styled.div` + display: flex; + justify-content: center; + margin-top: 10px; + +`; + +const UploadButton = styled.button``; + +const CancelButton = styled.button``; + +const ConfirmButton = styled.button``; diff --git a/client/src/components/WaitingList.tsx b/client/src/components/WaitingList.tsx index 339ac5a0..ff54b05b 100644 --- a/client/src/components/WaitingList.tsx +++ b/client/src/components/WaitingList.tsx @@ -1,43 +1,63 @@ import styled from "styled-components"; -import { useEffect } from "react"; -import tokenRequestApi from "../apis/TokenRequestApi"; - -// TODO 서버에서 받아온 데이터를 동적으로 렌더링하여 리스트로 보내는 컴포넌트 // ? 엔드포인트를 알 수 없음 ===> 서버에 추후 문의 -// const WaitingListContents = ( { waitingList }) => { -// return waitingList.map((study) => ( -//
{study.name}
-// )); -// }; +import { useEffect, useState } from "react"; +import { + WaitingStudyGroupItemDto, + getWaitingStudyGroupList, + cancelStudyGroupApplication, +} from "../apis/StudyGroupApi"; +import { GiCancel } from "react-icons/gi"; +import { useRecoilValue } from "recoil"; +import { LogInState } from "../recoil/atoms/LogInState"; const WaitingList = () => { - // const [waitingList, setWaitingList] = useState([]); + const [waitingList, setWaitingList] = useState([]); + const isLoggedIn = useRecoilValue(LogInState); - // TODO 서버에서 리스트를 받아오는 코드 // ? 엔드포인트를 알 수 없음 ===> 서버에 추후 문의 useEffect(() => { const fetchWaitingList = async () => { try { - //const token = localStorage.getItem("accessToken"); - const res = await tokenRequestApi.get(`/waiting-list`); - console.log(res.data); - // setWaitingList(res.data); + const data = await getWaitingStudyGroupList(); + if (data === null) { + return; + } + setWaitingList(data.beStudys); // ! 에러 해결 필요 } catch (error) { - console.error(error); + console.error("스터디 그룹 리스트를 불러오는데 실패했습니다.", error); } }; - fetchWaitingList(); }, []); + const handleCancelButton = async (id: number) => { + try { + await cancelStudyGroupApplication(id, isLoggedIn); + setWaitingList(waitingList.filter((study) => study.id !== id)); + } catch (error) { + console.error("가입 신청 철회에 실패했습니다", error); + } + }; + + const WaitingStudyGroupItem = ({ id, title }: WaitingStudyGroupItemDto) => { + return ( + + {title} + handleCancelButton(id)}> + + + + ); + }; + return ( 신청중인 스터디 - {/* */} - {/* // TODO 원래는 서버에서 보내준 데이터 or 개인 데이터에서 맵핑한 데이터를 동적으로 렌더링 할 예정이지만, 현재 API 명세서에 데이터가 명확하지 않아, 임시로 유사한 동작을 구현 */} -
    -
  • 스터디 이름
  • -
  • 스터디 이름
  • -
  • 스터디 이름
  • -
+ {waitingList.map((study) => ( + + ))}
); }; @@ -46,3 +66,6 @@ export default WaitingList; const WaitingListWrapper = styled.div``; const WaitingListTitle = styled.div``; +const ItemWrapper = styled.div``; +const ItemTitle = styled.div``; +const CancelButton = styled.button``; diff --git a/client/src/components/calendar/Calendar.tsx b/client/src/components/calendar/Calendar.tsx index 14a95dc4..e9a29418 100644 --- a/client/src/components/calendar/Calendar.tsx +++ b/client/src/components/calendar/Calendar.tsx @@ -1,79 +1,25 @@ import FullCalendar from "@fullcalendar/react"; import timeGridPlugin from "@fullcalendar/timegrid"; import { useState, useEffect } from "react"; -import axios from "axios"; -import { v4 as uuidv4 } from "uuid"; +import { generateStudyEvents, Event } from "../../apis/CalendarApi"; -export interface Event { - id: string; - title: string; - allDay: boolean; - start: string; - end: string; - description: string; - overlap: boolean; - extendedProps: { - department: string; - }; - color?: string; -} -export interface ServerEvent { - title: string; - allDay: boolean; - schedules: { - start: string; - end: string; - }[]; - hours: { - start: string; - end: string; - }[]; - description: string; - overlap: string; - extendedProps: { - department: string; - }; - color?: string; -} const Calendar = () => { const [events, setEvents] = useState([]); - // TODO 서버 측 데이터 형식을 클라이언트 측 데이터 형식으로 변환하는 함수 - const transformData = (serverEvent: ServerEvent): Event[] => { - const transformedEvents = serverEvent.schedules.map((schedules, index) => { - return { - id: uuidv4(), - title: serverEvent.title, - allDay: serverEvent.allDay, - start: `${schedules.start.replaceAll(".", "-")}T${ - serverEvent.hours[index].start - }`, - end: `${schedules.end.replaceAll(".", "-")}T${ - serverEvent.hours[index].end - }`, - description: serverEvent.description, - overlap: serverEvent.overlap === "true", - extendedProps: serverEvent.extendedProps, - color: serverEvent.color, - }; - }); - // console.log(transformedEvents); // ! Debug - return transformedEvents; - }; - - // TODO 서버에서 데이터를 받아오는 함수 useEffect(() => { - axios - .get("http://localhost:3001/event") - .then((res) => { - const transformedData = res.data.flatMap(transformData); - setEvents(transformedData); - }) - .catch((error) => console.error(error)); + const fetchEvents = async () => { + try { + const generatedEvents = await generateStudyEvents(true); + setEvents(generatedEvents); + } catch (error) { + console.error(error); + } + }; + + fetchEvents(); }, []); - // TODO 이벤트를 클릭했을 때 발생하는 함수 ===> // TODO 추후 이벤트의 상세 내용을 표현할 예정 - const handleEventClick = (info: any) => { + const handleEventClick = (info: { event: any }) => { alert("clicked"); console.log(info.event); }; @@ -83,13 +29,16 @@ const Calendar = () => { ); }; -export default Calendar; +export default Calendar; \ No newline at end of file diff --git a/client/src/components/modal/CheckPasswordModal.tsx b/client/src/components/modal/CheckPasswordModal.tsx new file mode 100644 index 00000000..f2494184 --- /dev/null +++ b/client/src/components/modal/CheckPasswordModal.tsx @@ -0,0 +1,72 @@ +import styled from "styled-components"; +import Modal from "react-modal"; +import { ChangeEvent, useState } from "react"; + +const customStyles = { + content: { + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + marginRight: "-50%", + transform: "translate(-50%, -50%)", + }, +}; + +interface CheckPasswordModalProps { + isOpen: boolean; + closeModal: () => void; + onPasswordEntered: (enteredPassword: string) => void; +} + +const CheckPasswordModal = ({ + isOpen, + closeModal, + onPasswordEntered, +}: CheckPasswordModalProps) => { + const [modalState, setModalState] = useState(""); + + const handleInputChange = (event: ChangeEvent) => { + event.preventDefault(); + const { value } = event.target; + setModalState(value); + }; + + const handleSubmitClick = () => { + closeModal(); + onPasswordEntered(modalState); + }; + + const handleFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + handleSubmitClick(); + }; + + return ( + <> + + 개인정보 수정 전, 비밀번호를 재확인합니다 +
+ + 확인 + +
+ + ); +}; + +export default CheckPasswordModal; + +const ModalExplain = styled.div``; +const ModalInput = styled.input``; +const ModalButton = styled.button``; diff --git a/client/src/components/modal/StudyInfoEditModal.tsx b/client/src/components/modal/StudyInfoEditModal.tsx index 79cd9dee..b6694b5e 100644 --- a/client/src/components/modal/StudyInfoEditModal.tsx +++ b/client/src/components/modal/StudyInfoEditModal.tsx @@ -2,10 +2,13 @@ import { useState } from "react"; import Modal from "react-modal"; import styled from "styled-components"; import { + StudyInfoDto, updateStudyGroupInfo, StudyGroupUpdateDto, - StudyInfoDto, } from "../../apis/StudyGroupApi"; +import { LogInState } from "../../recoil/atoms/LogInState"; +import { useRecoilValue } from "recoil"; +import { useParams } from "react-router-dom"; const customStyles = { content: { @@ -24,22 +27,48 @@ interface UserInfoEditModalProps { studyInfo: StudyInfoDto | null; } -const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { +const StudyInfoEditModal = ({ + isOpen, + closeModal, + studyInfo, +}: UserInfoEditModalProps) => { const [modalState, setModalState] = useState({ - id: 123, // Replace with the actual study group ID - studyName: "", - studyPeriodStart: "", - studyPeriodEnd: "", - daysOfWeek: [], - studyTimeStart: "", - studyTimeEnd: "", - memberCountMin: 1, // Replace with the actual member count minimum value - memberCountMax: 5, // Replace with the actual member count maximum value - platform: "", - introduction: "", // Replace with the actual introduction value - tags: {}, // Replace with the actual tags object + id: studyInfo?.id || 0, + studyName: studyInfo?.studyName || "", + studyPeriodStart: studyInfo?.studyPeriodStart || "", + studyPeriodEnd: studyInfo?.studyPeriodEnd || "", + daysOfWeek: studyInfo?.daysOfWeek || [], + studyTimeStart: studyInfo?.studyTimeStart || "", + studyTimeEnd: studyInfo?.studyTimeEnd || "", + memberCountMin: studyInfo?.memberCountMin || 0, + memberCountMax: studyInfo?.memberCountMax || 0, + platform: studyInfo?.platform || "", + introduction: studyInfo?.introduction || "", + tags: studyInfo?.tags || {}, }); + const isLoggedIn = useRecoilValue(LogInState); + const { id } = useParams(); + const parsedId = Number(id); + + console.log(modalState); + + const handleCheckboxChange = (event: React.ChangeEvent) => { + const { value, checked } = event.target; + let updatedDaysOfWeek = [...modalState.daysOfWeek]; + + if (checked) { + updatedDaysOfWeek.push(value); + } else { + updatedDaysOfWeek = updatedDaysOfWeek.filter((day) => day !== value); + } + + setModalState((prevState) => ({ + ...prevState, + daysOfWeek: updatedDaysOfWeek, + })); + }; + const handleInputChange = (event: React.ChangeEvent) => { const { name, value } = event.target; setModalState((prevState) => ({ @@ -49,13 +78,8 @@ const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { }; const handleSaveClick = async () => { - // if (accessToken === undefined) { - // alert("권한이 없습니다."); - // return; - // } - try { - await updateStudyGroupInfo(modalState); + await updateStudyGroupInfo(modalState, isLoggedIn, parsedId); closeModal(); } catch (error) { alert("스터디 그룹 정보를 업데이트하는 중에 오류가 발생했습니다."); @@ -69,12 +93,7 @@ const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { return ( <> - +
스터디명 { 스터디 요일 선택
+ + + + +
스터디 시작 시간 @@ -140,6 +209,13 @@ const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { value={modalState.platform} onChange={handleInputChange} /> + Introduction + 저장 @@ -152,7 +228,7 @@ const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { ); }; -export default UserInfoEditModal; +export default StudyInfoEditModal; const ModalExplain = styled.div``; const UserInfoEditInput = styled.input``; diff --git a/client/src/components/modal/UserInfoEditModal.tsx b/client/src/components/modal/UserInfoEditModal.tsx index 53e0020a..d5fbe315 100644 --- a/client/src/components/modal/UserInfoEditModal.tsx +++ b/client/src/components/modal/UserInfoEditModal.tsx @@ -29,36 +29,32 @@ const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { passwordCheck: "", }); - // TODO : form에 입력된 유저의 닉네임, 비밀번호를 상태에 저장하는 코드s const handleInputChange = (event: ChangeEvent) => { const { name, value } = event.target; - setModalState((prevState: any) => ({ + setModalState((prevState) => ({ ...prevState, [name]: value, })); }; - // TODO : Save 버튼을 클릭 시, 유저의 닉네임, 비밀번호를 서버에 PATCH하는 코드 - const handleSaveClick = () => { - // TODO : 새로운 비밀번호와 비밀번호 확인이 일치하는지 확인하는 코드, 일치하지 않으면 에러메시지를 띄워주는 코드 + const handleSaveClick = async () => { if (modalState.password !== modalState.passwordCheck) { alert("새로운 비밀번호와 비밀번호 확인이 서로 일치하지 않습니다."); return; } + try { const updateDto: MemberUpdateDto = { nickName: modalState.nickname, password: modalState.password, }; - console.log(updateDto) - updateMember(isLoggedIn, updateDto); + await updateMember(isLoggedIn, updateDto); // Await the updateMember function call closeModal(); } catch (error) { alert("로그인이 필요합니다."); } }; - // TODO : Cancel 버튼을 클릭 시, 모달을 닫는 코드 const handleCancelClick = () => { closeModal(); }; @@ -69,7 +65,7 @@ const UserInfoEditModal = ({ isOpen, closeModal }: UserInfoEditModalProps) => { isOpen={isOpen} onRequestClose={closeModal} style={customStyles} - contentLabel="Example Modal" + contentLabel="UserInfoEditModal" > 변경할 Nickname diff --git a/client/src/components/studyManage/CandidateManage.tsx b/client/src/components/studyManage/CandidateManage.tsx new file mode 100644 index 00000000..35645ef0 --- /dev/null +++ b/client/src/components/studyManage/CandidateManage.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { LogInState } from "../../recoil/atoms/LogInState"; +import { + approveStudyGroupApplication, + getStudyGroupMemberWaitingList, + rejectStudyGroupApplication, + StudyGroupMemberWaitingListDto, +} from "../../apis/StudyGroupApi"; +import { useParams } from "react-router-dom"; +import { BsCheckCircle, BsFillXCircleFill } from "react-icons/bs"; + +const CandidateManage = () => { + const [waitingList, setWaitingList] = + useState(null); + const { id } = useParams<{ id: string }>(); + const isLoggedIn = useRecoilValue(LogInState); + + console.log(waitingList); + + useEffect(() => { + if (!isLoggedIn) { + throw new Error("로그인 상태를 확인해주세요"); + } + + const fetchWaitingList = async () => { + const data = await getStudyGroupMemberWaitingList(Number(id), isLoggedIn); + setWaitingList(data); + }; + + if (id) { + fetchWaitingList(); + } + }, [id, isLoggedIn]); + + const handleApproveCandidate = async (nickname: string) => { + approveStudyGroupApplication(Number(id), nickname, isLoggedIn); + }; + + const handleDenyCandidate = async (nickname: string) => { + rejectStudyGroupApplication(Number(id), nickname); + }; + + return ( +
+
회원의 가입 대기 리스트
+ <> + {waitingList && + waitingList.nickName.map((nickname, index) => ( +
+ {nickname} + + +
+ ))} + +
+ ); +}; + +export default CandidateManage; diff --git a/client/src/components/studyManage/MemberManage.tsx b/client/src/components/studyManage/MemberManage.tsx index cfab909c..71d5ae80 100644 --- a/client/src/components/studyManage/MemberManage.tsx +++ b/client/src/components/studyManage/MemberManage.tsx @@ -1,50 +1,89 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { LogInState } from "../../recoil/atoms/LogInState"; +import { useParams } from "react-router-dom"; import { - StudyGroupMemberListDto, + StudyGroupMemberApprovalDto, + delegateStudyGroupLeader, + forceExitStudyGroup, getStudyGroupMemberList, } from "../../apis/StudyGroupApi"; -import styled from "styled-components"; -import { useRecoilValue } from "recoil"; -import { LogInState } from "../../recoil/atoms/LogInState"; +import { GiBootKick, GiLaurelCrown } from "react-icons/gi"; -const GroupMemberManage = ({ id }: { id: number }) => { - const [memberList, setMemberList] = useState( - {} as StudyGroupMemberListDto - ); +// TODO: 스터디 그룹에 가입된 회원 리스트 타입 +export interface StudyGroupMemberListDto { + nickName: string[]; +} + +const MemberManage = () => { const isLoggedIn = useRecoilValue(LogInState); + const { id } = useParams<{ id: string }>(); + const [memberList, setMemberList] = useState( + null + ); + + console.log(memberList); useEffect(() => { - const fetchMemberList = async (id: number) => { + // 스터디 그룹 멤버 리스트를 불러오는 함수 + const fetchMemberList = async () => { try { - const data = await getStudyGroupMemberList(id, isLoggedIn); - if (data === undefined) { - return <>텅..; + const response = await getStudyGroupMemberList(Number(id), isLoggedIn); + if (response) { + setMemberList(response); + } else { + setMemberList(null); } - setMemberList(data); } catch (error) { - console.error(error); + console.error("멤버 목록을 불러오는데 실패했습니다", error); } }; - fetchMemberList(id); - }, []); + fetchMemberList(); + }, [id, isLoggedIn]); - console.log(memberList); + // TODO : 스터디 그룹장의 권한을 위임하는 함수 + const handlePrivileges = async (nickname: string) => { + const data: StudyGroupMemberApprovalDto = { + nickName: nickname, + }; + + try { + await delegateStudyGroupLeader(Number(id), data); + console.log("권한 위임에 성공했습니다"); + } catch (error) { + console.error("권한 위임에 실패했습니다", error); + } + }; + + // TODO : 스터디 그룹에서 강제로 퇴출하는 함수 + const handleForcedKicked = async (nickname: string) => { + console.log(nickname); + const data: StudyGroupMemberApprovalDto = { + nickName: nickname, + }; + await forceExitStudyGroup(Number(id), data); + }; return ( - - {memberList && memberList.nickName ? ( - memberList.nickName.map((nickname, index) => ( - {nickname} - )) - ) : ( -
저는 텅텅 비어있고, 멘탈은 탈탈 나가있어요..
- )} -
+ <> +
회원 목록
+ <> + {memberList && + memberList.nickName.map((nickname, index) => ( +
+ {nickname} + + +
+ ))} + + ); }; -export default GroupMemberManage; - -const Wrapper = styled.div``; -const Name = styled.div``; +export default MemberManage; diff --git a/client/src/components/studyManage/StudyInfo.tsx b/client/src/components/studyManage/StudyInfo.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/pages/Profile.tsx b/client/src/pages/Profile.tsx index ed9e283b..2ec5e7d0 100644 --- a/client/src/pages/Profile.tsx +++ b/client/src/pages/Profile.tsx @@ -5,6 +5,7 @@ import ProfileStudyList from "./ProfileStudyList"; import ProfileCalendar from "./ProfileCalendar"; import ProfileStudyManage from "./ProfileStudyManage"; + const Profile = () => { const navigate = useNavigate(); return ( @@ -26,7 +27,7 @@ const Profile = () => { } /> } /> - } /> + } /> } /> diff --git a/client/src/pages/ProfileCalendar.tsx b/client/src/pages/ProfileCalendar.tsx index 119ce59f..5652d4e0 100644 --- a/client/src/pages/ProfileCalendar.tsx +++ b/client/src/pages/ProfileCalendar.tsx @@ -1,11 +1,31 @@ +import styled from "styled-components"; import Calendar from "../components/calendar/Calendar"; const ProfileCalendar = () => { return ( -
- -
+ <> + + + + + + ); }; export default ProfileCalendar; + +const Wrapper = styled.div` + display: flex; + + justify-content: center; + overflow: hidden; +`; +const CalendarWrapper = styled.div` + width: 960px; + height: 90vh; + background-color: #ffffff; + margin-top: 3px; + padding: 10px; + // z-index 조정 필요 : 헤더보다 아래에 위치할 수 있도록 +`; diff --git a/client/src/pages/ProfileInfo.tsx b/client/src/pages/ProfileInfo.tsx index 4a51d5c6..30b3a009 100644 --- a/client/src/pages/ProfileInfo.tsx +++ b/client/src/pages/ProfileInfo.tsx @@ -13,6 +13,7 @@ import { useState, useEffect, ChangeEvent } from "react"; import UserInfoEditModal from "../components/modal/UserInfoEditModal"; import { useRecoilValue } from "recoil"; import { LogInState } from "../recoil/atoms/LogInState"; +import { useNavigate } from "react-router-dom"; const ProfileInfo = () => { const isLoggedIn = useRecoilValue(LogInState); @@ -26,6 +27,7 @@ const ProfileInfo = () => { }); // 멤버 정보 수정 (클라이언트에서 수정된 데이터) const [isIntroduceEdit, setIsIntroduceEdit] = useState(false); + const navigate = useNavigate(); // TODO 최초 페이지 진입 시 유저의 정보를 조회하는 코드 useEffect(() => { @@ -42,6 +44,7 @@ const ProfileInfo = () => { }, [isModalOpen, isLoggedIn]); // TODO Edit 버튼을 클릭 시, 유저의 닉네임, 비밀번호를 수정할 수 있도록 상태를 변경하는 코드 + // 현재 Modal 구현은 완료했으나 비동기 처리로 인해 계속된 오류 발생. 추가적인 최적화 작업 요함 const handleEditClick = async () => { const enteredPassword = prompt( "개인정보 수정 전 비밀번호를 확인해야 합니다." @@ -91,6 +94,9 @@ const ProfileInfo = () => { const handleDeleteClick = async () => { try { await deleteMember(); + alert("회원탈퇴가 완료되었습니다."); + localStorage.clear(); + navigate("/"); } catch (error) { console.error(error); } @@ -98,17 +104,18 @@ const ProfileInfo = () => { return ( - {/* 유저의 프로필 사진이 입력되는 자리 ===> 별도의 컴포넌트로 관리 중! components/ProfileImg */} - - - - {/* 유저의 기본정보가 입력되는 자리 */} - - - - - Edit - + + + + + + + + + Edit + + + {/* 유저의 자기소개와 원하는 유형의 팀원을 정리하는 자리 */} {!isIntroduceEdit ? ( @@ -148,6 +155,7 @@ const ProfileInfo = () => { export default ProfileInfo; const Wrapper = styled.div``; +const ProfileBaseWrapper = styled.div``; const ProfileImage = styled.div``; const ProfileBaseInfo = styled.div``; const IntroduceAndDesired = styled.div``; diff --git a/client/src/pages/ProfileStudyList.tsx b/client/src/pages/ProfileStudyList.tsx index 2da2d458..d22c7733 100644 --- a/client/src/pages/ProfileStudyList.tsx +++ b/client/src/pages/ProfileStudyList.tsx @@ -1,5 +1,69 @@ import { useEffect, useState } from "react"; +import WaitingList from "../components/WaitingList"; +import { + getStudyGroupList, + StudyGroup, + StudyGroupListDto, +} from "../apis/StudyGroupApi"; +import styled from "styled-components"; +import { useNavigate } from "react-router-dom"; + +const ProfileStudyList = () => { + const [studyList, setStudyList] = useState({ + leaders: [], + members: [], + }); + const navigate = useNavigate(); + + useEffect(() => { + const fetchStudyList = async () => { + const { data, status } = await getStudyGroupList(); + if (status < 299) { + setStudyList(data); + } + }; + // response.data도 항상 있음. + fetchStudyList(); + }, []); + + const StudyCard = ({ id, title, tagValues }: StudyGroup) => { + const handleClick = () => { + navigate(`/profile/${id}`); + }; + + return ( + + {title} + {tagValues.join(", ")} + {/* 이미지 표시 로직 추가 */} + + ); + }; + + return ( + <> + +

Leaders

+ {studyList.leaders.map((leader) => ( + + ))} + +

Members

+ {studyList.members.map((member) => ( + + ))} // import styled from "styled-components"; +import WaitingList from "../components/WaitingList";// import styled from "styled-components"; import WaitingList from "../components/WaitingList"; import { getStudyGroupList, StudyGroupListDto } from "../apis/StudyGroupApi"; @@ -87,3 +151,4 @@ export default ProfileStudyList; // height: 100%; // object-fit: cover; // } + diff --git a/client/src/pages/ProfileStudyManage.tsx b/client/src/pages/ProfileStudyManage.tsx index 33282576..370341c7 100644 --- a/client/src/pages/ProfileStudyManage.tsx +++ b/client/src/pages/ProfileStudyManage.tsx @@ -1,29 +1,48 @@ import { useState, useEffect } from "react"; -import { deleteStudyGroupInfo, getStudyGroupInfo, StudyInfoDto } from "../apis/StudyGroupApi"; +import { + changeStudyGroupRecruitmentStatus, + deleteStudyGroupInfo, + exitStudyGroup, + getStudyGroupInfo, + StudyInfoDto, +} from "../apis/StudyGroupApi"; import styled from "styled-components"; import StudyInfoEditModal from "../components/modal/StudyInfoEditModal"; +import { useParams, useNavigate } from "react-router-dom"; +import { useRecoilValue } from "recoil"; +import { LogInState } from "../recoil/atoms/LogInState"; +import MemberManage from "../components/studyManage/MemberManage"; +import CandidateManage from "../components/studyManage/CandidateManage"; -interface ReadStudyInfoProps { - id: 1; -} - -const ProfileStudyManage = ({ id }: ReadStudyInfoProps) => { +const ProfileStudyManage = () => { const [studyInfo, setStudyInfo] = useState(null); const [isModalOpen, setModalOpen] = useState(false); + const { id } = useParams(); + const parsedId = Number(id); + const navigate = useNavigate(); + const isLoggedIn = useRecoilValue(LogInState); + const isRecruiting = studyInfo?.isRecruited; + + console.log(studyInfo); // TODO : 최초 페이지 진입 시 스터디 정보를 조회하는 코드 useEffect(() => { const fetchStudyGroupInfo = async () => { + if (isNaN(parsedId)) { + alert("잘못된 접근입니다"); + navigate("/profile"); + return; + } try { - const studyInfo = await getStudyGroupInfo(id); + const studyInfo = await getStudyGroupInfo(parsedId, isLoggedIn); setStudyInfo(studyInfo); } catch (error) { - console.error("권한이 없습니다"); + console.log(error); } }; fetchStudyGroupInfo(); - }, [id]); + }, [parsedId]); // TODO : 스터디 정보를 수정하는 코드 const handleEditClick = () => { @@ -32,27 +51,59 @@ const ProfileStudyManage = ({ id }: ReadStudyInfoProps) => { // TODO : 스터디 정보를 삭제하는 코드 const handleDeleteClick = async () => { - try { - await deleteStudyGroupInfo(id); - } catch (error) { - console.error(error); - } + await deleteStudyGroupInfo(parsedId, isLoggedIn); + }; + + // TODO : 스터디에서 탈퇴하는 코드 + const handleExitClick = async () => { + await exitStudyGroup(parsedId, isLoggedIn); + }; + + // TODO : 스터디 모집 상태를 수정하는 코드 + const handleRecuitCloseClick = async () => { + await changeStudyGroupRecruitmentStatus(parsedId, isLoggedIn); }; return ( + {!isRecruiting ? ( + + ) : ( +
스터디 모집 완료
+ )}
스터디 명: {studyInfo?.studyName}
스터디 인원: {studyInfo?.memberCountCurrent}
+
스터디장: {studyInfo?.leaderNickName}
스터디 플랫폼: {studyInfo?.platform}
스터디 기간: {studyInfo?.studyPeriodStart} ~ {studyInfo?.studyPeriodEnd}
-
태그:
+
+ 태그: + {studyInfo?.tags && ( + <> + {Object.entries(studyInfo.tags).map(([category, tags]) => ( +
+ {category}: + {tags.map((tag) => ( + {tag} + ))} +
+ ))} + + )} +
+
+ 만남의 날 : 매 주 {studyInfo?.daysOfWeek} {studyInfo?.studyTimeStart} ~{" "} + {studyInfo?.studyTimeEnd} +
+
스터디 소개
{studyInfo?.introduction}
- {/* StudyInfoEditModal */} {isModalOpen && ( { studyInfo={studyInfo} /> )} + + +
); }; export default ProfileStudyManage; -const Wrapper = styled.div``; \ No newline at end of file +const Wrapper = styled.div``; diff --git a/client/src/test/TestPage.tsx b/client/src/test/TestPage.tsx deleted file mode 100644 index 808c9c31..00000000 --- a/client/src/test/TestPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import GroupMemberManage from "../components/studyManage/MemberManage" - -const TestPage = () => { - return ( -
-

Test Page

- -
- ) -} - -export default TestPage \ No newline at end of file