From a524905a0ea37b512b933d5e461d52976cbeb55b Mon Sep 17 00:00:00 2001 From: Park Jinwoo Date: Mon, 20 Nov 2023 21:39:50 +0900 Subject: [PATCH] =?UTF-8?q?[teat]=20main=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20with=20verce?= =?UTF-8?q?l=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(#44): react-query 초기 설정 * feat(#44): 공모전 카테고리 및 전체 조회 * refactor(#44): getCompeitionList와 useCompetitionList 분리 * feat(#44): userId 라우터 설정, HeaderSelected 설정 * feat(#44): 개인 프로필 작성 API 연동 * refactor(#44): 개인 프로필 작성 페이지 서버 연동 * refactor(#44): examples 하나로 통일 * feat(#44): 개인 프로필 작성 완료 모달 * feat(#44): 프로필 작성 api 연동 * feat(#44): Axios headers Authorization 추가 * feat(#44): 유저 추가 프로필 작성 request body 수정 * [feat]수많은 경우의 수의 팀 자세히 보러가기 페이지 개발 완료, api 연동 완료 (#70) * feat(#49) : 팀 자세히 보러가기 1,2,3 경우의 수 완료 * feat(#49) : 팀 자세히 보러가기 4,5 경우의 수 완료 * feat(#49) : 합류 신청하기 모달 전 완료 * feat(#49) : 팀 함류 api 생성 완료 * feat(#49)팀 합류 api 연동 완료 * feat(#49)팀 합류 api 연동 완료 * [feat] 내 팀 - 내가 오픈한 팀 (#72) * chore(#44): react-query 초기 설정 * feat(#44): 공모전 카테고리 및 전체 조회 * refactor(#44): getCompeitionList와 useCompetitionList 분리 * feat(#44): userId 라우터 설정, HeaderSelected 설정 * feat(#44): 개인 프로필 작성 API 연동 * refactor(#44): 개인 프로필 작성 페이지 서버 연동 * refactor(#44): examples 하나로 통일 * feat(#44): 개인 프로필 작성 완료 모달 * feat(#44): 프로필 작성 api 연동 * feat(#44): Axios headers Authorization 추가 * feat(#44): 유저 추가 프로필 작성 request body 수정 * feat(#62): 내 팀 오픈하기 Form 제작 * feat(#62): 팀 오픈하기 취소 모달 * feat(#62): 팀 오픈하기 완료 중 모달 * feat(#62): 모달 content 수정 * feat(#62): 반응형 max-width 제외 * feat(#62): 내팀 - 내가 오픈한 팀 페이지 * fix(#62): vercel 오류 해결 * remove(#62): recommendation 폴더 삭제 * feat(#62): 팀원이 없는 경우 * fix 로그인 로직이 필요한 경우 모달 띄우기 (#74) * fix(#71) : 로그인 로직이 필요한 경우 모달 띄우기 * Fix(#71) : 미사용 변수 제거 * [feat] 활동중인 팀, 지원한 팀 페이지 및 api 연동 완료 (#76) * feat(#75) : 지원한 팀 완료 * feat(#75) : 활동중인 팀 페이지 완료 * fix(#75) : 미사용 변수 제거 * [feat] 팀 오픈하기 API 연동, 유효성 검사, 최종 모달 (#78) * feat(#73): 로직과 다르게 데이터를 입력했을 때 error * feat(#73): 팀 오픈하기 API 연동 * feat(#73): 팀 오픈하기 최종 모달 * [feat] 내 팀 - 활동했던 팀 페이지 API 연동 (#80) * feat(#79): 활동했던 팀 추천사 컴포넌트, 대회명 컴포넌트 * docs(#79): 내가 오픈한 팀 페이지 생성 * feat(#79): 내 팀 - 내가 오픈한 팀 디자인 수정 * feat(#79): 내 팀 - 활동했던 팀 API 연동 * fix(#79): 내팀 - 활동했던 팀 페이지 API 연동 * [feat] 프로필 수정 가능케 함, 추천사 받았는지 여부 체크 (#81) * feat(#77) : 추천사 못받았으면 잠금 * feat(#77) : 받은 추천사가 없으면 프로필이 보이지 않음. * feat(#77) : 프로필 subinfo 구조 변경 * feat(#77) : 프로필 subinfo 수정 반영 * [fix] 버그 수정 및 간단한 수정 (#83) * docs(#82): 공모전 리스트 페이지 검색 박스 background image 변경 * fix(#82): 새로고침 시 내 팀 카테고리 초기화 수정 * [feat] 공모전 상단의 모집 리스트 API 연동 (#86) * remove(#84): 더미데이터 삭제 * feat(#84): 공모전 리스트 상단의 모집중인 팀 리스트 API 연동 * [feat] 계정 설정하기 페이지 개발 완료 (#87) * feat(#85) : 계정정보 수정 로직 완성 * feat(#85) : 계정정보 수정 api 연동 완료 * fix(#85) : useProfile 등 훅 사용시 리렌더링 로직 논의 * fix(#85) : vercel파일 오류 슈정 * test(#88) : wanteam.vercel.app 배포 테스트 --------- Co-authored-by: parkyejin --- .env | 4 +- public/assets/images/common/glasses.svg | 58 +++++ .../assets/images/common/kakaoReviewSend.svg | 66 ++++++ public/assets/images/common/kakaotalk.svg | 14 ++ public/assets/images/common/members.svg | 26 +++ public/assets/images/common/questionmark.svg | 12 + public/assets/images/common/right_arrow.svg | 3 + public/assets/images/common/warning.svg | 18 ++ .../competition_search_background.svg | 4 + public/assets/images/main/speech_bubble3.svg | 70 +++--- public/assets/images/myteam/apply_fire.svg | 4 + .../images/myteam/complete_final_button.svg | 36 +++ public/assets/images/myteam/myteam_end.svg | 9 + public/assets/images/myteam/team_member.svg | 18 ++ src/Router.tsx | 14 +- src/apis/competition/getRecruitingTeam.tsx | 7 + src/apis/contest/postJoinTeam.ts | 13 ++ .../login/postKakaoAccessTokenFromCode.ts | 3 +- src/apis/modify/patchModifyProfile.ts | 20 ++ src/apis/myTeam/getActiveTeam.ts | 9 + src/apis/myTeam/getAppliedTeam.ts | 2 +- src/apis/myTeam/getEndTeam.ts | 7 + src/apis/myTeam/getOpenedTeam.ts | 7 + src/apis/myTeam/postTeamOpen.ts | 10 + .../profile/getIsUserGetExternalReview.ts | 14 ++ src/components/common/Button.tsx | 8 +- src/components/common/Modal.tsx | 22 +- src/components/common/ProfileBoxMember.tsx | 19 +- src/components/common/StarTtile.tsx | 36 +++ .../common/modals/NeedKakaoReviewModal.tsx | 133 +++++++++++ .../common/{ => modals}/NeedLoginModal.tsx | 2 +- .../competitionList/CompetitionListBox.tsx | 1 + .../competitionList/CompetitionRecruiting.tsx | 17 +- .../competitionList/CompetitionSearch.tsx | 10 +- .../competitionList/RecruitingBox.tsx | 26 +-- src/components/contest/RecruitTeamItem.tsx | 10 +- src/components/contest/RecruitTeamList.tsx | 4 +- .../contestTeam/JoinTeamCompleteModal.tsx | 71 ++++++ src/components/contestTeam/JoinTeamModal.tsx | 74 +++++++ .../contestTeam/JoinTeamModalInner.tsx | 47 ++++ .../contestTeam/JoinTeamRefusedModal.tsx | 29 +++ .../contestTeam/JoinTeamRefusedModalInner.tsx | 47 ++++ src/components/contestTeam/TeamMembers.tsx | 100 ++++++--- src/components/header/Header.tsx | 27 ++- src/components/login/KakaoLogin.tsx | 3 +- src/components/main/MainCompetition.tsx | 4 +- src/components/main/MainPopularTeam.tsx | 2 +- src/components/main/PopularTeamBox.tsx | 2 +- src/components/main/RightBox.tsx | 4 +- src/components/modify/SelectInput.tsx | 79 +++++++ src/components/modify/TextAreaInput.tsx | 98 ++++++++ src/components/modify/TextInput.tsx | 77 +++++++ .../myteam/ActivityAreaSelectBox.tsx | 16 +- src/components/myteam/ButtonBox.tsx | 33 ++- src/components/myteam/MyTeamCreateOpen.tsx | 122 +++++++++- src/components/myteam/MyTeamOpen.tsx | 103 --------- .../myteam/active/MyTeamActiveContainer.tsx | 164 ++++++++++++++ .../myteam/active/TeamMemberScrollBox.tsx | 57 +++++ .../myteam/apply/MyTeamApplyContainer.tsx | 209 +++++++++++++++++- src/components/myteam/end/MyTeamEndBox.tsx | 142 ++++++++++++ src/components/myteam/open/FormTitle.tsx | 37 ++++ src/components/myteam/open/MyTeamOpenBox.tsx | 193 ++++++++++++++++ src/components/profile/ProfileInfo.tsx | 22 +- src/components/profile/ProfileKeyword.tsx | 24 +- src/components/profile/ProfileNotReviewed.tsx | 59 +++++ src/components/profile/ProfilePersonality.tsx | 27 ++- src/components/profile/ProfileReview.tsx | 37 ++-- src/components/profile/ProfileSubInfo.tsx | 191 ++++++++++++++-- .../profile/ProfileSubInfoContent.tsx | 46 ---- src/constants/Profile.ts | 35 --- src/constants/competitionList.ts | 81 ------- src/constants/main.ts | 6 +- src/constants/myteam.ts | 2 +- src/hooks/competition/useRecruitingTeam.tsx | 15 ++ src/hooks/contest/useJoinTeam.ts | 31 +++ src/hooks/login/useLoginWithKakaoToken.tsx | 1 + src/hooks/modify/useModifyProfile.ts | 35 +++ src/hooks/myTeam/useActiveTeam.ts | 12 + src/hooks/myTeam/useEndTeam.ts | 12 + src/hooks/myTeam/useOpenedTeam.ts | 12 + src/hooks/myTeam/useTeamOpen.ts | 19 ++ .../profile/useIsUserGetExternalReview.ts | 17 ++ src/hooks/profile/useProfile.ts | 16 +- src/hooks/profile/useTicketNumber.ts | 8 +- src/interface/Contest.ts | 9 + src/interface/Modify.ts | 13 ++ src/interface/MyTeam.ts | 105 ++++++++- src/interface/Profile.ts | 12 + src/keys/competitionKeys.tsx | 4 + src/keys/myteamKeys.tsx | 3 + src/pages/contestTeam/ContestTeam.tsx | 94 +++++++- src/pages/login/Oauth.tsx | 34 +-- src/pages/modify/ProfileModify.tsx | 196 ++++++++++++++++ src/pages/myteam/MyTeamActive.tsx | 24 ++ src/pages/myteam/MyTeamApply.tsx | 50 ++--- src/pages/myteam/MyTeamCreate.tsx | 21 +- src/pages/myteam/MyTeamEnd.tsx | 24 ++ src/pages/myteam/MyTeamOpen.tsx | 24 ++ src/pages/profile/Profile.tsx | 18 ++ src/pages/request/Request.tsx | 5 +- src/recoil/atom.ts | 4 + src/recoil/myteam.ts | 4 + 102 files changed, 3275 insertions(+), 553 deletions(-) create mode 100644 public/assets/images/common/glasses.svg create mode 100644 public/assets/images/common/kakaoReviewSend.svg create mode 100644 public/assets/images/common/kakaotalk.svg create mode 100644 public/assets/images/common/members.svg create mode 100644 public/assets/images/common/questionmark.svg create mode 100644 public/assets/images/common/right_arrow.svg create mode 100644 public/assets/images/common/warning.svg create mode 100644 public/assets/images/competition/competition_search_background.svg create mode 100644 public/assets/images/myteam/apply_fire.svg create mode 100644 public/assets/images/myteam/complete_final_button.svg create mode 100644 public/assets/images/myteam/myteam_end.svg create mode 100644 public/assets/images/myteam/team_member.svg create mode 100644 src/apis/competition/getRecruitingTeam.tsx create mode 100644 src/apis/contest/postJoinTeam.ts create mode 100644 src/apis/modify/patchModifyProfile.ts create mode 100644 src/apis/myTeam/getActiveTeam.ts create mode 100644 src/apis/myTeam/getEndTeam.ts create mode 100644 src/apis/myTeam/getOpenedTeam.ts create mode 100644 src/apis/myTeam/postTeamOpen.ts create mode 100644 src/apis/profile/getIsUserGetExternalReview.ts create mode 100644 src/components/common/StarTtile.tsx create mode 100644 src/components/common/modals/NeedKakaoReviewModal.tsx rename src/components/common/{ => modals}/NeedLoginModal.tsx (96%) create mode 100644 src/components/contestTeam/JoinTeamCompleteModal.tsx create mode 100644 src/components/contestTeam/JoinTeamModal.tsx create mode 100644 src/components/contestTeam/JoinTeamModalInner.tsx create mode 100644 src/components/contestTeam/JoinTeamRefusedModal.tsx create mode 100644 src/components/contestTeam/JoinTeamRefusedModalInner.tsx create mode 100644 src/components/modify/SelectInput.tsx create mode 100644 src/components/modify/TextAreaInput.tsx create mode 100644 src/components/modify/TextInput.tsx delete mode 100644 src/components/myteam/MyTeamOpen.tsx create mode 100644 src/components/myteam/active/MyTeamActiveContainer.tsx create mode 100644 src/components/myteam/active/TeamMemberScrollBox.tsx create mode 100644 src/components/myteam/end/MyTeamEndBox.tsx create mode 100644 src/components/myteam/open/FormTitle.tsx create mode 100644 src/components/myteam/open/MyTeamOpenBox.tsx create mode 100644 src/components/profile/ProfileNotReviewed.tsx delete mode 100644 src/components/profile/ProfileSubInfoContent.tsx create mode 100644 src/hooks/competition/useRecruitingTeam.tsx create mode 100644 src/hooks/contest/useJoinTeam.ts create mode 100644 src/hooks/modify/useModifyProfile.ts create mode 100644 src/hooks/myTeam/useActiveTeam.ts create mode 100644 src/hooks/myTeam/useEndTeam.ts create mode 100644 src/hooks/myTeam/useOpenedTeam.ts create mode 100644 src/hooks/myTeam/useTeamOpen.ts create mode 100644 src/hooks/profile/useIsUserGetExternalReview.ts create mode 100644 src/interface/Modify.ts create mode 100644 src/keys/myteamKeys.tsx create mode 100644 src/pages/modify/ProfileModify.tsx create mode 100644 src/pages/myteam/MyTeamActive.tsx create mode 100644 src/pages/myteam/MyTeamEnd.tsx create mode 100644 src/pages/myteam/MyTeamOpen.tsx diff --git a/.env b/.env index 71e82b4..fe43915 100644 --- a/.env +++ b/.env @@ -1,4 +1,6 @@ -REACT_APP_FRONTEND_BASE_URL = http://localhost:5173 +#REACT_APP_FRONTEND_BASE_URL = http://localhost:5173 +REACT_APP_FRONTEND_BASE_URL = https://wanteam.vercel.app/ + REACT_APP_KAKAO_JS_SDK_KEY = d87912066b8856b33098631a51ed95df VITE_KAKAO_JS_SDK_KEY=d87912066b8856b33098631a51ed95df VITE_KAKAO_CLIENT_ID = 4c6c969c5156dea4d46723ee82b3216e \ No newline at end of file diff --git a/public/assets/images/common/glasses.svg b/public/assets/images/common/glasses.svg new file mode 100644 index 0000000..9cd093a --- /dev/null +++ b/public/assets/images/common/glasses.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/common/kakaoReviewSend.svg b/public/assets/images/common/kakaoReviewSend.svg new file mode 100644 index 0000000..06297b0 --- /dev/null +++ b/public/assets/images/common/kakaoReviewSend.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/common/kakaotalk.svg b/public/assets/images/common/kakaotalk.svg new file mode 100644 index 0000000..6e41767 --- /dev/null +++ b/public/assets/images/common/kakaotalk.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/assets/images/common/members.svg b/public/assets/images/common/members.svg new file mode 100644 index 0000000..1af1b45 --- /dev/null +++ b/public/assets/images/common/members.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/common/questionmark.svg b/public/assets/images/common/questionmark.svg new file mode 100644 index 0000000..dfb59f9 --- /dev/null +++ b/public/assets/images/common/questionmark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/assets/images/common/right_arrow.svg b/public/assets/images/common/right_arrow.svg new file mode 100644 index 0000000..75651a0 --- /dev/null +++ b/public/assets/images/common/right_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/common/warning.svg b/public/assets/images/common/warning.svg new file mode 100644 index 0000000..65bf62f --- /dev/null +++ b/public/assets/images/common/warning.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/competition/competition_search_background.svg b/public/assets/images/competition/competition_search_background.svg new file mode 100644 index 0000000..bdaa0cf --- /dev/null +++ b/public/assets/images/competition/competition_search_background.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/main/speech_bubble3.svg b/public/assets/images/main/speech_bubble3.svg index cbfeb67..f45a7d5 100644 --- a/public/assets/images/main/speech_bubble3.svg +++ b/public/assets/images/main/speech_bubble3.svg @@ -1,68 +1,68 @@ - + - - - - + + + + - - - - - + + + + + - - - - + + + + - - - - - - - + + + + + + + - + - + - + - - + + - + - + - - + + - + - - - - + + + + diff --git a/public/assets/images/myteam/apply_fire.svg b/public/assets/images/myteam/apply_fire.svg new file mode 100644 index 0000000..34967f3 --- /dev/null +++ b/public/assets/images/myteam/apply_fire.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/myteam/complete_final_button.svg b/public/assets/images/myteam/complete_final_button.svg new file mode 100644 index 0000000..6793c90 --- /dev/null +++ b/public/assets/images/myteam/complete_final_button.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/myteam/myteam_end.svg b/public/assets/images/myteam/myteam_end.svg new file mode 100644 index 0000000..714d4df --- /dev/null +++ b/public/assets/images/myteam/myteam_end.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/assets/images/myteam/team_member.svg b/public/assets/images/myteam/team_member.svg new file mode 100644 index 0000000..93043c1 --- /dev/null +++ b/public/assets/images/myteam/team_member.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Router.tsx b/src/Router.tsx index 98b6517..b06daa1 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -16,13 +16,16 @@ import Request from './pages/request/Request'; import Main from './pages/main/Main'; import ScrollToTop from './components/common/ScrollToTop'; import MyTeam from './pages/myteam/MyTeam'; -import MyTeamOpen from './components/myteam/MyTeamOpen'; import ContestTeam from './pages/contestTeam/ContestTeam'; import MyTeamManagement from './pages/myteam/MyTeamManagement'; import MyTeamApply from './pages/myteam/MyTeamApply'; import Modal from './components/common/Modal'; import ExternalReview from './pages/review/ExternalReview'; import MyTeamCreate from './pages/myteam/MyTeamCreate'; +import MyTeamActive from './pages/myteam/MyTeamActive'; +import MyTeamEnd from './pages/myteam/MyTeamEnd'; +import MyTeamOpen from './pages/myteam/MyTeamOpen'; +import ProfileModify from './pages/modify/ProfileModify'; function Router() { return ( @@ -44,19 +47,20 @@ function Router() { } /> } /> + } /> + } /> }> } /> } /> - 활동중인 팀} /> - 활동했던 팀} /> + } /> + } /> } - > + /> - } /> } /> } /> } /> diff --git a/src/apis/competition/getRecruitingTeam.tsx b/src/apis/competition/getRecruitingTeam.tsx new file mode 100644 index 0000000..a06a60a --- /dev/null +++ b/src/apis/competition/getRecruitingTeam.tsx @@ -0,0 +1,7 @@ +import { ResponseRecruitingTeam } from '../../interface/MyTeam'; +import Axios from '../axios'; + +export async function getRecruitingTeam(): Promise { + const { data } = await Axios.get(`/api/teams/recruiting`); + return data; +} diff --git a/src/apis/contest/postJoinTeam.ts b/src/apis/contest/postJoinTeam.ts new file mode 100644 index 0000000..3dd49c9 --- /dev/null +++ b/src/apis/contest/postJoinTeam.ts @@ -0,0 +1,13 @@ +import { AxiosPromise } from 'axios'; +import Axios from '../axios'; + +import { RequestJoinTeam, ResponseJoinTeam } from '../../interface/Contest'; + +export const postJoinTeam = ( + requestData: RequestJoinTeam, +): AxiosPromise => + Axios.post(`/api/teams/apply`, { + teamId: requestData.teamId, + }); + +export default postJoinTeam; diff --git a/src/apis/login/postKakaoAccessTokenFromCode.ts b/src/apis/login/postKakaoAccessTokenFromCode.ts index c09f72d..ebb8b29 100644 --- a/src/apis/login/postKakaoAccessTokenFromCode.ts +++ b/src/apis/login/postKakaoAccessTokenFromCode.ts @@ -2,7 +2,8 @@ import axios from 'axios'; const GRANT_TYPE = 'authorization_code'; const client_id = import.meta.env.VITE_KAKAO_CLIENT_ID; -const redirect_uri = 'http://localhost:5173/login/oauth'; +// const redirect_uri = 'http://localhost:5173/login/oauth'; +const redirect_uri = 'https://wanteam.vercel.app/login/oauth'; const postUrl = 'https://kauth.kakao.com/oauth/token'; const postKakaoAccessTokenFromCode = (accessCode: string) => diff --git a/src/apis/modify/patchModifyProfile.ts b/src/apis/modify/patchModifyProfile.ts new file mode 100644 index 0000000..bcdf7ac --- /dev/null +++ b/src/apis/modify/patchModifyProfile.ts @@ -0,0 +1,20 @@ +import { AxiosPromise } from 'axios'; +import Axios from '../axios'; + +import { + RequestModifyProfile, + ResponseModifyProfile, +} from '../../interface/Modify'; + +export const patchModifyProfile = ( + requestData: RequestModifyProfile, +): AxiosPromise => + Axios.patch(`/api/users/account-info`, { + username: requestData.username, + location: requestData.location, + major: requestData.major, + task: requestData.task, + selfIntroduce: requestData.selfIntroduce, + }); + +export default patchModifyProfile; diff --git a/src/apis/myTeam/getActiveTeam.ts b/src/apis/myTeam/getActiveTeam.ts new file mode 100644 index 0000000..ab3c1b3 --- /dev/null +++ b/src/apis/myTeam/getActiveTeam.ts @@ -0,0 +1,9 @@ +import { AxiosPromise } from 'axios'; +import Axios from '../axios'; + +import { ResponseActiveTeam } from '../../interface/MyTeam'; + +export const getActiveTeam = (): AxiosPromise => + Axios.get(`/api/teams/proceed-team`, {}); + +export default getActiveTeam; diff --git a/src/apis/myTeam/getAppliedTeam.ts b/src/apis/myTeam/getAppliedTeam.ts index 0ab5a7c..8626d8e 100644 --- a/src/apis/myTeam/getAppliedTeam.ts +++ b/src/apis/myTeam/getAppliedTeam.ts @@ -4,6 +4,6 @@ import Axios from '../axios'; import { ResponseAppliedTeam } from '../../interface/MyTeam'; export const getAppliedTeam = (): AxiosPromise => - Axios.get(`/api/team/applied-team`, {}); + Axios.get(`/api/teams/applied-team`, {}); export default getAppliedTeam; diff --git a/src/apis/myTeam/getEndTeam.ts b/src/apis/myTeam/getEndTeam.ts new file mode 100644 index 0000000..5d8e81e --- /dev/null +++ b/src/apis/myTeam/getEndTeam.ts @@ -0,0 +1,7 @@ +import { ResponseEndTeam } from '../../interface/MyTeam'; +import Axios from '../axios'; + +export async function getEndTeam(): Promise { + const { data } = await Axios.get(`/api/teams/worked-team`); + return data; +} diff --git a/src/apis/myTeam/getOpenedTeam.ts b/src/apis/myTeam/getOpenedTeam.ts new file mode 100644 index 0000000..1d0688d --- /dev/null +++ b/src/apis/myTeam/getOpenedTeam.ts @@ -0,0 +1,7 @@ +import { ResponseOpenedTeam } from '../../interface/MyTeam'; +import Axios from '../axios'; + +export async function getOpenedTeam(): Promise { + const { data } = await Axios.get(`/api/teams/opened-myself`); + return data; +} diff --git a/src/apis/myTeam/postTeamOpen.ts b/src/apis/myTeam/postTeamOpen.ts new file mode 100644 index 0000000..ecbc2da --- /dev/null +++ b/src/apis/myTeam/postTeamOpen.ts @@ -0,0 +1,10 @@ +import { RequestTeamOpen } from '../../interface/MyTeam'; +import Axios from '../axios'; + +export async function postTeamOpen( + teamOpenData: RequestTeamOpen, +): Promise { + await Axios.post(`/api/teams/open`, teamOpenData); +} + +export default postTeamOpen; diff --git a/src/apis/profile/getIsUserGetExternalReview.ts b/src/apis/profile/getIsUserGetExternalReview.ts new file mode 100644 index 0000000..2296c76 --- /dev/null +++ b/src/apis/profile/getIsUserGetExternalReview.ts @@ -0,0 +1,14 @@ +import { AxiosPromise } from 'axios'; +import Axios from '../axios'; + +import { + RequestIsUserGetExternalReview, + ResponseIsUserGetExternalReview, +} from '../../interface/Profile'; + +export const getIsUserGetExternalReview = ( + requestData: RequestIsUserGetExternalReview, +): AxiosPromise => + Axios.get(`/api/reviews/non-user/check/${requestData.userId}`, {}); + +export default getIsUserGetExternalReview; diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index 7b63300..19e77af 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -5,8 +5,12 @@ interface ButtonProps { onClick?: () => void; } -const Button = ({ children, onClick }: ButtonProps) => { - return {children}; +const Button = ({ children, onClick, ...rest }: ButtonProps) => { + return ( + + {children} + + ); }; export default Button; diff --git a/src/components/common/Modal.tsx b/src/components/common/Modal.tsx index 6254408..5121af7 100644 --- a/src/components/common/Modal.tsx +++ b/src/components/common/Modal.tsx @@ -1,19 +1,23 @@ -import NeedLoginModal from './NeedLoginModal'; -import { useRecoilState } from 'recoil'; -import { loginModalState } from '../../recoil/atom'; +import NeedLoginModal from './modals/NeedLoginModal'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { loginModalState, needKakaoReviewModalState } from '../../recoil/atom'; +import NeedKakaoReviewModal from './modals/NeedKakaoReviewModal'; const Modal = () => { const [isLoginModalVisible, setIsLoginModalVisible] = useRecoilState(loginModalState); + const isKakaoReviewModalVisible = useRecoilValue(needKakaoReviewModalState); return ( <> - isLoginModalVisible && ( - - ) + {isLoginModalVisible && ( + + )} + {isKakaoReviewModalVisible && } ); }; export default Modal; +NeedKakaoReviewModal; diff --git a/src/components/common/ProfileBoxMember.tsx b/src/components/common/ProfileBoxMember.tsx index 4dcdfd3..f0235ed 100644 --- a/src/components/common/ProfileBoxMember.tsx +++ b/src/components/common/ProfileBoxMember.tsx @@ -1,6 +1,8 @@ import styled from 'styled-components'; import { ProfileBoxProps } from '../../interface/Contest'; import { useNavigate } from 'react-router-dom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { loginInfoState, loginModalState } from '../../recoil/atom'; const ProfileBoxMember: React.FC = ({ hasProfileButton, @@ -11,6 +13,16 @@ const ProfileBoxMember: React.FC = ({ height, }) => { const navigate = useNavigate(); + const loginInfo = useRecoilValue(loginInfoState); + const setLoginModal = useSetRecoilState(loginModalState); + + const handleClick = () => { + if (loginInfo.isLogin) { + navigate(`/profile/${memberInfo.teamMemberId}`); + } else { + setLoginModal(true); + } + }; return ( = ({
{memberInfo.teamMemberTask[0]} {hasProfileButton ? ( - navigate(`/profile/${memberInfo.teamMemberId}`)} - > - 프로필 보기 - + 프로필 보기 ) : ( {memberInfo.teamMemberMajor[0]} )} @@ -80,6 +88,7 @@ const Name = styled.div` ${(props) => props.theme.fonts.heading5}; color: ${(props) => props.theme.colors.gray90}; margin: 0.8rem 0; + white-space: nowrap; /* font-size: 130%; */ `; diff --git a/src/components/common/StarTtile.tsx b/src/components/common/StarTtile.tsx new file mode 100644 index 0000000..29850e2 --- /dev/null +++ b/src/components/common/StarTtile.tsx @@ -0,0 +1,36 @@ +import { styled } from 'styled-components'; + +interface StarTitleProps { + children: React.ReactNode; + style?: React.CSSProperties; +} + +const StarTitle = ({ children, style }: StarTitleProps) => { + return ( + + + {children} + + ); +}; +export default StarTitle; + +const TitleContainer = styled.div` + ${({ theme }) => theme.fonts.heading4}; + color: ${({ theme }) => theme.colors.gray90}; + margin-bottom: 0.8rem; + + display: flex; + align-items: center; + gap: 0.8rem; + + span { + ${({ theme }) => theme.fonts.subtitleM}; + color: ${({ theme }) => theme.colors.primary40}; + } +`; + +const Star = styled.img` + width: 2rem; + height: 2rem; +`; diff --git a/src/components/common/modals/NeedKakaoReviewModal.tsx b/src/components/common/modals/NeedKakaoReviewModal.tsx new file mode 100644 index 0000000..d87ab28 --- /dev/null +++ b/src/components/common/modals/NeedKakaoReviewModal.tsx @@ -0,0 +1,133 @@ +import styled from 'styled-components'; + +import { useRecoilState, useRecoilValue } from 'recoil'; +import { + loginInfoState, + needKakaoReviewModalState, +} from '../../../recoil/atom'; + +import closeSrc from '/assets/images/common/closeButton.svg'; +import kakaoSrc from '/assets/images/common/kakaotalk.svg'; +import reviewSrc from '/assets/images/common/kakaoReviewSend.svg'; +import { kakao } from '../../login/KakaoLogin'; +const NeedKakaoReviewModal = () => { + const [isModalVisible, setIsModalVisible] = useRecoilState( + needKakaoReviewModalState, + ); + const loginUserInfo = useRecoilValue(loginInfoState); + const handleKakaoMessageSend = () => { + kakao.Share.sendCustom({ + templateId: 99541, + templateArgs: { + name: loginUserInfo.data?.name, + userId: loginUserInfo.data?.userId, + }, + // serverCallbackArgs: { + // isSendSuccess: 'no', // 사용자 정의 파라미터 설정 + // }, + }); + }; + return ( + + + setIsModalVisible(false)} /> + + 지금 추천사 요청을 보내보세요! + + { + '함께 활동했던 지인이 추천사를 작성해주면,\n프로필이 업데이트 됩니다.' + } + + + + + ); +}; + +const BlurLayout = styled.div<{ $isModalVisible: boolean }>` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(43, 43, 46, 0.5); + backdrop-filter: blur(8px); + color: ${({ theme }) => theme.colors.white}; + z-index: 999; + + display: ${(props) => (props.$isModalVisible ? 'flex' : 'none')}; + justify-content: center; + align-items: center; +`; +const ModalContainer = styled.div` + position: relative; + min-width: 60rem; + /* height: 36rem; */ + + border-radius: 1.2rem; + border: 1px solid ${({ theme }) => theme.colors.gray20}; + background-color: ${({ theme }) => theme.colors.white}; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + justify-content: space-between; + gap: 2rem; + + //모달창을 벗어나지 않음 + overflow: hidden; + + padding: 7rem; +`; + +const CloseImg = styled.img` + position: absolute; + right: 1.5rem; + top: 1.5rem; + width: 1.5rem; + height: 1.5rem; + cursor: pointer; +`; +const SendImg = styled.img` + width: 20rem; +`; +const Title = styled.div` + ${({ theme }) => theme.fonts.heading2_1}; + color: ${({ theme }) => theme.colors.gray90}; +`; +const Subtitle = styled.div` + ${({ theme }) => theme.fonts.bodyXL}; + color: ${({ theme }) => theme.colors.gray90}; + white-space: break-spaces; + text-align: center; + margin-bottom: 2rem; +`; +const Button = styled.div` + position: relative; + width: 39rem; + height: 6rem; + + ${({ theme }) => theme.fonts.heading5}; + + background-color: #fee500; + color: ${({ theme }) => theme.colors.gray100}; + border-radius: 0.8rem; + + display: flex; + justify-content: center; + align-items: center; + + cursor: pointer; +`; +const KakaoImg = styled.img` + position: absolute; + width: 2.3rem; + + left: 3rem; +`; + +export default NeedKakaoReviewModal; diff --git a/src/components/common/NeedLoginModal.tsx b/src/components/common/modals/NeedLoginModal.tsx similarity index 96% rename from src/components/common/NeedLoginModal.tsx rename to src/components/common/modals/NeedLoginModal.tsx index 5e2b18e..05c17be 100644 --- a/src/components/common/NeedLoginModal.tsx +++ b/src/components/common/modals/NeedLoginModal.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import loginSrc from '/assets/images/common/login-modal.svg'; -import OneSquareButtonModal from './OneSquareButtonModal'; +import OneSquareButtonModal from '../OneSquareButtonModal'; import { useNavigate } from 'react-router-dom'; interface NeedLoginModalProps { diff --git a/src/components/competitionList/CompetitionListBox.tsx b/src/components/competitionList/CompetitionListBox.tsx index 4691ccd..2b9118a 100644 --- a/src/components/competitionList/CompetitionListBox.tsx +++ b/src/components/competitionList/CompetitionListBox.tsx @@ -20,6 +20,7 @@ const CompetitionListBox = () => { <> {competitionList?.data.map((competition) => ( { + const { recruitingTeam } = useRecruitingTeam(); + console.log(recruitingTeam?.data.recruitingTeams); + return ( 지금 모집 중인 팀을 만나보세요. - {recruitingList.map((recruiting) => ( - + {recruitingTeam?.data.recruitingTeams.map((recruitingTeam) => ( + ))} {}} /> @@ -35,4 +43,5 @@ const RecruitingBoxLayout = styled.div` display: flex; gap: 2.5rem; padding: 0 10rem; + width: 100%; `; diff --git a/src/components/competitionList/CompetitionSearch.tsx b/src/components/competitionList/CompetitionSearch.tsx index 9b13b58..16c577f 100644 --- a/src/components/competitionList/CompetitionSearch.tsx +++ b/src/components/competitionList/CompetitionSearch.tsx @@ -13,7 +13,6 @@ const CompetitionSearch = () => { }; return ( - {/* */} {searchButtonList.map((text, index) => ( @@ -25,7 +24,6 @@ const CompetitionSearch = () => { /> ))} - {/* */} ); }; @@ -33,7 +31,11 @@ const CompetitionSearch = () => { export default CompetitionSearch; const CompetitionSearchBox = styled.div` - background: ${({ theme }) => theme.colors.primary40}; + background-image: url('/assets/images/competition/competition_search_background.svg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + width: 100%; padding: 3.5rem 2rem; border-radius: 1.2rem; @@ -52,5 +54,3 @@ const CompetitionButtonLayout = styled.div` justify-content: center; align-items: center; `; - -// const SearchBackgroundImg = styled.img``; diff --git a/src/components/competitionList/RecruitingBox.tsx b/src/components/competitionList/RecruitingBox.tsx index 619e5ce..bb48bfb 100644 --- a/src/components/competitionList/RecruitingBox.tsx +++ b/src/components/competitionList/RecruitingBox.tsx @@ -1,25 +1,20 @@ import { styled } from 'styled-components'; +import { RecruitingTeamData } from '../../interface/MyTeam'; -interface RecruitingProps { - title: string; - name: string; - description: string; - profile: string; +interface RecruitingBoxProps { + recruitingTeam: RecruitingTeamData; } -const RecruitingBox = ({ - title, - name, - description, - profile, -}: RecruitingProps) => { +const RecruitingBox = ({ recruitingTeam }: RecruitingBoxProps) => { return ( - {title} + {recruitingTeam.contesttitle} - - {name} - "{description}" + + {recruitingTeam.teamLeaderName} + + "{recruitingTeam.teamLeaderMessage}" + ); }; @@ -36,6 +31,7 @@ const RecruitingLayout = styled.div` align-items: center; border: 1px solid ${({ theme }) => theme.colors.gray20}; cursor: pointer; + width: 20%; `; const RecruitingHr = styled.hr` diff --git a/src/components/contest/RecruitTeamItem.tsx b/src/components/contest/RecruitTeamItem.tsx index 491a0b8..5eeb01a 100644 --- a/src/components/contest/RecruitTeamItem.tsx +++ b/src/components/contest/RecruitTeamItem.tsx @@ -7,6 +7,8 @@ import { } from '../../interface/Contest'; import ProfileBoxMember from '../common/ProfileBoxMember'; import { useNavigate } from 'react-router-dom'; +import { loginInfoState, loginModalState } from '../../recoil/atom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; // import ProfileBoxLeader from '../common/ProfileBoxLeader'; const RecruitTeamItem = ({ teamData }: { teamData: ContestTeamList }) => { @@ -20,6 +22,12 @@ const RecruitTeamItem = ({ teamData }: { teamData: ContestTeamList }) => { height: 27.6, }; const navigate = useNavigate(); + const loginInfo = useRecoilValue(loginInfoState); + const setLoginModal = useSetRecoilState(loginModalState); + const handleTeamDetailClick = () => { + if (loginInfo.isLogin == true) navigate(`./${teamData.teamId}`); + else setLoginModal(true); + }; return ( @@ -39,7 +47,7 @@ const RecruitTeamItem = ({ teamData }: { teamData: ContestTeamList }) => {
{teamData.leaderMessage} - navigate(`./${teamData.teamId}`)}> + 팀 자세히 보러가기 diff --git a/src/components/contest/RecruitTeamList.tsx b/src/components/contest/RecruitTeamList.tsx index 1d9c54d..44b7813 100644 --- a/src/components/contest/RecruitTeamList.tsx +++ b/src/components/contest/RecruitTeamList.tsx @@ -4,11 +4,11 @@ import { useNavigate, useParams } from 'react-router-dom'; import useContestTeamList from '../../hooks/contest/useContestTeamList'; const RecruitTeamList = () => { + const { contestId } = useParams(); const navigate = useNavigate(); const handleBtnClicked = () => { - navigate('3'); + navigate(`/myteam/create/${contestId}`); }; - const { contestId } = useParams(); const { contestTeamListData } = useContestTeamList(contestId as string); console.log(contestTeamListData?.data.data); return ( diff --git a/src/components/contestTeam/JoinTeamCompleteModal.tsx b/src/components/contestTeam/JoinTeamCompleteModal.tsx new file mode 100644 index 0000000..9b822d6 --- /dev/null +++ b/src/components/contestTeam/JoinTeamCompleteModal.tsx @@ -0,0 +1,71 @@ +import styled from 'styled-components'; + +import glassSrc from '/assets/images/common/glasses.svg'; +import { useNavigate } from 'react-router-dom'; +import OneSquareButtonModal from '../common/OneSquareButtonModal'; + +interface JoinTeamCompleteModalProps { + isModalVisible: boolean; + setIsModalVisible: React.Dispatch>; + userId?: string; +} +const JoinTeamCompleteModal: React.FC = ({ + isModalVisible, + setIsModalVisible, + userId, +}) => { + const navigate = useNavigate(); + const handleButtonClick = () => { + // handleUseTicket.mutate(); + // setIsModalVisible(false); + // window.location.reload(); + setIsModalVisible(false); + navigate(`/myteam/${userId}/apply`); + }; + const handleCloseButtonClick = () => { + setIsModalVisible(false); + navigate(`/myteam/${userId}/apply`); + }; + + return ( + + + + + 팀 합류 신청이 완료되었어요! + + + 최종 합류 여부가 결정되면 이메일로 알림을 보내드려요. + + + + ); +}; +const JoinTeamCompleteModalInner = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1.8rem; + + padding: 5rem; +`; +const JoinTeamCompleteModalimg = styled.img` + width: 12.7rem; +`; +const JoinTeamCompleteModalTitle = styled.div` + ${({ theme }) => theme.fonts.heading4}; + color: ${({ theme }) => theme.colors.primary60}; +`; +const JoinTeamCompleteModalContent = styled.div` + ${({ theme }) => theme.fonts.bodyL}; + color: ${({ theme }) => theme.colors.gray90}; +`; +export default JoinTeamCompleteModal; diff --git a/src/components/contestTeam/JoinTeamModal.tsx b/src/components/contestTeam/JoinTeamModal.tsx new file mode 100644 index 0000000..5d70583 --- /dev/null +++ b/src/components/contestTeam/JoinTeamModal.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react'; +import TwoButtonModal from '../common/TwoButtonModal'; +import JoinTeamModalInner from './JoinTeamModalInner'; +import useJoinTeam from '../../hooks/contest/useJoinTeam'; +import JoinTeamCompleteModal from './JoinTeamCompleteModal'; +import JoinTeamRefusedModal from './JoinTeamRefusedModal'; + +interface JoinTeamModalProps { + isModalVisible: boolean; + setIsModalVisible: React.Dispatch>; + teamId?: string; + userId?: string | number; +} +const JoinTeamModal: React.FC = ({ + isModalVisible, + setIsModalVisible, + teamId, + userId, +}) => { + const handleJoinTeam = useJoinTeam(teamId as string); + const handleLeftButtonClick = () => { + setIsModalVisible(false); + }; + //응답에 따라 모달창을 변경 + const handleRightButtonClick = async () => { + try { + await handleJoinTeam.mutate(); + console.log('Success: A'); + setIsCompleteModalVisible(true); + setIsModalVisible(false); + } catch (error: any) { + if (error.response && error.response.status === 409) { + console.log('Conflict: B 409'); + } else { + console.error('Error:', error); + } + setIsRefusedModalVisible(true); + setIsModalVisible(false); + } + }; + const handleCloseButtonClick = () => { + setIsModalVisible(false); + }; + + const [isCompleteModalVisible, setIsCompleteModalVisible] = useState(false); + const [isRefusedModalVisible, setIsRefusedModalVisible] = useState(false); + return ( + + + + + + ); +}; +export default JoinTeamModal; diff --git a/src/components/contestTeam/JoinTeamModalInner.tsx b/src/components/contestTeam/JoinTeamModalInner.tsx new file mode 100644 index 0000000..0b1fe1f --- /dev/null +++ b/src/components/contestTeam/JoinTeamModalInner.tsx @@ -0,0 +1,47 @@ +import styled from 'styled-components'; + +import glassSrc from '/assets/images/common/glasses.svg'; + +const JoinTeamModalInner = () => { + return ( + + + + 이 팀에 <span>합류</span>를 신청하시겠어요? + + 최종 합류 여부는 팀장의 승인을 거쳐 결정됩니다. + + ); +}; +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + + margin: 5rem 3rem; +`; +const Img = styled.img` + width: 12rem; + height: 10.2rem; +`; +const Title = styled.div` + ${({ theme }) => theme.fonts.heading4}; + + color: ${({ theme }) => theme.colors.gray90}; + span { + color: ${({ theme }) => theme.colors.primary60}; + } + + text-align: center; +`; +const Content = styled.div` + ${({ theme }) => theme.fonts.bodyL}; + + color: ${({ theme }) => theme.colors.gray70}; + + text-align: center; + white-space: break-spaces; +`; +export default JoinTeamModalInner; diff --git a/src/components/contestTeam/JoinTeamRefusedModal.tsx b/src/components/contestTeam/JoinTeamRefusedModal.tsx new file mode 100644 index 0000000..e13eac9 --- /dev/null +++ b/src/components/contestTeam/JoinTeamRefusedModal.tsx @@ -0,0 +1,29 @@ +import OneButtonModal from '../common/OneButtonModal'; +import JoinTeamRefusedModalInner from './JoinTeamRefusedModalInner'; +interface JoinTeamRefusedModalProps { + isModalVisible: boolean; + setIsModalVisible: React.Dispatch>; + userId?: string; +} +const JoinTeamRefusedModal: React.FC = ({ + isModalVisible, + setIsModalVisible, +}) => { + const handleButtonClick = () => { + setIsModalVisible(false); + }; + return ( + + + + ); +}; + +export default JoinTeamRefusedModal; diff --git a/src/components/contestTeam/JoinTeamRefusedModalInner.tsx b/src/components/contestTeam/JoinTeamRefusedModalInner.tsx new file mode 100644 index 0000000..83285a1 --- /dev/null +++ b/src/components/contestTeam/JoinTeamRefusedModalInner.tsx @@ -0,0 +1,47 @@ +import styled from 'styled-components'; + +import warnSrc from '/assets/images/common/warning.svg'; + +const JoinTeamRefusedModalInner = () => { + const TITLE = '같은 공모전에 이미 지원한 팀이 있어요'; + const SUBTITLE = '한 공모전 당 하나의 팀에만 지원할 수 있어요.'; + return ( + + {TITLE} + {SUBTITLE} + + + ); +}; +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2rem; + + margin: 2rem 3rem; +`; + +const Title = styled.div` + ${({ theme }) => theme.fonts.heading3}; + + color: ${({ theme }) => theme.colors.gray90}; + text-align: center; +`; +const Subtitle = styled.div` + ${({ theme }) => theme.fonts.bodyXL}; + color: ${({ theme }) => theme.colors.gray90}; + + text-align: center; + white-space: break-spaces; +`; +const Img = styled.img` + width: 28.2rem; + height: 35.9rem; + + /* border: 1px solid white; */ + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.46); /* 섀도우 설정 */ +`; +export default JoinTeamRefusedModalInner; diff --git a/src/components/contestTeam/TeamMembers.tsx b/src/components/contestTeam/TeamMembers.tsx index cfa3fef..b94d99b 100644 --- a/src/components/contestTeam/TeamMembers.tsx +++ b/src/components/contestTeam/TeamMembers.tsx @@ -2,6 +2,8 @@ import styled from 'styled-components'; import { ProfileBoxProps, ProfileProps } from '../../interface/Contest'; import ProfileBoxMember from '../common/ProfileBoxMember'; +import EmptySrc from '/assets/images/common/members.svg'; + const TeamMembers = ({ memberDatas, leftMember, @@ -14,35 +16,76 @@ const TeamMembers = ({ max?: number; }) => { return ( - - - 팀원들 - - {leftMember}자리 남았어요! - - - - 지금까지 정원 {max}명 중 {cur}명의 팀원이 - 합류했어요. - - - {memberDatas?.map((memberData, index) => { - const teamMemberDataProps: ProfileBoxProps = { - hasProfileButton: false, - isBgColorWhite: false, - hasBorder: true, - memberInfo: memberData, - width: 20, - height: 27.6, - }; - return ; - })} - - + <> +
+ {cur == 1 ? ( + <> + 팀원들 + + + 아직 합류한 팀원이 없어요. + + + ) : ( + + + 팀원들 + + {leftMember}자리 남았어요! + + + + 지금까지 정원 {max}명 중 {cur}명의 팀원이 + 합류했어요. + + + {memberDatas?.map((memberData, index) => { + const teamMemberDataProps: ProfileBoxProps = { + hasProfileButton: true, + isBgColorWhite: false, + hasBorder: true, + memberInfo: memberData, + width: 20, + height: 27.6, + }; + return ; + })} + + + )} + ); }; +const EmptyMember = styled.div` + width: 100%; + height: 31rem; + background-color: ${(props) => props.theme.colors.gray5}; + border: 1px solid ${(props) => props.theme.colors.gray20}; + border-radius: 1rem; + + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 1rem; + ${(props) => props.theme.fonts.subtitleL}; + color: ${(props) => props.theme.colors.gray90}; +`; +const EmptyImg = styled.img` + width: 6rem; +`; + +const Hr = styled.div` + width: 100%; + height: 1px; + border-top: 1px solid ${(props) => props.theme.colors.gray20}; +`; const TeamMembersLayout = styled.div` width: 100%; + + display: flex; + flex-direction: column; + gap: 1.5rem; `; const TeamMembersTitle = styled.div` ${(props) => props.theme.fonts.heading3}; @@ -74,5 +117,10 @@ const TeamMembersLeftInfo = styled.div` color: ${(props) => props.theme.colors.primary60}; } `; -const TeamMembersProfileContainer = styled.div``; +const TeamMembersProfileContainer = styled.div` + display: flex; + align-items: center; + gap: 1.8rem; +`; + export default TeamMembers; diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index bc366d8..777d076 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -1,7 +1,12 @@ import styled from 'styled-components'; import { Link, useNavigate } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; -import { headerSelectedState, loginInfoState } from '../../recoil/atom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { + headerSelectedState, + loginInfoState, + loginModalState, +} from '../../recoil/atom'; +import { useEffect } from 'react'; import logoSrc from '/assets/images/header/wanteam-logo.svg'; import starSrc from '/assets/images/common/star.svg'; @@ -11,10 +16,10 @@ import LoginProfile from './LoginProfile'; const Header = () => { const loginInfo = useRecoilValue(loginInfoState); const headerSelectedIndex = useRecoilValue(headerSelectedState); + const setLoginModal = useSetRecoilState(loginModalState); const navigate = useNavigate(); - // useEffect(() => { - // if (localStorage.getItem('kakaoAccessToken')) setIsLogin(true); - // }, [isLogin]); + useEffect(() => {}, [loginInfo]); + return ( <> @@ -32,13 +37,21 @@ const Header = () => { 공모전 리스트 navigate(`/myteam/${loginInfo.data?.userId}/open`)} + onClick={() => + loginInfo.isLogin + ? navigate(`/myteam/${loginInfo.data?.userId}/open`) + : setLoginModal(true) + } $isSelected={headerSelectedIndex === Headers.myTeam} > 내 팀 navigate(`/profile/${loginInfo.data?.userId}`)} + onClick={() => + loginInfo.isLogin + ? navigate(`/profile/${loginInfo.data?.userId}`) + : setLoginModal(true) + } $isSelected={headerSelectedIndex === Headers.myProfile} > 내 프로필 diff --git a/src/components/login/KakaoLogin.tsx b/src/components/login/KakaoLogin.tsx index 95403e6..459e3c7 100644 --- a/src/components/login/KakaoLogin.tsx +++ b/src/components/login/KakaoLogin.tsx @@ -7,7 +7,8 @@ import { LoginResult, ResponseLogin } from '../../interface/Login'; export const kakao = (window as any).Kakao; export const kakaoAuthorize = () => { - const redirectUri = 'http://localhost:5173/login/oauth'; + // const redirectUri = 'http://localhost:5173/login/oauth'; + const redirectUri = 'https://wanteam.vercel.app/login/oauth'; kakao.Auth.authorize({ redirectUri: `${redirectUri}`, scope: 'profile_nickname,profile_image,account_email,account_email', diff --git a/src/components/main/MainCompetition.tsx b/src/components/main/MainCompetition.tsx index 4877ce1..331cda0 100644 --- a/src/components/main/MainCompetition.tsx +++ b/src/components/main/MainCompetition.tsx @@ -16,7 +16,7 @@ const MainCompetition = () => { {competitionList.map((competition) => ( - + ))} @@ -24,7 +24,7 @@ const MainCompetition = () => { {competitionList.map((competition) => ( - + ))} diff --git a/src/components/main/MainPopularTeam.tsx b/src/components/main/MainPopularTeam.tsx index 2c761b8..5220c24 100644 --- a/src/components/main/MainPopularTeam.tsx +++ b/src/components/main/MainPopularTeam.tsx @@ -12,7 +12,7 @@ const MainPopularTeam = () => { {popularTeam.map((team) => ( - + ))} diff --git a/src/components/main/PopularTeamBox.tsx b/src/components/main/PopularTeamBox.tsx index dd926a2..715af47 100644 --- a/src/components/main/PopularTeamBox.tsx +++ b/src/components/main/PopularTeamBox.tsx @@ -11,7 +11,7 @@ const PopularTeamBox = ({ title, name, content }: PopularTeamBoxProps) => {

{title}


- +

{name}님의 팀

"{content}"

diff --git a/src/components/main/RightBox.tsx b/src/components/main/RightBox.tsx index 5aa4b38..5a4aaaa 100644 --- a/src/components/main/RightBox.tsx +++ b/src/components/main/RightBox.tsx @@ -3,10 +3,10 @@ import { styled } from 'styled-components'; const RightBox = () => { return ( -

+

공모전・대회 공고 확인하고

원하는 팀원 찾으러 가기

-

+
diff --git a/src/components/modify/SelectInput.tsx b/src/components/modify/SelectInput.tsx new file mode 100644 index 0000000..7602472 --- /dev/null +++ b/src/components/modify/SelectInput.tsx @@ -0,0 +1,79 @@ +import styled from 'styled-components'; +import { REGIONS } from '../../constants/Join'; +import { InputDataArray } from '../../interface/Join'; + +const SelectInput = ({ + onChangeFunc, + buttonActiveSetFunc, + index, + value, +}: { + onChangeFunc: any; + buttonActiveSetFunc: any; + index: number; + value?: number; +}) => { + const handleChange = (event: any) => { + onChangeFunc(event); + // console.log(event.target.value); + if (event.target.value > 0) + buttonActiveSetFunc((curr: InputDataArray) => { + const newArr = [...curr]; + newArr[index] = true; + return newArr; + }); + else { + buttonActiveSetFunc((curr: InputDataArray) => { + const newArr = [...curr]; + newArr[index] = false; + return newArr; + }); + } + }; + return ( + + + + + ); +}; +const SelectContainer = styled.div` + width: 100%; + height: 6rem; + + display: flex; + justify-content: center; + align-items: center; + + border-radius: 0.5rem; + border: 1px solid ${(props) => props.theme.colors.gray20}; + background-color: white; + + padding: 1.4rem 2.4rem; +`; + +const Label = styled.label` + width: 12rem; + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.subtitleL}; +`; +const Select = styled.select` + width: 100%; + /* flex: 1; */ + + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.bodyL}; + + border: none; +`; +const Option = styled.option` + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.bodyL}; +`; +export default SelectInput; diff --git a/src/components/modify/TextAreaInput.tsx b/src/components/modify/TextAreaInput.tsx new file mode 100644 index 0000000..db646ed --- /dev/null +++ b/src/components/modify/TextAreaInput.tsx @@ -0,0 +1,98 @@ +import styled from 'styled-components'; +import { InputDataArray, InputProps } from '../../interface/Join'; +import React, { useEffect, useState } from 'react'; + +const TextAreaInput = ({ + inputProps, + onChangeFunc, + buttonActiveSetFunc, + index, + value, +}: { + inputProps: InputProps; + onChangeFunc: any; + buttonActiveSetFunc: any; + index: number; + value?: string; +}) => { + const MAX_LENGTH = 140; + const [text, setText] = useState(''); + useEffect(() => { + if (value !== undefined) { + setText(value); + } + }, [value]); + const handleChange = (event: React.ChangeEvent) => { + if (event.target.value.length > 0) + buttonActiveSetFunc((curr: InputDataArray) => { + const newArr = [...curr]; + newArr[index] = true; + return newArr; + }); + else { + buttonActiveSetFunc((curr: InputDataArray) => { + const newArr = [...curr]; + newArr[index] = false; + return newArr; + }); + } + + setText(event.target.value); + onChangeFunc(event); + }; + return ( + + + + + {text.length}/{MAX_LENGTH} + + + ); +}; +const InputContainer = styled.div` + position: relative; + + width: 100%; + + display: flex; + justify-content: center; + /* align-items: center; */ + + border-radius: 0.5rem; + border: 1px solid ${(props) => props.theme.colors.gray20}; + background-color: white; + + padding: 1.4rem 2.4rem; +`; + +const Label = styled.label` + width: 12rem; + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.subtitleL}; +`; +const Input = styled.textarea` + width: 100%; + min-height: 10rem; + + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.bodyL}; + + border: none; + resize: none; +`; +const LengthCount = styled.div` + position: absolute; + right: 2rem; + bottom: 2rem; + + color: ${(props) => props.theme.colors.gray50}; + ${(props) => props.theme.fonts.buttonXXS}; +`; +export default TextAreaInput; diff --git a/src/components/modify/TextInput.tsx b/src/components/modify/TextInput.tsx new file mode 100644 index 0000000..faef80c --- /dev/null +++ b/src/components/modify/TextInput.tsx @@ -0,0 +1,77 @@ +import styled from 'styled-components'; +import { InputDataArray, InputProps } from '../../interface/Join'; + +const TextInput = ({ + inputProps, + onChangeFunc, + buttonActiveSetFunc, + index, + value, +}: { + inputProps: InputProps; + onChangeFunc: any; + buttonActiveSetFunc: any; + index: number; + value?: string; +}) => { + const handleChange = (event: React.ChangeEvent) => { + if (event.target.value.length > 0) + buttonActiveSetFunc((curr: InputDataArray) => { + const newArr = [...curr]; + newArr[index] = true; + return newArr; + }); + else { + buttonActiveSetFunc((curr: InputDataArray) => { + const newArr = [...curr]; + newArr[index] = false; + return newArr; + }); + } + onChangeFunc(event); + }; + return ( + + + + + ); +}; +const InputContainer = styled.div` + position: relative; + + width: 100%; + height: 6rem; + + display: flex; + justify-content: center; + align-items: center; + + border-radius: 0.5rem; + border: 1px solid ${(props) => props.theme.colors.gray20}; + background-color: white; + + padding: 1.4rem 2.4rem; +`; + +const Label = styled.label` + width: 12rem; + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.subtitleL}; +`; +const Input = styled.input` + width: 100%; + /* flex: 1; */ + + color: ${(props) => props.theme.colors.gray80}; + ${(props) => props.theme.fonts.bodyL}; + + border: none; +`; +export default TextInput; diff --git a/src/components/myteam/ActivityAreaSelectBox.tsx b/src/components/myteam/ActivityAreaSelectBox.tsx index 7af3c85..3265691 100644 --- a/src/components/myteam/ActivityAreaSelectBox.tsx +++ b/src/components/myteam/ActivityAreaSelectBox.tsx @@ -1,12 +1,22 @@ import { styled } from 'styled-components'; import { activityAreaOptions } from '../../constants/myteam'; -const ActivityAreaSelectBox = () => { +interface ActivityAreaSelectBoxProps { + value: number; + onChange: (e: React.ChangeEvent) => void; +} + +const ActivityAreaSelectBox = ({ + value, + onChange, +}: ActivityAreaSelectBoxProps) => { return ( - + {activityAreaOptions.map((option) => ( - + ))} ); diff --git a/src/components/myteam/ButtonBox.tsx b/src/components/myteam/ButtonBox.tsx index 08aeb9c..0956271 100644 --- a/src/components/myteam/ButtonBox.tsx +++ b/src/components/myteam/ButtonBox.tsx @@ -2,8 +2,15 @@ import { useState } from 'react'; import { styled } from 'styled-components'; import TwoButtonModal from '../common/TwoButtonModal'; import { useNavigate, useParams } from 'react-router-dom'; +import { RequestTeamOpen } from '../../interface/MyTeam'; +import { UseTeamOpen } from '../../hooks/myTeam/useTeamOpen'; +import OneSquareButtonModal from '../common/OneSquareButtonModal'; -const ButtonBox = () => { +interface ButtonBoxProps { + teamOpen: RequestTeamOpen; +} + +const ButtonBox = ({ teamOpen }: ButtonBoxProps) => { const [openCancelModal, setOpenCancelModal] = useState(false); const [openCompleteModal, setOpenCompleteModal] = useState(false); const [openCompleteFinalModal, setOpenCompleteFinalModal] = useState(false); @@ -11,9 +18,12 @@ const ButtonBox = () => { const navigate = useNavigate(); openCompleteFinalModal; + const teamOpenMutation = UseTeamOpen(teamOpen); + const handleCompleteModalClick = () => { setOpenCompleteModal(false); setOpenCompleteFinalModal(true); + teamOpenMutation.mutate(); }; return ( @@ -48,6 +58,7 @@ const ButtonBox = () => { )} + {openCompleteModal && ( { )} + + {openCompleteFinalModal && ( + navigate(`/list/${contestId}`), + }} + onCloseClickFunc={() => setOpenCompleteFinalModal(false)} + $isModalVisible={openCompleteFinalModal} + > + + +

팀 오픈이 완료되었어요!

+

지원자들의 프로필을 확인하고 딱 맞는 팀원과 합류하세요.

+
+
+ )} ); diff --git a/src/components/myteam/MyTeamCreateOpen.tsx b/src/components/myteam/MyTeamCreateOpen.tsx index a4b323b..74712b2 100644 --- a/src/components/myteam/MyTeamCreateOpen.tsx +++ b/src/components/myteam/MyTeamCreateOpen.tsx @@ -1,8 +1,62 @@ import { styled } from 'styled-components'; -import FormTitle from './FormTitle'; +import FormTitle from './open/FormTitle'; import ActivityAreaSelectBox from './ActivityAreaSelectBox'; +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; +import { RequestTeamOpen } from '../../interface/MyTeam'; + +interface MyTeamCreateOpenProps { + onTeamOpenChange: (newTeamOpen: RequestTeamOpen) => void; +} + +const MyTeamCreateOpen = ({ onTeamOpenChange }: MyTeamCreateOpenProps) => { + const [recruitmentNumber, setRecruitmentNumber] = useState(0); + const [activityEndDate, setActivityEndDate] = useState(''); + const [activityArea, setActivityArea] = useState(0); + const isRecruitmentNumberValid = recruitmentNumber <= 10; + + const [text1, setText1] = useState(''); + const [text2, setText2] = useState(''); + const [text3, setText3] = useState(''); + const { contestId } = useParams(); + + const isActivityEndDateValid = () => { + const regex = /^\d{4}-\d{2}-\d{2}$/; + if (!activityEndDate) { + return true; + } + return ( + regex.test(activityEndDate) && new Date(activityEndDate) > new Date() + ); + }; + + const teamOpen = { + contestId: contestId, + max: recruitmentNumber, + location: activityArea, + endDate: activityEndDate, + leaderMessage: text1, + notice: text2, + chatLink: text3, + }; + + const updateTeamOpen = (newValues: RequestTeamOpen) => { + const updatedTeamOpen = { ...teamOpen, ...newValues }; + onTeamOpenChange(updatedTeamOpen); + }; + + useEffect(() => { + updateTeamOpen(teamOpen); + }, [ + recruitmentNumber, + activityEndDate, + activityArea, + text1, + text2, + text3, + contestId, + ]); -const MyTeamCreateOpen = () => { return ( @@ -15,21 +69,47 @@ const MyTeamCreateOpen = () => {

- 명 + ) => + setRecruitmentNumber(Number(e.target.value)) + } + $isValid={isRecruitmentNumberValid} + /> + 명

-

10 이하의 숫자만 입력 가능합니다.

+ + 10 이하의 숫자만 입력 가능합니다. +
- + ) => + setActivityArea(Number(e.target.value)) + } + /> - + + ) => + setActivityEndDate(e.target.value) + } + $isValid={isActivityEndDateValid()} + /> + + YYYY-MM-DD와 동일한 형식으로 올바른 날짜를 입력해주세요. + +

활동 종료 날짜에 팀원 모두에게 추천사 작성 링크가 발송됩니다. 팀 활동 종료 후 추천사를 작성할 날짜로 입력해주세요. @@ -47,6 +127,7 @@ const MyTeamCreateOpen = () => { setText1(e.target.value)} /> @@ -58,6 +139,7 @@ const MyTeamCreateOpen = () => { setText2(e.target.value)} /> @@ -69,6 +151,7 @@ const MyTeamCreateOpen = () => { setText3(e.target.value)} /> @@ -121,13 +204,36 @@ const FormInputBox = styled.div` gap: 0.8rem; `; -const RecruitmentInput = styled(Input)` +const RecruitmentInput = styled(Input)<{ $isValid: boolean }>` width: 7rem; margin-right: 0.5rem; + border-color: ${({ $isValid, theme }) => + $isValid ? theme.colors.gray40 : theme.colors.error60}; + color: ${({ $isValid, theme }) => + $isValid ? 'inherit' : theme.colors.error60}; +`; + +const Description = styled.div<{ $isValid: boolean }>` + color: ${({ $isValid, theme }) => + $isValid ? 'inherit' : theme.colors.error60}; +`; + +const ActivityDescription = styled(Description)<{ $isValid: boolean }>` + display: ${({ $isValid }) => ($isValid ? 'none' : 'block')}; +`; + +const ActivityInputBox = styled.div` + display: flex; + align-items: center; + gap: 1rem; `; -const ActivityEndInput = styled(Input)` +const ActivityEndInput = styled(Input)<{ $isValid: boolean }>` width: 20rem; + border-color: ${({ $isValid, theme }) => + $isValid ? theme.colors.gray40 : theme.colors.error60}; + color: ${({ $isValid, theme }) => + $isValid ? 'inherit' : theme.colors.error60}; `; const Hr = styled.hr` diff --git a/src/components/myteam/MyTeamOpen.tsx b/src/components/myteam/MyTeamOpen.tsx deleted file mode 100644 index 1317739..0000000 --- a/src/components/myteam/MyTeamOpen.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { styled } from 'styled-components'; -import Button from '../common/Button'; -import Title from '../common/Title'; -import ProfileBoxMember from '../common/ProfileBoxMember'; - -const teamMember = { - teamMemberId: 1, - teamMemberName: '김민수', - teamMemberImage: '/assets/images/review/profile.svg', - teamMemberTask: ['기획', '디자인'], - teamMemberMajor: ['경영학과', '컴퓨터공학과'], -}; - -const MyTeamOpen = () => { - return ( - - - - 2024 국제 대학생 창업교류전 한국대표 모집 - - - - - - <> - 합류한 팀원들 - - - - - <> - 지원자 - - - - - - - ); -}; - -export default MyTeamOpen; - -const MyTeamOpenContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; - background: ${({ theme }) => theme.colors.primary10}; - padding: 6rem 10rem; - gap: 4rem; -`; - -const CompetitionBox = styled.div` - display: flex; - flex-direction: column; - align-items: center; - width: 25rem; - gap: 1.5rem; -`; - -const TeamMemberBox = styled.div` - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -`; - -const TeamMembers = styled.div` - border-radius: 8px; - background: ${({ theme }) => theme.colors.white}; - padding: 2rem; - - display: flex; - gap: 1.5rem; - overflow-x: scroll; -`; - -const CompetitionTitle = styled.div` - ${({ theme }) => theme.fonts.subtitleXL}; - color: ${({ theme }) => theme.colors.gray90}; -`; - -const CompetitionImg = styled.img` - width: 100%; - height: 31.7rem; - object-fit: cover; - border-radius: 8px; - border: 1px solid ${({ theme }) => theme.colors.gray40}; -`; diff --git a/src/components/myteam/active/MyTeamActiveContainer.tsx b/src/components/myteam/active/MyTeamActiveContainer.tsx new file mode 100644 index 0000000..6200470 --- /dev/null +++ b/src/components/myteam/active/MyTeamActiveContainer.tsx @@ -0,0 +1,164 @@ +import styled from 'styled-components'; +import { ActiveTeamData } from '../../../interface/MyTeam'; +import starSrc from '/assets/images/common/star.svg'; +import TeamMemberScrollBox from './TeamMemberScrollBox'; + +const MyTeamActiveContainer: React.FC = (props) => { + return ( + + + + {props.contestTitle} + + + + + 카카오톡 오픈채팅방 링크 : + {props.chatLink} + + + + 팀원 : + {props.memberSize}명 + {' '} + + 활동 지역 : + {props.location} + {' '} + + 활동 종료 예정일 : + {props.endDate} + {' '} + +

팀장의 한마디 :
+ {props.leaderMessage} + {' '} + +
모집 공고 :
+ {props.notice} +
+ + + + + + 합류한 팀원들 + 총 {props.memberSize}명 + + + + ); +}; +const Layout = styled.div` + width: 100%; + /* height: 80.3rem; */ + background-color: ${(props) => props.theme.colors.primary10}; + + border: 1px solid ${(props) => props.theme.colors.primary10}; + border-radius: 1.4rem; + + display: flex; + flex-direction: column; + gap: 2rem; + + padding: 3rem 4rem; +`; +const ContestInfoContainer = styled.div` + display: flex; + justify-content: center; + /* align-items: center; */ + gap: 2rem; +`; +//공모전 제목 및 이미지 박스 +const TitleBox = styled.div` + width: 17rem; + + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: center; + align-items: center; +`; +const Title = styled.div` + ${(props) => props.theme.fonts.subtitleS}; + color: ${(props) => props.theme.colors.gray90}; + width: 100%; +`; +const ContestImg = styled.img` + width: 16.9rem; + /* height: 23.1rem; */ + border-radius: 0.5rem; +`; + +// 공모전 링크 및 정보 박스 +const ContentBox = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: center; + align-items: center; + + flex-grow: 1; +`; +const KakaotalkLink = styled.div` + width: 100%; + + ${(props) => props.theme.fonts.subtitleM}; + color: ${(props) => props.theme.colors.white}; + background-color: ${(props) => props.theme.colors.primary40}; + + display: flex; + /* justify-content: center; */ + align-items: center; + + border-radius: 1.2rem; + border: 1px soild ${(props) => props.theme.colors.gray20}; + + padding: 1.3rem 2.8rem; + a { + text-decoration: underline; + } +`; +const TeamInfoBox = styled.div` + width: 100%; + + display: flex; + flex-direction: column; + justify-content: center; + /* align-items: center; */ + gap: 0.7rem; + + border-radius: 0.8rem; + background-color: ${(props) => props.theme.colors.white}; + + padding: 2.1rem 2.5rem; +`; +const TeamInfoItem = styled.div` + ${(props) => props.theme.fonts.bodyM}; + color: ${(props) => props.theme.colors.gray90}; + + span, + div { + ${(props) => props.theme.fonts.subtitleM}; + } +`; +const MemberTitle = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; + & span:nth-child(2) { + ${(props) => props.theme.fonts.heading4}; + color: ${(props) => props.theme.colors.primary90}; + } + & span:nth-child(3) { + ${(props) => props.theme.fonts.subtitleL}; + color: ${(props) => props.theme.colors.primary40}; + } +`; +const StarImg = styled.img` + width: 2.8rem; +`; +export default MyTeamActiveContainer; diff --git a/src/components/myteam/active/TeamMemberScrollBox.tsx b/src/components/myteam/active/TeamMemberScrollBox.tsx new file mode 100644 index 0000000..b8cdb84 --- /dev/null +++ b/src/components/myteam/active/TeamMemberScrollBox.tsx @@ -0,0 +1,57 @@ +import styled from 'styled-components'; +import { ProfileBoxProps, ProfileProps } from '../../../interface/Contest'; +import ProfileBoxMember from '../../common/ProfileBoxMember'; +import { Role } from '../../contest/RecruitTeamItem'; +interface TeamMemberScrollBoxProps { + teamLeaderInfo: ProfileProps; + teamMembersInfo: ProfileProps[]; + width?: number; +} +const TeamMemberScrollBox: React.FC = (props) => { + return ( + + + 팀장 + + + {props.teamMembersInfo.map((memberData) => { + const profileProps: ProfileBoxProps = { + hasBorder: true, + hasProfileButton: true, + isBgColorWhite: true, + memberInfo: memberData, + }; + return ; + })} + + ); +}; +const Layout = styled.div` + display: flex; + gap: 2.4rem; + + overflow-x: scroll; + + padding: 20px 0; + + &::-webkit-scrollbar { + width: 7px; + height: 7px; + border-radius: 6px; + /* background: ${(props) => props.theme.colors.gray20}; */ + } + &::-webkit-scrollbar-thumb { + background: ${(props) => props.theme.colors.primary60}; + border-radius: 6px; + } +`; +const TeamLeaderTmpBox = styled.div` + position: relative; + ${(props) => props.theme.fonts.subtitleS}; +`; +export default TeamMemberScrollBox; diff --git a/src/components/myteam/apply/MyTeamApplyContainer.tsx b/src/components/myteam/apply/MyTeamApplyContainer.tsx index acb7760..798214e 100644 --- a/src/components/myteam/apply/MyTeamApplyContainer.tsx +++ b/src/components/myteam/apply/MyTeamApplyContainer.tsx @@ -1,14 +1,57 @@ import styled from 'styled-components'; import { AppliedTeamData } from '../../../interface/MyTeam'; +import ProfileBoxMember from '../../common/ProfileBoxMember'; +import { ProfileBoxProps } from '../../../interface/Contest'; +import { Role } from '../../contest/RecruitTeamItem'; +import { useNavigate } from 'react-router-dom'; const MyTeamApplyContainer: React.FC = (props) => { - props; + const teamLeaderBoxProps: ProfileBoxProps = { + hasBorder: true, + hasProfileButton: false, + isBgColorWhite: false, + memberInfo: props.leaderInfo, + // width: 20, + // height: 22.7, + }; + const navigate = useNavigate(); return ( - - {/* */} + {props.contestTitle} + + + + 팀장 + + + navigate(`/list/${props.contestId}/${props.teamId}`)} + > + 자세히 보러가기 + + + + + 팀 합류 여부 : + + {props.status} + + + + 팀장의 한 마디 +
+ + {props.leaderMessage} + + + 모집 현황 : {props.cur}명 / {props.max}명{' '} + + 활동 지역 : {props.location} + 활동 종료 예정일 : {props.endDate} + + ); }; @@ -16,16 +59,164 @@ const Layout = styled.div` width: 100%; height: 35.3rem; background-color: ${(props) => props.theme.colors.primary10}; + + border: 1px solid ${(props) => props.theme.colors.primary10}; + border-radius: 1.4rem; + + display: flex; + justify-content: center; + /* align-items: center; */ + gap: 2rem; + + padding: 3rem 4rem; +`; +const TitleBox = styled.div` + width: 17rem; + + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: center; + align-items: center; `; -const TitleBox = styled.div``; const Title = styled.div` ${(props) => props.theme.fonts.subtitleS}; color: ${(props) => props.theme.colors.gray90}; width: 100%; - height: 35.3rem; `; -// const ContestImg = styled.img` -// width: 16.9rem; -// height: 23.1rem; -// `; +const ContestImg = styled.img` + width: 16.9rem; + /* height: 23.1rem; */ + border-radius: 0.5rem; +`; + +//팀장 컴포넌트 박스 +const TeamLeaderBox = styled.div` + width: 17rem; + /* height: 100%; */ + + /* flex-grow: 1; */ + + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem; + + margin-top: 2rem; +`; +const TeamLeaderEmptyBox = styled.div` + position: relative; + ${(props) => props.theme.fonts.subtitleS}; +`; +const TeamInfoButton = styled.button` + height: 3rem; + + color: ${(props) => props.theme.colors.white}; + background-color: ${(props) => props.theme.colors.primary40}; + border-radius: 0.8rem; + + display: flex; + justify-content: center; + align-items: center; +`; + +//팀 정보 박스 +const TeamInfoBox = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + /* align-items: center; */ + gap: 1rem; + + flex-grow: 1; + + ${(props) => props.theme.fonts.subtitleM} + border: 1px solid ${(props) => props.theme.colors.gray20}; + border-radius: 0.8rem; + + padding: 2.7rem 3.6rem; + margin-top: 2rem; +`; +const TeamJoinStatus = styled.div` + display: flex; + align-items: center; + + gap: 0.5rem; + /* border-radius: 0.7rem; */ +`; +const TeamJoinStatusBox = styled.div<{ $status: any }>` + display: flex; + align-items: center; + justify-content: center; + + /* width: 5.2rem; */ + height: 3.3rem; + + border-radius: 0.7rem; + color: ${(props) => props.theme.colors.white}; + background-color: ${(props) => { + if (props.$status === '합류') { + return '#83C877'; + } else if (props.$status === '검토 중') { + return '#F5BD68'; + } else if (props.$status === '반려') { + return '#D9635D'; + } else { + console.log('민정아 반환값 잘못줬다.'); + return '#83C877'; // 다른 경우 투명색 또는 다른 적절한 기본값 + } + }}; + + padding: 0.3rem 0.8rem; +`; + +const TeamLeaderIntroduceTitle = styled.div` + color: ${(props) => props.theme.colors.gray90}; + + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + + width: 100%; + > div { + flex-grow: 1; //너비를 제외하고 최대로 차지 + height: 1px; + border-top: 1px solid ${(props) => props.theme.colors.primary40}; + } +`; + +//재사용 + +const TeamLeaderIntroduce = styled.div` + width: 100%; + height: 100px; + + border: 1px solid ${(props) => props.theme.colors.gray40}; + border-radius: 1.2rem; + background-color: ${(props) => props.theme.colors.white}; + + color: ${(props) => props.theme.colors.primary60}; + + display: flex; + justify-content: center; + align-items: center; +`; +const TeamStatusBox = styled.div` + width: 100%; + /* height: 100px; */ + + color: ${(props) => props.theme.colors.primary60}; + + display: flex; + /* justify-content: center; */ + align-items: center; + gap: 6rem; +`; +const TeamStatusItem = styled.div` + color: ${(props) => props.theme.colors.gray90}; + span { + color: ${(props) => props.theme.colors.primary60}; + } +`; export default MyTeamApplyContainer; diff --git a/src/components/myteam/end/MyTeamEndBox.tsx b/src/components/myteam/end/MyTeamEndBox.tsx new file mode 100644 index 0000000..49046b4 --- /dev/null +++ b/src/components/myteam/end/MyTeamEndBox.tsx @@ -0,0 +1,142 @@ +import { styled } from 'styled-components'; +import StarTtile from '../../common/StarTtile'; +import Button from '../../common/Button'; +import TeamMemberScrollBox from '../active/TeamMemberScrollBox'; +import Title from '../../common/Title'; +import { EndTeamData } from '../../../interface/MyTeam'; + +interface MyTeamEndBoxProps { + endTeam: EndTeamData; +} + +const MyTeamEndBox = ({ endTeam }: MyTeamEndBoxProps) => { + return ( + + {endTeam && ( + <> + + 다음 팀 활동을 위해 +

팀원들에게 추천사를 남겨주세요!

+

팀원들에게 추천사를 남겨야

+

내가 받은 추천사를 볼 수 있어요!

+ + {'myteam_end'} + + 추천사 작성하러 가기 + {'right_arrow'} + + +
+ + +

대회명

+

{endTeam.contestTitle}

+
+ + + 함께했던 팀원들 총 {endTeam.memberSize}명 + +

활동 종료일 : {endTeam.endDate}

+
+ {endTeam.leaderInfo && endTeam.teamMemberInfos && ( + + )} +
+ + )} +
+ ); +}; + +export default MyTeamEndBox; + +const MyTeamEndBoxContainer = styled.div` + display: flex; + background: ${({ theme }) => theme.colors.primary10}; + padding: 6rem; + gap: 4rem; + border-radius: 10px; + margin-bottom: 3rem; +`; + +const Box = styled.div` + background: ${({ theme }) => theme.colors.white}; + border-radius: 10px; + border: 1px solid ${({ theme }) => theme.colors.gray20}; +`; + +const MyTeamEndLeft = styled(Box)` + width: 35%; + padding: 5rem 5.5rem; + + h1 { + ${({ theme }) => theme.fonts.subtitleXL}; + color: ${({ theme }) => theme.colors.primary60}; + margin-bottom: 1.5rem; + } + p { + ${({ theme }) => theme.fonts.bodyM}; + color: ${({ theme }) => theme.colors.primary90}; + } +`; + +const MyTeamDiv = styled.div` + margin: 3rem 0 5rem 0; + text-align: center; + position: relative; +`; + +const MyTeamEndRight = styled.div` + width: 65%; + display: flex; + justify-content: space-between; + flex-direction: column; +`; + +const MyTeamEndRightTop = styled(Box)` + padding: 1rem 2rem; + display: flex; + align-items: center; + + gap: 1.5rem; + color: ${({ theme }) => theme.colors.gray90}; + p { + ${({ theme }) => theme.fonts.bodyL}; + } + h1 { + ${({ theme }) => theme.fonts.subtitleL}; + } +`; + +const ButtonStyle = styled(Button)` + height: 6rem; + position: absolute; + bottom: -5rem; + left: 50%; + transform: translateX(-50%); + padding: 0 4rem; +`; + +const RightFlexBox = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + p { + ${({ theme }) => theme.fonts.subtitleS}; + color: ${({ theme }) => theme.colors.gray90}; + } +`; + +const EndTitle = styled(Title)` + ${({ theme }) => theme.fonts.subtitleL}; +`; diff --git a/src/components/myteam/open/FormTitle.tsx b/src/components/myteam/open/FormTitle.tsx new file mode 100644 index 0000000..7d58f20 --- /dev/null +++ b/src/components/myteam/open/FormTitle.tsx @@ -0,0 +1,37 @@ +import { styled } from 'styled-components'; + +interface FormTitleProps { + title: string; +} + +const FormTitle = ({ title }: FormTitleProps) => { + return ( + + + {title} + + + + ); +}; + +export default FormTitle; + +const FormTitleBox = styled.div` + min-width: 20rem; +`; + +const FormTitleText = styled.div` + position: relative; + ${({ theme }) => theme.fonts.subtitleXXL}; + color: ${({ theme }) => theme.colors.gray90}; + display: inline-block; +`; + +const FormTitleCircle = styled.img` + width: 0.8rem; + height: 0.8rem; + position: absolute; + top: -0.1rem; + right: -1.4rem; +`; diff --git a/src/components/myteam/open/MyTeamOpenBox.tsx b/src/components/myteam/open/MyTeamOpenBox.tsx new file mode 100644 index 0000000..d8005d6 --- /dev/null +++ b/src/components/myteam/open/MyTeamOpenBox.tsx @@ -0,0 +1,193 @@ +import { styled } from 'styled-components'; +import Button from '../../common/Button'; +import ProfileBoxMember from '../../common/ProfileBoxMember'; +import StarTitle from '../../common/StarTtile'; +import { TeamData } from '../../../interface/MyTeam'; + +interface MyTeamOpenBoxProps { + myTeamOpen: TeamData; +} + +const MyTeamOpenBox = ({ myTeamOpen }: MyTeamOpenBoxProps) => { + return ( + + {myTeamOpen && ( + <> + + {myTeamOpen.contestTitle} + + + + + +
+ + 합류한 팀원들 + 총 {myTeamOpen.teamMemberSize}명 + + + {myTeamOpen.teamMemberSize !== 0 && + myTeamOpen.teamMemberInfos ? ( + myTeamOpen.teamMemberInfos.map((teamMember) => ( + + )) + ) : ( + + {'team_member'} + 아직 합류한 팀원이 없어요 + + )} + +
+
+ 지원자 + + {myTeamOpen.applyMemberSize !== 0 && + myTeamOpen.teamMemberInfos ? ( + <> + + +

+ 현재 {myTeamOpen.applyMemberSize}명이 +

+

팀원으로 지원했어요.

+
+ {myTeamOpen.applyMemberSize !== 0 && + myTeamOpen.teamMemberInfos.map((teamMember) => ( + + ))} + + ) : ( + + {'team_member'} + 아직 합류한 팀원이 없어요 + + )} +
+
+
+ + )} +
+ ); +}; + +export default MyTeamOpenBox; + +const MyTeamOpenContainer = styled.div` + display: flex; + background: ${({ theme }) => theme.colors.primary10}; + padding: 6rem 10rem; + gap: 4rem; + margin-bottom: 3rem; + border-radius: 10px; +`; + +const CompetitionBox = styled.div` + display: flex; + flex-direction: column; + width: 30%; + justify-content: space-between; +`; + +const CompetitionTitle = styled.div` + ${({ theme }) => theme.fonts.subtitleXL}; + color: ${({ theme }) => theme.colors.gray90}; +`; + +const CompetitionImg = styled.img` + width: 100%; + object-fit: cover; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.colors.gray40}; + margin: 0.5rem 0 1rem 0; +`; + +const TeamMemberBox = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + width: 70%; + gap: 2rem; +`; + +const TeamMembersBox = styled.div` + border-radius: 8px; + background: ${({ theme }) => theme.colors.white}; + padding: 2rem; + width: 100%; + + display: flex; + align-items: center; + gap: 1.5rem; + overflow-x: scroll; + + &::-webkit-scrollbar { + width: 1rem; + height: 0.8rem; + } + &::-webkit-scrollbar-thumb { + background: ${({ theme }) => theme.colors.primary60}; + border-radius: 10px; + } +`; + +const NoTeamMember = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem; + gap: 1rem; + + ${({ theme }) => theme.fonts.subtitleL}; + color: ${({ theme }) => theme.colors.gray90}; +`; + +const ApplyContent = styled.div` + width: 13.2rem; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + + p { + ${({ theme }) => theme.fonts.subtitleS}; + color: ${({ theme }) => theme.colors.gray90}; + text-align: center; + } + span { + ${({ theme }) => theme.fonts.subtitleS}; + color: ${({ theme }) => theme.colors.primary60}; + } +`; + +const ApplyFireImg = styled.img` + margin-bottom: 1rem; +`; diff --git a/src/components/profile/ProfileInfo.tsx b/src/components/profile/ProfileInfo.tsx index cdfa6e6..40ede42 100644 --- a/src/components/profile/ProfileInfo.tsx +++ b/src/components/profile/ProfileInfo.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; const ProfileInfo = ({ @@ -15,10 +16,14 @@ const ProfileInfo = ({ major?: string; selfIntroduction?: string; }) => { + const navigate = useNavigate(); return ( <> {name} 님의 프로필 + navigate('/profile/modify')}> + 계정 설정에서 변경하기 + {name} @@ -47,13 +52,15 @@ const ProfileText = styled.div` margin: 3rem 0; `; const ProfileContainer = styled.div` + position: relative; width: 100%; height: 29.6rem; background-color: ${(props) => props.theme.colors.primary10}; - border-radius: 1 as { + border-radius: 1rem; + /* as { profiledata: ProfileData; isloading: boolean; - } + } */ display: flex; align-items: center; @@ -92,4 +99,15 @@ const ProfileValue = styled.div` display: inline-block; `; +const ModifyProfile = styled.div` + position: absolute; + bottom: 1rem; + right: 1rem; + + ${(props) => props.theme.fonts.subtitleXL}; + color: ${(props) => props.theme.colors.primary60}; + text-decoration: underline; + + cursor: pointer; +`; export default ProfileInfo; diff --git a/src/components/profile/ProfileKeyword.tsx b/src/components/profile/ProfileKeyword.tsx index 1b90c24..e4afea0 100644 --- a/src/components/profile/ProfileKeyword.tsx +++ b/src/components/profile/ProfileKeyword.tsx @@ -1,29 +1,35 @@ import styled from 'styled-components'; import { keywordList } from '../../constants/KeywordList'; import { Keyword } from '../../interface/Profile'; +import ProfileNotReviewed from './ProfileNotReviewed'; const DETAIL = '키워드 옆의 레벨은 해당 키워드를 받은 횟수를 의미해요.\n키워드는 가장 많이 받은 순서대로 상위 5개까지만 노출돼요.'; const ProfileKeyword = ({ keywords, name, + isUserGetExternalReview, }: { keywords?: Keyword[]; name?: string; + isUserGetExternalReview?: boolean; }) => { return ( {name} 님의 장점 키워드 {DETAIL} - - {keywords?.map((data: Keyword, index: number) => { - const [keywordIdx, count] = Object.values(data) as [number, number]; - return ( - - {keywordList[keywordIdx]} | Lv.{count} - - ); - })} + {isUserGetExternalReview ? ( + keywords?.map((data: Keyword, index: number) => { + const [keywordIdx, count] = Object.values(data) as [number, number]; + return ( + + {keywordList[keywordIdx]} | Lv.{count} + + ); + }) + ) : ( + + )} ); }; diff --git a/src/components/profile/ProfileNotReviewed.tsx b/src/components/profile/ProfileNotReviewed.tsx new file mode 100644 index 0000000..65c8095 --- /dev/null +++ b/src/components/profile/ProfileNotReviewed.tsx @@ -0,0 +1,59 @@ +import styled from 'styled-components'; + +import questionmarkSrc from '/assets/images/common/questionmark.svg'; +import { useSetRecoilState } from 'recoil'; +import { needKakaoReviewModalState } from '../../recoil/atom'; + +const ProfileNotReviewed = () => { + const setKakaoReviewModalVisible = useSetRecoilState( + needKakaoReviewModalState, + ); + return ( + + + + {'아직 받은 추천사가 없어요!\n추천사를 요청해 프로필을 채워보세요.'} + + + + ); +}; +const Layout = styled.div` + width: 100%; + height: 31rem; + + border-radius: 1rem; + border: 1px solid ${(props) => props.theme.colors.gray20}; + background-color: ${(props) => props.theme.colors.gray5}; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2rem; +`; +const QuestionmarkImg = styled.img` + width: 9rem; +`; +const Subtitle = styled.div` + ${({ theme }) => theme.fonts.subtitleL}; + white-space: break-spaces; + + display: flex; + justify-content: center; + align-items: center; + text-align: center; +`; +const Button = styled.button` + width: 31.2rem; + height: 6.4rem; + + ${({ theme }) => theme.fonts.buttonL}; + border: 1px solid ${(props) => props.theme.colors.primary20}; + border-radius: 3.2rem; + background-color: ${(props) => props.theme.colors.primary60}; + color: ${(props) => props.theme.colors.white}; +`; +export default ProfileNotReviewed; diff --git a/src/components/profile/ProfilePersonality.tsx b/src/components/profile/ProfilePersonality.tsx index 76903fd..bdab051 100644 --- a/src/components/profile/ProfilePersonality.tsx +++ b/src/components/profile/ProfilePersonality.tsx @@ -5,15 +5,18 @@ import { TEAM_CURTURE_CATEGORY, WORK_METHOD_CATEGORY, } from '../../constants/Profile'; +import ProfileNotReviewed from './ProfileNotReviewed'; const ProfilePersonality = ({ teamCurturesData, workMethodsData, name, + isUserGetExternalReview, }: { teamCurturesData?: TeamCulture[]; workMethodsData?: WorkMethod[]; name?: string; + isUserGetExternalReview?: boolean; }) => { return ( @@ -21,16 +24,20 @@ const ProfilePersonality = ({ 스펙트럼 위의 별은 {name} 님이 받은 후기의 평균치를 의미해요. - - - - + {isUserGetExternalReview ? ( + + + + + ) : ( + + )} ); }; diff --git a/src/components/profile/ProfileReview.tsx b/src/components/profile/ProfileReview.tsx index a1ac086..e170221 100644 --- a/src/components/profile/ProfileReview.tsx +++ b/src/components/profile/ProfileReview.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; import ProfileReviewContentsBox from './ProfileReviewContentsBox'; import { Comment } from '../../interface/Profile'; +import ProfileNotReviewed from './ProfileNotReviewed'; const ProfileReview = ({ reviewData, @@ -8,12 +9,14 @@ const ProfileReview = ({ isLocked, setIsLackModalVisible, setIsUseModalVisible, + isUserGetExternalReview, }: { reviewData?: Comment[]; name?: string; isLocked?: boolean; setIsLackModalVisible: React.Dispatch>; setIsUseModalVisible: React.Dispatch>; + isUserGetExternalReview?: boolean; }) => { const DETAIL_LOCKED_TICKET = isLocked ? `티켓을 사용하면 ${name} 님의 한 줄 추천사를 언제든지 열람하실 수 있습니다.` @@ -22,20 +25,26 @@ const ProfileReview = ({ return ( {name} 님이 받은 한 줄 추천사 - - {DETAIL_LOCKED_TICKET} - - 한 줄 추천사 수{LOCKED_TICKET_AMMOUNT}개 - 최신순↑ - - - + {isUserGetExternalReview ? ( + <> + + {DETAIL_LOCKED_TICKET} + + 한 줄 추천사 수{LOCKED_TICKET_AMMOUNT}개 + 최신순↑ + + + {' '} + + ) : ( + + )} ); }; diff --git a/src/components/profile/ProfileSubInfo.tsx b/src/components/profile/ProfileSubInfo.tsx index e3ecbfe..d7b1168 100644 --- a/src/components/profile/ProfileSubInfo.tsx +++ b/src/components/profile/ProfileSubInfo.tsx @@ -1,5 +1,6 @@ import styled from 'styled-components'; -import ProfileSubInfoContents from './ProfileSubInfoContent'; +import { useState } from 'react'; +import { useProfileCreate } from '../../hooks/profile/useProfileCreate'; /* const activitiesData: SubInfoContentsProps = { @@ -47,25 +48,133 @@ const ProfileSubInfo = ({ tools?: string[]; certificates?: string[]; }) => { - const titles1 = ['대외활동 및 인턴', '수상 경력']; - const titles2 = ['사용 가능 툴', '보유 자격증']; + const TITLES = [ + '대외활동 및 인턴', + '수상 경력', + '사용 가능 툴', + '보유 자격증', + ]; + const NAMES = ['internships', 'awards', 'tools', 'certificates']; + + const [isModifying, setIsModyfying] = useState(false); + const [texts, setTexts] = useState({ + internships: internships?.join('\n'), + awards: awards?.join('\n'), + tools: tools?.join('\n'), + certificates: certificates?.join('\n'), + }); + + const handleTextChange = (event: React.ChangeEvent) => { + setTexts((prev: any) => ({ + ...prev, + [event.target.name]: event.target.value, + })); + }; + + const profileCreateMutation = useProfileCreate({ + internships: texts?.internships?.split('\n') as string[], + awards: texts?.awards?.split('\n') as string[], + tools: texts?.tools?.split('\n') as string[], + certificates: texts?.certificates?.split('\n') as string[], + }); + const handleClickModify = () => { + setIsModyfying((curr) => !curr); + if (isModifying) { + profileCreateMutation.mutate(); + } + }; return ( 이력 - + + + {isModifying && '저장'} + {!isModifying && '수정'} + + {TITLES[0]} + + {isModifying ? ( + <> + {texts?.internships?.length}/150 + + + ) : ( + internships?.map((content: any, index: number) => ( + {content} + )) + )} + + {TITLES[1]} + + {isModifying ? ( + <> + {texts?.awards?.length}/150 + + + ) : ( + awards?.map((content: any, index: number) => ( + {content} + )) + )} + + 스킬 - + + {' '} + + {isModifying && '저장'} + {!isModifying && '수정'} + + {TITLES[2]} + + {isModifying ? ( + <> + {texts?.tools?.length}/150 + + + ) : ( + tools?.map((content: any, index: number) => ( + {content} + )) + )} + + {TITLES[3]} + + {isModifying ? ( + <> + {texts?.certificates?.length}/150 + + + ) : ( + certificates?.map((content: any, index: number) => ( + {content} + )) + )} + + ); @@ -82,5 +191,61 @@ const ProfileSubInfoTitle = styled.div` color: ${(props) => props.theme.colors.gray100}; margin: 1.8rem 0; `; +const ProfileSubInfoContents = styled.div` + position: relative; + + background-color: ${(props) => props.theme.colors.primary10}; + width: 60rem; + /* height: 28.4rem; */ + + border-radius: 1.2rem; + padding: 1rem 3rem 4rem 3rem; +`; + +// + +const ContentsSubTitle = styled.div` + ${(props) => props.theme.fonts.heading4}; + color: ${(props) => props.theme.colors.gray100}; + padding: 1rem 0; +`; +const ContentsDetailBox = styled.div` + position: relative; +`; +const ContentsDetail = styled.div` + ${(props) => props.theme.fonts.bodyM}; + color: ${(props) => props.theme.colors.gray90}; + margin-left: 2.2rem; +`; +const ModifyProfile = styled.div` + position: absolute; + top: 1rem; + right: 2rem; + + ${(props) => props.theme.fonts.subtitleXL}; + color: ${(props) => props.theme.colors.primary60}; + text-decoration: underline; + + cursor: pointer; +`; +const ModifyingArea = styled.textarea` + width: 100%; + min-height: 15rem; + + border-radius: 0.8rem; + border: 1px solid ${(props) => props.theme.colors.gray20}; + color: ${(props) => props.theme.colors.gray90}; + ${(props) => props.theme.fonts.bodyM}; + + resize: none; +`; +const LengthCount = styled.div` + position: absolute; + right: 1.5rem; + bottom: 1.5rem; + + color: ${(props) => props.theme.colors.gray70}; + ${(props) => props.theme.fonts.bodyM}; +`; export default ProfileSubInfo; diff --git a/src/components/profile/ProfileSubInfoContent.tsx b/src/components/profile/ProfileSubInfoContent.tsx deleted file mode 100644 index 41d4433..0000000 --- a/src/components/profile/ProfileSubInfoContent.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import styled from 'styled-components'; - -const ProfileSubInfoContents = ({ - props1, - props2, - titles, -}: { - props1?: string[]; - props2?: string[]; - titles: string[]; -}) => { - return ( - - {titles[0]} - {props1?.map((content: any, index: number) => ( - {content} - ))} - {titles[1]} - {props2?.map((content: any, index: number) => ( - {content} - ))} - - ); -}; - -const ContentsContainer = styled.div` - background-color: ${(props) => props.theme.colors.primary10}; - width: 60rem; - /* height: 28.4rem; */ - - border-radius: 1.2rem; - padding: 3rem 3rem; -`; - -const ContentsSubTitle = styled.div` - ${(props) => props.theme.fonts.heading4}; - color: ${(props) => props.theme.colors.gray100}; - padding: 1rem 0; -`; -const ContentsDetail = styled.div` - ${(props) => props.theme.fonts.bodyM}; - color: ${(props) => props.theme.colors.gray90}; - margin-left: 2.2rem; -`; - -export default ProfileSubInfoContents; diff --git a/src/constants/Profile.ts b/src/constants/Profile.ts index b2afc40..3b540ef 100644 --- a/src/constants/Profile.ts +++ b/src/constants/Profile.ts @@ -1,40 +1,5 @@ import { ICategory } from '../interface/Profile'; -// export const ProfileSubInfoData: IProfileSubInfoData = { -// activitiesData: { -// title: '대외활동 및 인턴', -// contents: [ -// '한국대학생IT경영학회 (23.08~): 기획 파트, 팀원 모집 플랫폼 "Wanteam." 기획', -// '캐캐오기업 (22.01~22.12): 제품 기획 부서, 초콜릿 신제품 맛보기 테스트', -// '코코넛기업 (21.06~21.09): 영업 부서, 코코넛 사세요 맛 좋은 코코넛 사세요', -// ], -// }, -// awardsData: { -// title: '수상 경력', -// contents: [ -// '초콜릿 맛있게 먹기 공모전 최우수상', -// '샘송 광고 기획 아이디어 공모전 장려상', -// '멍 때리기 대회 대상', -// ], -// }, -// toolsData: { -// title: '사용 가능 툴', -// contents: [ -// '노션: 휘황찬란하게 활용하지는 못하지만 적당히 쓸 줄은 알아요!', -// '지라: 인턴할 때 써봐서 어느 정도 활용 가능합니다.', -// '피그마: 간단한 와이어프레임 정도는 그릴 수 있어요! ', -// ], -// }, -// licensesData: { -// title: '대외활동 및 인턴', -// contents: [ -// '늦게 자기 자격증 1급', -// '해리 포터 덕후 자격증 마스터', -// '(G)-IDLE 팬클럽 ‘네버랜드’ 4기', -// ], -// }, -// }; - export const TEAM_CURTURE_CATEGORY: ICategory = { title: '팀 문화', category: [ diff --git a/src/constants/competitionList.ts b/src/constants/competitionList.ts index 7c96633..d90d20f 100644 --- a/src/constants/competitionList.ts +++ b/src/constants/competitionList.ts @@ -1,41 +1,3 @@ -export const recruitingList = [ - { - id: 1, - title: '세종특별자치시 2023 일자리 아이디어(정책) 공모전', - name: '박형준', - description: '봄감자가 맛있답니다', - profile: '/assets/images/review/profile.svg', - }, - { - id: 2, - title: '외국인 인식 개선을 위한 콘텐츠(영상, 웹툰) 제작 공모전', - name: '민혜린', - description: '봄감자가 맛있답니다', - profile: '/assets/images/review/profile.svg', - }, - { - id: 3, - title: '[인천국제공항공사] 우리동네 여행코스 대국민 공모전', - name: '채영대', - description: '봄감자가 맛있답니다', - profile: '/assets/images/review/profile.svg', - }, - { - id: 4, - title: '2024 KT&G 국제 대학생 창업교류전(ASVF) 한국 대표 모집', - name: '이재영', - description: '디자인', - profile: '/assets/images/review/profile.svg', - }, - { - id: 5, - title: '세종특별자치시 2023 일자리 아이디어(정책) 공모전', - name: '박진우', - description: '프론트엔드', - profile: '/assets/images/review/profile.svg', - }, -]; - export const searchButtonList = [ '전체', '기획/아이디어', @@ -49,46 +11,3 @@ export const searchButtonList = [ '창업/스타트업', '기타', ]; - -export const competitionList = [ - { - contestId: 1, - title: '제 1회 삼성전자 주니어 소프트웨어 창작대회', - images: '/assets/images/competition/competition.svg', - teamNum: 8, - remainDay: 11, - company: '한국경제신문', - }, - { - contestId: 2, - title: '2024 KT&G 국제 대학생 창업교류전(ASVF) 한국 대표 모집', - images: '/assets/images/competition/competition.svg', - teamNum: 8, - remainDay: 11, - company: '한국경제신문', - }, - { - contestId: 3, - title: '제 1회 삼성전자 주니어 소프트웨어 창작대회', - images: '/assets/images/competition/competition.svg', - teamNum: 8, - remainDay: 11, - company: '한국경제신문', - }, - { - contestId: 4, - title: '제 1회 삼성전자 주니어 소프트웨어 창작대회', - images: '/assets/images/competition/competition.svg', - teamNum: 8, - remainDay: 11, - company: '한국경제신문', - }, - { - contestId: 5, - title: '제 1회 삼성전자 주니어 소프트웨어 창작대회', - images: '/assets/images/competition/competition.svg', - teamNum: 8, - remainDay: 11, - company: '한국경제신문', - }, -]; diff --git a/src/constants/main.ts b/src/constants/main.ts index 8d52fb4..95766a3 100644 --- a/src/constants/main.ts +++ b/src/constants/main.ts @@ -8,7 +8,7 @@ export const competitionList = [ teamNum: 3, }, { - contestId: '1', + contestId: '2', title: '2021년도 제 1회 삼성 SDS 아이디어 공모전', company: '삼성 SDS', images: ['/assets/images/competition/competition.svg'], @@ -16,7 +16,7 @@ export const competitionList = [ teamNum: 3, }, { - contestId: '1', + contestId: '3', title: '2021년도 제 1회 삼성 SDS 아이디어 공모전', company: '삼성 SDS', images: ['/assets/images/competition/competition.svg'], @@ -24,7 +24,7 @@ export const competitionList = [ teamNum: 3, }, { - contestId: '1', + contestId: '4', title: '2021년도 제 1회 삼성 SDS 아이디어 공모전', company: '삼성 SDS', images: ['/assets/images/competition/competition.svg'], diff --git a/src/constants/myteam.ts b/src/constants/myteam.ts index 12168c2..383a4a4 100644 --- a/src/constants/myteam.ts +++ b/src/constants/myteam.ts @@ -15,4 +15,4 @@ export const activityAreaOptions = [ '경상북도', '경상남도', '제주도', -]; +].map((name, index) => ({ id: index + 1, name })); diff --git a/src/hooks/competition/useRecruitingTeam.tsx b/src/hooks/competition/useRecruitingTeam.tsx new file mode 100644 index 0000000..e56a4ea --- /dev/null +++ b/src/hooks/competition/useRecruitingTeam.tsx @@ -0,0 +1,15 @@ +import { useQuery } from 'react-query'; +import { ResponseRecruitingTeam } from '../../interface/MyTeam'; +import { recruitingTeamKeys } from '../../keys/competitionKeys'; +import { getRecruitingTeam } from '../../apis/competition/getRecruitingTeam'; + +interface UseRecruitingTeam { + recruitingTeam?: ResponseRecruitingTeam; +} + +export function useRecruitingTeam(): UseRecruitingTeam { + const { data: recruitingTeam } = useQuery(recruitingTeamKeys.all, () => + getRecruitingTeam(), + ); + return { recruitingTeam }; +} diff --git a/src/hooks/contest/useJoinTeam.ts b/src/hooks/contest/useJoinTeam.ts new file mode 100644 index 0000000..70f4a3d --- /dev/null +++ b/src/hooks/contest/useJoinTeam.ts @@ -0,0 +1,31 @@ +import { useMutation } from 'react-query'; +import postJoinTeam from '../../apis/contest/postJoinTeam'; + +// export const useJoinTeam = (teamId: string) => { +// const { data: joinTeamData } = useQuery('joinTeam', () => +// postJoinTeam({ +// teamId: teamId as string, +// }), +// ); + +// return { joinTeamData }; +// }; + +// export default useJoinTeam; + +interface UseJoinTeam { + mutate: () => void; +} +export function useJoinTeam(teamId: string): UseJoinTeam { + const { mutate } = useMutation( + 'joinTeam', + () => + postJoinTeam({ + teamId: teamId as string, + }), + {}, + ); + return { mutate }; +} + +export default useJoinTeam; diff --git a/src/hooks/login/useLoginWithKakaoToken.tsx b/src/hooks/login/useLoginWithKakaoToken.tsx index eac88a7..51f85ae 100644 --- a/src/hooks/login/useLoginWithKakaoToken.tsx +++ b/src/hooks/login/useLoginWithKakaoToken.tsx @@ -30,6 +30,7 @@ const useLoginWithKakaoToken = () => { }, }); navigate('/join/request'); + // window.location.reload(); } else { if (responseLogin.statusCode == 404) { console.log('로그인실패 회원이 아님', responseLogin); diff --git a/src/hooks/modify/useModifyProfile.ts b/src/hooks/modify/useModifyProfile.ts new file mode 100644 index 0000000..75dd4e6 --- /dev/null +++ b/src/hooks/modify/useModifyProfile.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from 'react-query'; +import { RequestModifyProfile } from '../../interface/Modify'; +import patchModifyProfile from '../../apis/modify/patchModifyProfile'; +import { useNavigate } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { loginInfoState } from '../../recoil/atom'; + +interface UseModifyProfile { + mutate: () => void; +} + +export function useModifyProfile( + requestData: RequestModifyProfile, + userId: string, +): UseModifyProfile { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const setLoginInfo = useSetRecoilState(loginInfoState); + + const { mutate } = useMutation(() => patchModifyProfile(requestData), { + onSuccess: () => { + queryClient.invalidateQueries('modifyProfile'); + navigate(`/profile/${userId}`); //미쳤다 이 코드 + setLoginInfo((curr) => ({ + ...curr, + data: { + ...curr.data!, + name: requestData.username, + }, + })); + // setLoginInfo((curr) => ({ ...curr, data: {...curr.data,name:requestData.username}} })); + }, + }); + return { mutate }; +} diff --git a/src/hooks/myTeam/useActiveTeam.ts b/src/hooks/myTeam/useActiveTeam.ts new file mode 100644 index 0000000..ed4e2ff --- /dev/null +++ b/src/hooks/myTeam/useActiveTeam.ts @@ -0,0 +1,12 @@ +import { useQuery } from 'react-query'; +import getActiveTeam from '../../apis/myTeam/getActiveTeam'; + +export const useActiveTeam = () => { + const { data: activeTeamData } = useQuery('activeTeam', () => + getActiveTeam(), + ); + + return { activeTeamData }; +}; + +export default useActiveTeam; diff --git a/src/hooks/myTeam/useEndTeam.ts b/src/hooks/myTeam/useEndTeam.ts new file mode 100644 index 0000000..7067518 --- /dev/null +++ b/src/hooks/myTeam/useEndTeam.ts @@ -0,0 +1,12 @@ +import { useQuery } from 'react-query'; +import { getEndTeam } from '../../apis/myTeam/getEndTeam'; +import { ResponseEndTeam } from '../../interface/MyTeam'; + +interface UseEndTeam { + endTeam?: ResponseEndTeam; +} + +export function useEndTeam(): UseEndTeam { + const { data: endTeam } = useQuery('endTeam', () => getEndTeam()); + return { endTeam }; +} diff --git a/src/hooks/myTeam/useOpenedTeam.ts b/src/hooks/myTeam/useOpenedTeam.ts new file mode 100644 index 0000000..943d7b6 --- /dev/null +++ b/src/hooks/myTeam/useOpenedTeam.ts @@ -0,0 +1,12 @@ +import { useQuery } from 'react-query'; +import { getOpenedTeam } from '../../apis/myTeam/getOpenedTeam'; +import { ResponseOpenedTeam } from '../../interface/MyTeam'; + +interface UseOpenedTeam { + openedTeam?: ResponseOpenedTeam; +} + +export function useOpenedTeam(): UseOpenedTeam { + const { data: openedTeam } = useQuery('openedTeam', () => getOpenedTeam()); + return { openedTeam }; +} diff --git a/src/hooks/myTeam/useTeamOpen.ts b/src/hooks/myTeam/useTeamOpen.ts new file mode 100644 index 0000000..25829d4 --- /dev/null +++ b/src/hooks/myTeam/useTeamOpen.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from 'react-query'; +import { RequestTeamOpen } from '../../interface/MyTeam'; +import postTeamOpen from '../../apis/myTeam/postTeamOpen'; +import { myteamKeys } from '../../keys/myteamKeys'; + +interface UseTeamOpen { + mutate: () => void; +} + +export function UseTeamOpen(teamOpen: RequestTeamOpen): UseTeamOpen { + const queryClient = useQueryClient(); + + const { mutate } = useMutation(myteamKeys.all, () => postTeamOpen(teamOpen), { + onSuccess: () => { + queryClient.invalidateQueries(); + }, + }); + return { mutate }; +} diff --git a/src/hooks/profile/useIsUserGetExternalReview.ts b/src/hooks/profile/useIsUserGetExternalReview.ts new file mode 100644 index 0000000..4f72249 --- /dev/null +++ b/src/hooks/profile/useIsUserGetExternalReview.ts @@ -0,0 +1,17 @@ +import { useQuery } from 'react-query'; + +import getIsUserGetExternalReview from '../../apis/profile/getIsUserGetExternalReview'; + +export const useIsUserGetExternalReview = (userId: string) => { + const { data: isUserGetExternalReviewData } = useQuery( + 'isUserGetExternalReview', + () => + getIsUserGetExternalReview({ + userId: userId, + }), + ); + + return { isUserGetExternalReviewData }; +}; + +export default useIsUserGetExternalReview; diff --git a/src/hooks/profile/useProfile.ts b/src/hooks/profile/useProfile.ts index c52d47f..ce77a65 100644 --- a/src/hooks/profile/useProfile.ts +++ b/src/hooks/profile/useProfile.ts @@ -2,10 +2,18 @@ import getProfile from '../../apis/profile/getProfile'; import { useQuery } from 'react-query'; export const useProfile = (userId?: string) => { - const { data: profileData, isLoading } = useQuery('profile', () => - getProfile({ - userId: userId, - }), + const { data: profileData, isLoading } = useQuery( + 'profile', + () => + getProfile({ + userId: userId, + }), + { + onSuccess: () => { + // queryClient.invalidateQueries('modifyProfile'); + //새로고침이나 리렌더링 + }, + }, ); return { profileData, isLoading }; diff --git a/src/hooks/profile/useTicketNumber.ts b/src/hooks/profile/useTicketNumber.ts index 5ecc125..44c43a4 100644 --- a/src/hooks/profile/useTicketNumber.ts +++ b/src/hooks/profile/useTicketNumber.ts @@ -1,10 +1,10 @@ import { useQuery } from 'react-query'; import getTicketNumber from '../../apis/profile/getTicketNumber'; -import { useSetRecoilState } from 'recoil'; -import { loginModalState } from '../../recoil/atom'; +// import { useSetRecoilState } from 'recoil'; +// import { loginModalState } from '../../recoil/atom'; export const useTicketNumber = () => { - const setIsLoginModalVisible = useSetRecoilState(loginModalState); + // const setIsLoginModalVisible = useSetRecoilState(loginModalState); const { data: TicketNumberData, isLoading } = useQuery( 'ticketNumber', () => getTicketNumber(), @@ -12,7 +12,7 @@ export const useTicketNumber = () => { retry: 0, //이거다!! // 에러가 발생했을 때 실행될 콜백 onError: (error) => { - setIsLoginModalVisible(true); + // setIsLoginModalVisible(true); // navigate('/login'); console.error('로그인이 필요한 에러가 발생했습니다:', error); console.log(error); diff --git a/src/interface/Contest.ts b/src/interface/Contest.ts index 2d5a9bd..80e5179 100644 --- a/src/interface/Contest.ts +++ b/src/interface/Contest.ts @@ -71,3 +71,12 @@ export interface ResponseContestTeamDetailInfo { message: string; data: ContestTeamDetailInfo; } + +////공모전 팀 합류하기 api +export interface RequestJoinTeam { + teamId?: string; +} + +export interface ResponseJoinTeam { + status: number; +} diff --git a/src/interface/Modify.ts b/src/interface/Modify.ts new file mode 100644 index 0000000..2ae819c --- /dev/null +++ b/src/interface/Modify.ts @@ -0,0 +1,13 @@ +//계정 정보 수정하기 api +export interface RequestModifyProfile { + username: string; + location: number; + major: string; + task: string; + selfIntroduce: string; +} +export interface ResponseModifyProfile { + status: number; + message: string; + data: null; +} diff --git a/src/interface/MyTeam.ts b/src/interface/MyTeam.ts index f998641..ea9c2fc 100644 --- a/src/interface/MyTeam.ts +++ b/src/interface/MyTeam.ts @@ -4,17 +4,114 @@ import { ProfileProps } from './Contest'; export interface RequestAppliedTeam { // contestId: string; } + +export interface RequestTeamOpen { + contestId?: string; + max: number; + location: number; + endDate: string; + leaderMessage: string; + notice: string; + chatLink: string; +} + export interface AppliedTeamData { + contestId: string; + contestTitle: string; + contestImage: string[]; + leaderInfo: ProfileProps; + teamId: number; + status: string; + leaderMessage: string; + max: number; + cur: number; + location: string; + endDate: string; +} + +export interface ResponseAppliedTeam { + status: number; + message: string; + data: AppliedTeamData[]; +} + +export interface TeamData { teamId?: number; teamMemberSize?: number; applyMemberSize?: number; teamMemberInfos?: ProfileProps[]; - applyMemberInfos?: ProfileProps[]; // Apply Member 정보에 대한 타입이 없어서 any로 처리했습니다. + applyMemberInfos?: ProfileProps[]; contestTitle?: string; - contestImage?: string[]; + contestImage: string[]; } -export interface ResponseAppliedTeam { + +export interface ResponseOpenedTeam { status: number; message: string; - data: AppliedTeamData[]; + data: TeamData[]; +} + +export interface RequestActiveTeam { + // contestId: string; +} +export interface ActiveTeamData { + contestId: string; + contestTitle: string; + contestImage: string[]; + leaderInfo: ProfileProps; + leaderMessage: string; + memberSize: number; + location: string; + endDate: string; + notice: string; + teamMemberInfos: ProfileProps[]; + chatLink: string; +} +export interface ResponseActiveTeam { + status: number; + message: string; + data: ActiveTeamData[]; +} + +export interface EndTeamData { + contestId: string; + contestTitle: string; + endDate: string; + memberSize: number; + leaderInfo: ProfileProps; + teamMemberInfos: ProfileProps[]; + possibleWriteReviews: boolean; +} + +export interface ResponseEndTeam { + status: number; + message: string; + data: EndTeamData[]; +} + +export interface PageResponseDTO { + startPage: number; + endPage: number; + currentPage: number; + totalCount: number; +} + +export interface RecruitingTeamData { + contestId: string; + contesttitle: string; + teamLeaderId: number; + teamLeaderName: string; + teamLeaderImage: string; + teamLeaderMessage: string; +} + +export interface RecruitingTeam { + pageResponseDTO: PageResponseDTO; + recruitingTeams: RecruitingTeamData[]; +} + +export interface ResponseRecruitingTeam { + status: number; + message: string; + data: RecruitingTeam; } diff --git a/src/interface/Profile.ts b/src/interface/Profile.ts index b70e91a..2e470c5 100644 --- a/src/interface/Profile.ts +++ b/src/interface/Profile.ts @@ -159,3 +159,15 @@ export interface ResponseUseTicket { message: string; data: UseTicketData; } +//유저가 외부 추천사를 받았는지 여부 +export interface RequestIsUserGetExternalReview { + userId?: string; +} +export interface IsUserGetExternalReviewData { + alreadyReviewed: boolean; +} +export interface ResponseIsUserGetExternalReview { + status: number; + message: string; + data: IsUserGetExternalReviewData; +} diff --git a/src/keys/competitionKeys.tsx b/src/keys/competitionKeys.tsx index 4be7c3d..ad8743d 100644 --- a/src/keys/competitionKeys.tsx +++ b/src/keys/competitionKeys.tsx @@ -1,3 +1,7 @@ export const competitionKeys = { all: ['competition'] as const, }; + +export const recruitingTeamKeys = { + all: ['recruitingTeam'] as const, +}; diff --git a/src/keys/myteamKeys.tsx b/src/keys/myteamKeys.tsx new file mode 100644 index 0000000..cab887f --- /dev/null +++ b/src/keys/myteamKeys.tsx @@ -0,0 +1,3 @@ +export const myteamKeys = { + all: ['myteam'] as const, +}; diff --git a/src/pages/contestTeam/ContestTeam.tsx b/src/pages/contestTeam/ContestTeam.tsx index 0fbfd02..25d5538 100644 --- a/src/pages/contestTeam/ContestTeam.tsx +++ b/src/pages/contestTeam/ContestTeam.tsx @@ -7,11 +7,12 @@ import ProfileBoxMember from '../../components/common/ProfileBoxMember'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { headerSelectedState, loginInfoState } from '../../recoil/atom'; import { Headers } from '../../constants/Header'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import useContestTeamDetailInfo from '../../hooks/contest/useContestTeamDetailInfo'; import { useNavigate, useParams } from 'react-router-dom'; import { TEAM_DETAIL_STATUS } from '../../constants/Contest'; import TeamMembers from '../../components/contestTeam/TeamMembers'; +import JoinTeamModal from '../../components/contestTeam/JoinTeamModal'; const ContestTeam = () => { const { teamId, contestId } = useParams(); @@ -21,7 +22,7 @@ const ContestTeam = () => { const userLogininfo = useRecoilValue(loginInfoState); const setHeaderSelected = useSetRecoilState(headerSelectedState); const navigate = useNavigate(); - useEffect(() => setHeaderSelected(Headers.list)); + const [isJoinTeamModalVisible, setIsJoinTeamModalVisible] = useState(false); const teamLeaderBoxProps: ProfileBoxProps = isLoading ? ({} as ProfileBoxProps) : { @@ -33,17 +34,32 @@ const ContestTeam = () => { height: 27.6, }; + const handleJoinTeam = () => { + setIsJoinTeamModalVisible(true); + }; + console.log(contestTeamDetailData); + + useEffect(() => setHeaderSelected(Headers.list)); + + //내가 오픈한 경우, 내 팀 페이지로 이동 if ( contestTeamDetailData?.data.data.status == TEAM_DETAIL_STATUS._1_내가오픈한경우 ) { navigate(`/myTeam/${userLogininfo.data?.userId}/${contestId}/${teamId}`); } - console.log(contestTeamDetailData); + return isLoading ? (
로딩중
) : ( + + navigate(-1)}> {'공모전으로 돌아가기'} @@ -91,6 +107,44 @@ const ContestTeam = () => { cur={contestTeamDetailData?.data.data.cur} max={contestTeamDetailData?.data.data.max} /> + + {contestTeamDetailData?.data.data.status == + TEAM_DETAIL_STATUS._2_남이오픈한경우_내가지원안함 && ( + <> + + 합류 신청하기 → + + + )} + {contestTeamDetailData?.data.data.status == + TEAM_DETAIL_STATUS._3_남이오픈한경우_내가지원완료_승인 && ( + <> + 합류 신청하기 → + 이미 합류 승인된 팀입니다. + + )}{' '} + {contestTeamDetailData?.data.data.status == + TEAM_DETAIL_STATUS._4_남이오픈한경우_내가지원완료_반려 && ( + <> + 합류 신청하기 → + 이미 지원이 반려된 팀입니다. + + )}{' '} + {contestTeamDetailData?.data.data.status == + TEAM_DETAIL_STATUS._5_남이오픈한경우_내가지원완료_승인반려아님 && ( + <> + + navigate(`/myteam/${userLogininfo.data?.userId}/apply`) + } + > + 지원 현황보기 → + + 이미 지원을 완료한 팀입니다. + + )} + ); }; @@ -100,6 +154,8 @@ const TeamLayout = styled.div` display: flex; flex-direction: column; + /* justify-content: center; */ + /* align-items: center; */ gap: 4.4rem; `; const TeamUndo = styled.div` @@ -216,4 +272,36 @@ const TeamNoticeContent = styled.div` white-space: break-spaces; `; +const FlexBox = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1.6rem; +`; +const CustomButton = styled.button<{ $isActive?: boolean }>` + width: 25.5rem; + height: 6.4rem; + + border-radius: 3.2rem; + border: 1px solid + ${(props) => + props.$isActive + ? props.theme.colors.primary20 + : props.theme.colors.gray50}; + + background-color: ${(props) => + props.$isActive ? props.theme.colors.primary60 : props.theme.colors.gray10}; + + ${(props) => props.theme.fonts.buttonL}; + color: ${(props) => + props.$isActive ? props.theme.colors.white : props.theme.colors.gray40}; + + cursor: ${(props) => (props.$isActive ? 'pointer' : 'default')}; +`; +const CustomMessage = styled.div` + ${(props) => props.theme.fonts.buttonL}; + color: ${(props) => props.theme.colors.gray70}; +`; export default ContestTeam; diff --git a/src/pages/login/Oauth.tsx b/src/pages/login/Oauth.tsx index b8cbb6c..89b1e1e 100644 --- a/src/pages/login/Oauth.tsx +++ b/src/pages/login/Oauth.tsx @@ -55,38 +55,6 @@ const Oauth = () => { } }; - // /** 카카오 어세스 토큰를 통해 로그인 하는 함수 - // * - // * @param kakaoAccessToken 카카오 어세스 토큰 - // */ - // const loginWithKakaoToken = async (kakaoAccessToken: string) => { - // try { - // const responseLogin: AxiosResponse = - // await postLoginWithKakaoToken(kakaoAccessToken); - // console.log('loginWithKakaoToken Complete', responseLogin); - - // setLoginInfoState({ - // isLogin: true, - // data: { - // userId: responseLogin.data.data.userId, - // refreshToken: responseLogin.data.data.refreshToken, - // accessToken: responseLogin.data.data.accessToken, - // profileImage: responseLogin.data.data.profileImage, - // name: responseLogin.data.data.name, - // }, - // }); - // } catch (error: any) { - // console.log('loginWithKakaoToken Error', error); - // //여기에 setlogin하면 될듯 - // //회원가입 페이지로 연결 - // if (error.response.data.status == 404) { - // navigate('/login/join', { - // state: { kakaoAccessToken: kakaoAccessToken }, - // }); - // } - // } - // }; - useEffect(() => { const params = new URLSearchParams(location.search); const kakaoAccessCode = params.get('code'); @@ -95,7 +63,7 @@ const Oauth = () => { ); kakaoAccessToken; //우리팀 서버에 카카오 토큰 유효성 검증하기 - + console.log('kakaoAccessToken 잘 됐니?', kakaoAccessToken); navigate('/'); }, []); return
auth
; diff --git a/src/pages/modify/ProfileModify.tsx b/src/pages/modify/ProfileModify.tsx new file mode 100644 index 0000000..194324e --- /dev/null +++ b/src/pages/modify/ProfileModify.tsx @@ -0,0 +1,196 @@ +import styled from 'styled-components'; +import React, { useEffect, useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { headerSelectedState, loginInfoState } from '../../recoil/atom'; +import { Headers } from '../../constants/Header'; + +import bgSrc from '/assets/images/join/join-bg.png'; +import starSrc from '/assets/images/common/star.svg'; +import { INPUT_PROPS, REGIONS } from '../../constants/Join'; +import { InputDataArray } from '../../interface/Join'; + +import useProfile from '../../hooks/profile/useProfile'; +import TextInput from '../../components/modify/TextInput'; +import SelectInput from '../../components/modify/SelectInput'; +import TextAreaInput from '../../components/modify/TextAreaInput'; +import { RequestModifyProfile } from '../../interface/Modify'; +import { useModifyProfile } from '../../hooks/modify/useModifyProfile'; + +const ProfileModify = () => { + const loginInfo = useRecoilValue(loginInfoState); + const { profileData, isLoading } = useProfile( + loginInfo.data?.userId as string | undefined, + ); + + const [inputValue, setInputValue] = useState({ + username: profileData?.data.data.username, + location: REGIONS.indexOf(profileData?.data.data.location as string), + major: profileData?.data.data.major[0], + task: profileData?.data.data.task[0], + selfIntroduce: profileData?.data.data.selfIntroduction, + }); + + // useProfile 로딩이 끝났을 때 + useEffect(() => { + if (!isLoading && profileData) { + setInputValue({ + username: profileData?.data.data.username, + location: REGIONS.indexOf(profileData?.data.data.location as string), + major: profileData?.data.data.major[0], + task: profileData?.data.data.task[0], + selfIntroduce: profileData?.data.data.selfIntroduction, + }); + } + }, [isLoading, profileData]); + + const [buttonActiveCount, setButtonActiveCount] = useState([ + true, + true, + true, + true, + true, + ]); + const handleModiftProfile = useModifyProfile( + inputValue as RequestModifyProfile, + loginInfo.data?.userId as unknown as string, + ); + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + console.log({ ...inputValue }); + handleModiftProfile.mutate(); + }; + + const handleChange = (event: any) => { + setInputValue((prev: any) => ({ + ...prev, + [event.target.name]: event.target.value, + })); + }; + const isAcvivateButton = (buttonActiveArr: InputDataArray) => { + if (buttonActiveArr.every((value) => value === true)) return true; + else return false; + }; + + const setHeaderSelected = useSetRecoilState(headerSelectedState); + useEffect(() => setHeaderSelected(Headers.login)); + + return ( + + + + + 계정 정보를 수정해주세요 + + + + + + + + + 저장하기 + + + + ); +}; + +const JoinLayout = styled.div` + width: 100%; + height: 100%; //수정 필요 + background: url(${bgSrc}) left top no-repeat; + background-size: cover; + + display: flex; + justify-content: center; + align-items: center; + + margin: auto; +`; +const JoinFormContainer = styled.form` + width: 78rem; + /* height: 70rem; */ + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2.4rem; + + border: 1px solid #3b3ef1; + border-radius: 2.4rem; + + background-color: rgba(239, 239, 253, 0.2); + backdrop-filter: blur(12px); + + padding: 3.3rem 6.6rem; + margin: 4rem 0; +`; +const TitleBox = styled.div` + width: 100%; + display: flex; + /* justify-content: left; */ + align-items: center; +`; +const TitleStarImg = styled.img` + width: 3rem; + height: 3rem; + margin-right: 1.2rem; +`; +const TitleText = styled.div` + ${(props) => props.theme.fonts.heading4}; + color: ${(props) => props.theme.colors.gray90}; +`; +const StartButton = styled.button<{ $isActive: boolean }>` + width: 25.5rem; + height: 6.4rem; + + border-radius: 3.2rem; + border: 1px solid + ${(props) => + props.$isActive + ? props.theme.colors.primary20 + : props.theme.colors.gray50}; + + background-color: ${(props) => + props.$isActive ? props.theme.colors.primary60 : props.theme.colors.gray10}; + + ${(props) => props.theme.fonts.buttonL}; + color: ${(props) => + props.$isActive ? props.theme.colors.white : props.theme.colors.gray40}; + + cursor: ${(props) => (props.$isActive ? 'pointer' : 'default')}; +`; +export default ProfileModify; diff --git a/src/pages/myteam/MyTeamActive.tsx b/src/pages/myteam/MyTeamActive.tsx new file mode 100644 index 0000000..0d39981 --- /dev/null +++ b/src/pages/myteam/MyTeamActive.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import useActiveTeam from '../../hooks/myTeam/useActiveTeam'; +import MyTeamActiveContainer from '../../components/myteam/active/MyTeamActiveContainer'; + +const MyTeamActive = () => { + const { activeTeamData } = useActiveTeam(); + console.log(activeTeamData); + + return ( + + {activeTeamData?.data.data.map((each) => ( + + ))} + + ); +}; +const ApplyLayout = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 3rem; +`; +export default MyTeamActive; diff --git a/src/pages/myteam/MyTeamApply.tsx b/src/pages/myteam/MyTeamApply.tsx index 45be6a9..9c25929 100644 --- a/src/pages/myteam/MyTeamApply.tsx +++ b/src/pages/myteam/MyTeamApply.tsx @@ -1,44 +1,24 @@ import styled from 'styled-components'; -import { ResponseAppliedTeam } from '../../interface/MyTeam'; +import useAppliedTeam from '../../hooks/myTeam/useAppliedTeam'; +import MyTeamApplyContainer from '../../components/myteam/apply/MyTeamApplyContainer'; -const DUMMY: ResponseAppliedTeam = { - status: 200, - message: '요청이 성공했습니다.', - data: [ - { - teamId: 1, - teamMemberSize: 1, - applyMemberSize: 0, - teamMemberInfos: [ - { - teamMemberId: 3, - teamMemberName: '맹구', - teamMemberImage: - 'http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg\t', - teamMemberTask: [], - teamMemberMajor: [], - }, - ], - applyMemberInfos: [], - contestTitle: - "[아모레퍼시픽/두리코스메틱등] 뷰티 대기업에서 마케팅/연구개발/MD '실무스펙' 한번에 쌓고 서류합격 확률 200%올리기", - contestImage: [ - 'https://www.wevity.com/upload/contest/20231030101137_0309ea76.jpg', - 'https://www.wevity.com/upload/contest/20231030100653_e642b132.jpg', - ], - }, - ], -}; const MyTeamApply = () => { - // const { appliedTeamData } = useAppliedTeam(); - DUMMY; + const { appliedTeamData } = useAppliedTeam(); + console.log(appliedTeamData); + return ( - {/* {DUMMY.data.map((each) => ( - - ))} */} + {appliedTeamData?.data.data.map((each) => ( + + ))} ); }; -const ApplyLayout = styled.div``; +const ApplyLayout = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 3rem; +`; export default MyTeamApply; diff --git a/src/pages/myteam/MyTeamCreate.tsx b/src/pages/myteam/MyTeamCreate.tsx index 2effb9c..2594265 100644 --- a/src/pages/myteam/MyTeamCreate.tsx +++ b/src/pages/myteam/MyTeamCreate.tsx @@ -3,19 +3,34 @@ import ContestInfo from '../../components/contest/ContestInfo'; import MyTeamCreateOpen from '../../components/myteam/MyTeamCreateOpen'; import ButtonBox from '../../components/myteam/ButtonBox'; import { useSetRecoilState } from 'recoil'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { headerSelectedState } from '../../recoil/atom'; import { Headers } from '../../constants/Header'; +import { RequestTeamOpen } from '../../interface/MyTeam'; const MyTeamCreate = () => { const setHeaderSelected = useSetRecoilState(headerSelectedState); useEffect(() => setHeaderSelected(Headers.myTeam)); + const [teamOpen, setTeamOpen] = useState({ + contestId: '', + max: 0, + location: 0, + endDate: '', + leaderMessage: '', + notice: '', + chatLink: '', + }); + + const handleTeamOpenChange = (newTeamOpen: RequestTeamOpen) => { + setTeamOpen(newTeamOpen); + }; + return ( - - + + ); }; diff --git a/src/pages/myteam/MyTeamEnd.tsx b/src/pages/myteam/MyTeamEnd.tsx new file mode 100644 index 0000000..0c438f7 --- /dev/null +++ b/src/pages/myteam/MyTeamEnd.tsx @@ -0,0 +1,24 @@ +import { styled } from 'styled-components'; +import MyTeamEndBox from '../../components/myteam/end/MyTeamEndBox'; +import { useEndTeam } from '../../hooks/myTeam/useEndTeam'; + +const MyTeamEnd = () => { + const { endTeam } = useEndTeam(); + + return ( + + {endTeam && + endTeam.data.length > 0 && + endTeam.data.map((myEndTeam) => ( + + ))} + + ); +}; + +export default MyTeamEnd; + +const MyTeamEndContainer = styled.div` + width: 122.4rem; + margin: 0 auto; +`; diff --git a/src/pages/myteam/MyTeamOpen.tsx b/src/pages/myteam/MyTeamOpen.tsx new file mode 100644 index 0000000..528d84a --- /dev/null +++ b/src/pages/myteam/MyTeamOpen.tsx @@ -0,0 +1,24 @@ +import { styled } from 'styled-components'; +import MyTeamOpenBox from '../../components/myteam/open/MyTeamOpenBox'; +import { useOpenedTeam } from '../../hooks/myTeam/useOpenedTeam'; + +const MyTeamOpen = () => { + const { openedTeam } = useOpenedTeam(); + + return ( + + {openedTeam && + openedTeam.data.length > 0 && + openedTeam.data.map((myTeamOpen) => ( + + ))} + + ); +}; + +export default MyTeamOpen; + +const MyTeamOpenContainer = styled.div` + width: 122.4rem; + margin: 0 auto; +`; diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx index 4a9215e..d912689 100644 --- a/src/pages/profile/Profile.tsx +++ b/src/pages/profile/Profile.tsx @@ -16,14 +16,23 @@ import ProfileTicketLackModal from '../../components/profile/ProfileTicketLack/P import useIsTicketUsed from '../../hooks/profile/useIsTicketUsed'; import useTicketNumber from '../../hooks/profile/useTicketNumber'; import ProfileTicketUseModal from '../../components/profile/profileTicketUse/ProfileTicketUseModal'; +import useIsUserGetExternalReview from '../../hooks/profile/useIsUserGetExternalReview'; const Profile = () => { const setHeaderSelected = useSetRecoilState(headerSelectedState); const { userId } = useParams(); + + // 인증 미필요 const { profileData, isLoading } = useProfile(userId as string); const { profileReviewData, isLoadingReview } = useProfileReview( userId as string, ); + const { isUserGetExternalReviewData } = useIsUserGetExternalReview( + userId as string, + ); + // console.log(isUserGetExternalReviewData); + + // 인증 필요 const { IsTicketUsedData } = useIsTicketUsed(userId as string); const { TicketNumberData } = useTicketNumber(); @@ -62,11 +71,17 @@ const Profile = () => { { isLocked={!IsTicketUsedData?.data.data.isUsed} setIsLackModalVisible={setIsLackModalVisible} setIsUseModalVisible={setIsUseModalVisible} + isUserGetExternalReview={ + isUserGetExternalReviewData?.data.data.alreadyReviewed + } /> ); diff --git a/src/pages/request/Request.tsx b/src/pages/request/Request.tsx index affda79..0f23c95 100644 --- a/src/pages/request/Request.tsx +++ b/src/pages/request/Request.tsx @@ -6,10 +6,11 @@ import bgSrc from '/assets/images/request/request-bg.png'; import starSrc from '/assets/images/common/star.svg'; import kakaotalkSrc from '/assets/images/request/request-kakaotalk.svg'; import OneButtonModal from '../../components/common/OneButtonModal'; -import ModalInner from '../../components/request/ModalInner'; + import { kakao } from '../../components/login/KakaoLogin'; import { loginInfoState } from '../../recoil/atom'; import JoinCompleteModal from '../../components/join/JoinCompleteModal'; +import ModalInner from '../../components/request/ModalInner'; const TITLE = '매력적인 프로필 완성을 위해 추천사를 요청해보세요.'; const CONTENT = [ '나와 딱 맞는 탁월한 팀원을 한번에 찾고 싶다면,\n나보다 나를 더 잘 아는 동료에게 추천사를 요청해 멋진 프로필을 완성하세요.', @@ -33,7 +34,7 @@ const Request = () => { templateId: 99541, templateArgs: { name: loginUserInfo.data?.name, - userId : loginUserInfo.data?.userId + userId: loginUserInfo.data?.userId, }, // serverCallbackArgs: { // isSendSuccess: 'no', // 사용자 정의 파라미터 설정 diff --git a/src/recoil/atom.ts b/src/recoil/atom.ts index 2864108..f9f96ac 100644 --- a/src/recoil/atom.ts +++ b/src/recoil/atom.ts @@ -36,3 +36,7 @@ export const loginModalState = atom({ key: 'loginModal', default: false, }); +export const needKakaoReviewModalState = atom({ + key: 'needKakaoReviewModal', + default: false, +}); \ No newline at end of file diff --git a/src/recoil/myteam.ts b/src/recoil/myteam.ts index 625d915..4776fba 100644 --- a/src/recoil/myteam.ts +++ b/src/recoil/myteam.ts @@ -1,6 +1,10 @@ import { atom } from 'recoil'; +import { recoilPersist } from 'recoil-persist'; + +const { persistAtom } = recoilPersist(); export const selectedTeamAtom = atom({ key: 'selectedTeamAtom', default: 0, + effects_UNSTABLE: [persistAtom], });