diff --git a/src/components/common/ProtectRoute.tsx b/src/components/common/ProtectRoute.tsx index 9cd173e8..47199d48 100644 --- a/src/components/common/ProtectRoute.tsx +++ b/src/components/common/ProtectRoute.tsx @@ -1,16 +1,38 @@ -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, useEffect, useState } from 'react'; import useAuthStore from '../../store/authStore'; import { Navigate } from 'react-router-dom'; +import { useModal } from '../../hooks/useModal'; +import Modal from './modal/Modal'; +import { MODAL_MESSAGE } from '../../constants/modalMessage'; interface ProtectRouteProps extends PropsWithChildren { redirectUrl: string; } const ProtectRoute = ({ children, redirectUrl }: ProtectRouteProps) => { const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const [shouldRedirect, setShouldRedirect] = useState(false); + + useEffect(() => { + if (!isLoggedIn) { + handleModalOpen(MODAL_MESSAGE.isNotLoggedIn); + const timer = setTimeout(() => setShouldRedirect(true), 1000); + return () => clearTimeout(timer); + } else { + setShouldRedirect(false); + } + }, [isLoggedIn, handleModalOpen]); if (!isLoggedIn) { - alert('로그인이 필요한 서비스입니다.'); - return ; + if (shouldRedirect) { + return ; + } + + return ( + + {message} + + ); } return <>{children}; diff --git a/src/components/mypage/ContentTab.styled.ts b/src/components/mypage/ContentTab.styled.ts index f40f53c5..974acd21 100644 --- a/src/components/mypage/ContentTab.styled.ts +++ b/src/components/mypage/ContentTab.styled.ts @@ -72,32 +72,6 @@ export const WrapperButton = styled.div<{ $height: string }>` padding: 0.5rem; `; -export const ScrollWrapper = styled.div<{ $height: string }>` - width: 100%; - height: calc(100% - ${({ $height }) => $height}); - overflow-y: auto; - background: ${({ theme }) => theme.color.lightgrey}; - border-radius: ${({ theme }) => theme.borderRadius.large}; - - &::-webkit-scrollbar { - width: 10px; - } - - &::-webkit-scrollbar-thumb { - background: #3e5879; - border-radius: 10px; - } - - &::-webkit-scrollbar-track { - background-color: transparent; - border-radius: 10px; - } - - &::-webkit-scrollbar-thumb:hover { - background: rgb(65, 100, 146); - } -`; - export const FilterContainer = styled.div` width: 100%; height: 100%; diff --git a/src/components/mypage/ContentTab.tsx b/src/components/mypage/ContentTab.tsx index b32a4c40..42568ec2 100644 --- a/src/components/mypage/ContentTab.tsx +++ b/src/components/mypage/ContentTab.tsx @@ -3,6 +3,7 @@ import * as S from './ContentTab.styled'; import { Link, Outlet, useLocation } from 'react-router-dom'; import { ROUTES } from '../../constants/routes'; import MovedInquiredLink from '../../pages/customerService/MoveInquiredLink'; +import ScrollWrapper from './ScrollWrapper'; interface Filter { title: string; @@ -60,18 +61,18 @@ export default function ContentTab({ filter, $justifyContent }: ContentProps) { - + - + ) : ( - + - + )} ); diff --git a/src/components/mypage/ScrollWrapper.styled.ts b/src/components/mypage/ScrollWrapper.styled.ts new file mode 100644 index 00000000..dc69ea1b --- /dev/null +++ b/src/components/mypage/ScrollWrapper.styled.ts @@ -0,0 +1,27 @@ +import styled from 'styled-components'; + +export const ScrollWrapper = styled.div<{ $height: string }>` + width: 100%; + height: calc(100% - ${({ $height }) => $height}); + overflow-y: auto; + background: ${({ theme }) => theme.color.lightgrey}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + + &::-webkit-scrollbar { + width: 10px; + } + + &::-webkit-scrollbar-thumb { + background: #3e5879; + border-radius: 10px; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + border-radius: 10px; + } + + &::-webkit-scrollbar-thumb:hover { + background: rgb(65, 100, 146); + } +`; diff --git a/src/components/mypage/ScrollWrapper.tsx b/src/components/mypage/ScrollWrapper.tsx new file mode 100644 index 00000000..8e6a6144 --- /dev/null +++ b/src/components/mypage/ScrollWrapper.tsx @@ -0,0 +1,19 @@ +import * as S from './ScrollWrapper.styled'; + +interface ScrollWrapperProps { + children: React.ReactNode; + $height?: string; + scrollRef?: React.RefObject | null; +} + +export default function ScrollWrapper({ + children, + $height = '0%', + scrollRef = null, +}: ScrollWrapperProps) { + return ( + + {children} + + ); +} diff --git a/src/components/mypage/activityLog/ActivityLog.tsx b/src/components/mypage/activityLog/ActivityLog.tsx index 3bf257a7..a601ee58 100644 --- a/src/components/mypage/activityLog/ActivityLog.tsx +++ b/src/components/mypage/activityLog/ActivityLog.tsx @@ -2,9 +2,5 @@ import { ACTIVITY_FILTER } from '../../../constants/myPageFilter'; import ContentTab from '../ContentTab'; export default function ActivityLog() { - return ( - <> - - - ); + return ; } diff --git a/src/components/mypage/activityLog/commentsActivity/CommentsActivity.tsx b/src/components/mypage/activityLog/commentsActivity/CommentsActivity.tsx index 3f7874bf..58405def 100644 --- a/src/components/mypage/activityLog/commentsActivity/CommentsActivity.tsx +++ b/src/components/mypage/activityLog/commentsActivity/CommentsActivity.tsx @@ -9,7 +9,7 @@ export default function CommentsActivity() { const { myCommentsData, isLoading } = useGetMyComments(); if (isLoading) { - return ; + return ; } if (!myCommentsData || myCommentsData.length === 0) { diff --git a/src/components/mypage/activityLog/inquiries/Inquiries.tsx b/src/components/mypage/activityLog/inquiries/Inquiries.tsx index d9654ec3..99b4d414 100644 --- a/src/components/mypage/activityLog/inquiries/Inquiries.tsx +++ b/src/components/mypage/activityLog/inquiries/Inquiries.tsx @@ -8,7 +8,7 @@ export default function Inquiries() { const { myInquiriesData, isLoading } = useGetMyInquiries(); if (isLoading) { - return ; + return ; } if (!myInquiriesData || myInquiriesData?.length === 0) diff --git a/src/components/mypage/joinedProject/MyJoinProjects.styled.ts b/src/components/mypage/joinedProject/MyJoinProjects.styled.ts index f34130da..d1e9efeb 100644 --- a/src/components/mypage/joinedProject/MyJoinProjects.styled.ts +++ b/src/components/mypage/joinedProject/MyJoinProjects.styled.ts @@ -50,28 +50,3 @@ export const WrapperProject = styled.div` width: 46%; } `; - -export const ScrollWrapper = styled.div` - width: 100%; - height: 100%; - max-height: 80vh; - overflow-y: auto; - - &::-webkit-scrollbar { - width: 10px; - } - - &::-webkit-scrollbar-thumb { - background: #3e5879; - border-radius: 10px; - } - - &::-webkit-scrollbar-track { - background-color: transparent; - border-radius: 10px; - } - - &::-webkit-scrollbar-thumb:hover { - background: rgb(65, 100, 146); - } -`; diff --git a/src/components/mypage/joinedProject/MyJoinProjects.tsx b/src/components/mypage/joinedProject/MyJoinProjects.tsx index ce0d37f1..089da270 100644 --- a/src/components/mypage/joinedProject/MyJoinProjects.tsx +++ b/src/components/mypage/joinedProject/MyJoinProjects.tsx @@ -5,12 +5,13 @@ import Project from './Project'; import { ROUTES } from '../../../constants/routes'; import NoContent from '../../common/noContent/NoContent'; import Spinner from '../Spinner'; +import ScrollWrapper from '../ScrollWrapper'; const MyJoinProjects = () => { const { myJoinedProjectListData, isLoading } = useMyJoinedProjectList(); if (isLoading) { - return ; + return ; } if (!myJoinedProjectListData) return; @@ -20,7 +21,7 @@ const MyJoinProjects = () => { 참여한 프로젝트 리스트 {myJoinedProjectListData && myJoinedProjectListData?.length > 0 ? ( - + {myJoinedProjectListData?.map((project) => ( { ))} - + ) : ( diff --git a/src/components/mypage/myProfile/MyProfile.styled.ts b/src/components/mypage/myProfile/MyProfile.styled.ts index b31897f3..017ee3b6 100644 --- a/src/components/mypage/myProfile/MyProfile.styled.ts +++ b/src/components/mypage/myProfile/MyProfile.styled.ts @@ -1,464 +1,38 @@ +import { Link } from 'react-router-dom'; import styled from 'styled-components'; -export const Box = styled.div` - position: relative; -`; - export const FilterWrapper = styled.div` display: flex; padding: 1rem 1.2rem; - justify-content: start; + justify-content: space-between; `; export const FilterTitle = styled.h1` font-size: 1.5em; `; -export const TitleWrapper = styled.div``; - -export const Container = styled.div` - background-color: ${({ theme }) => theme.color.lightgrey}; - border-radius: ${({ theme }) => - `${theme.borderRadius.large} 0 0 ${theme.borderRadius.large}`}; - padding: 2rem; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - padding: 2rem; - } - - form { - display: flex; - flex-direction: column; - align-items: flex-end; - } -`; - -export const BackgroundWrapper = styled.div` - background-color: #fff; - display: flex; - padding: 0.5rem 1.3rem; - border-radius: 15px; - - div { - width: 100%; - display: flex; - gap: 13px; - span { - width: content; - word-break: break-all; - overflow-wrap: break-word; - } - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } - } - - li { - span { - font-size: 0.7rem; - } - } -`; - -export const BackgroundBox = styled.div` - background-color: #fff; - display: flex; - padding: 1rem 1.3rem; - border-radius: 15px; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - padding: 1.2rem; - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } -`; - -export const ProfileSection = styled.div` - display: flex; - flex-direction: column; - gap: 1.25rem; - - button { - position: absolute; - top: 0.5rem; - right: 1.2rem; - background-color: #3e5879; - padding: 0.5rem; - border-radius: 50%; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - padding: 0.4rem; - } - - svg { - width: 1.3rem; - height: 1.3rem; - color: ${({ theme }) => theme.color.white}; - } - } - - a { - width: fit-content; - display: inline-block; - padding: 0.5rem 0.7rem; - border-radius: ${({ theme }) => theme.borderRadius.large}; - background-color: #3e5879; - color: ${({ theme }) => theme.color.white}; - font-size: 0.8rem; - margin-top: 1rem; - } -`; - -export const NicknameBackgroundBox = styled.div` - background-color: #fff; - display: flex; - padding: 1rem 1.3rem; - gap: 1rem; - border-radius: 15px; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - padding: 1.2rem; - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } -`; - -export const NicknameSpan = styled.span` - display: flex; - align-items: center; -`; - -export const IconWrapper = styled.div` - width: fit-content; - height: fit-content; +export const EditLink = styled(Link)` display: flex; justify-content: center; align-items: center; - background-color: ${({ theme }) => theme.color.white}; - padding: 0.2rem; - border-radius: 50%; - border: 1px solid #f0f0f0; -`; - -export const Bio = styled.p` - white-space: pre-line; - word-break: break-word; -`; - -export const Wrapper = styled.div` - display: flex; - flex-wrap: wrap; - gap: 1rem; - align-items: center; - - label { - font-weight: 700; - color: ${({ theme }) => theme.color.deepGrey}; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } - } - - ul { - display: flex; - flex-wrap: wrap; - gap: 13px; - - li { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - font-size: 0.7rem; - color: #a1a1a1; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 0.7rem; - } - - img { - background-color: white; - border-radius: 50%; - border: 1px solid #f0f0f0; - } - } - } - - button { - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: 0.9rem; - } - } -`; - -export const List = styled.div` - display: flex; - gap: 1rem; - flex-direction: column; - - label { - font-weight: 700; - color: ${({ theme }) => theme.color.deepGrey}; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } - } - ul { - display: flex; - flex-direction: column; - gap: 10px; - - li { - color: #a1a1a1; - - span { - color: ${({ theme }) => theme.color.primary}; - } - } - } -`; - -export const Form = styled.form` - display: flex; - gap: 3rem; -`; - -export const EditWrapper = styled.div` - display: flex; - flex-wrap: wrap; - gap: 1rem; - width: 100%; - align-items: center; - - label { - font-weight: 700; - color: ${({ theme }) => theme.color.deepGrey}; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } - } - - button { - margin-left: 1rem; - padding: 0.65rem 1rem; - min-width: 60px; - font-size: 0.9rem; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } - } -`; - -export const InputTextNickname = styled.div` - width: 20%; - min-width: 125px; -`; - -export const InputBeginner = styled.input` - accent-color: ${({ theme }) => theme.color.navy}; -`; - -export const InputTextGithub = styled.div` - width: 100%; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - width: 100%; - } -`; - -export const InputTextCareer = styled.div` - width: 100%; -`; - -export const InputWrapper = styled.div` - display: flex; - align-items: center; - position: relative; - width: 85%; - - input { - width: 100%; - } - - @media ${({ theme }) => theme.mediaQuery.tablet} { - width: 100%; - } -`; - -export const ErrorMessage = styled.span<{ message?: string }>` - position: absolute; - bottom: -1.8rem; - left: 0.5rem; - display: inline-block; - color: #d43636; - font-size: 0.8rem; - height: 1.2rem; -`; - -export const ErrorCareerMessage = styled.span<{ message?: string }>` - display: inline-block; - color: #d43636; - font-size: 0.7rem; -`; - -export const EditContainer = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 1rem; - position: relative; - - label { - font-weight: 700; - color: ${({ theme }) => theme.color.deepGrey}; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; - } - } -`; - -export const EditList = styled.div` - display: flex; - flex-wrap: wrap; - gap: 1rem; - width: 100%; - position: relative; - - background-color: ${({ theme }) => theme.color.white}; - padding: 1rem; - border-radius: 20px; - border: 1px solid #ccc; -`; - -export const CareerList = styled.div` - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 0.2fr; - align-items: center; - width: 100%; - gap: 0.5rem; - background-color: ${({ theme }) => theme.color.lightgrey}; - border-radius: 20px; - padding: 0.5rem; - - @media ${({ theme }) => theme.mediaQuery.tablet} { - gap: 0.4rem; - } -`; - -export const CareerWrapper = styled.div` - @media ${({ theme }) => theme.mediaQuery.tablet} { - flex: auto; - } -`; - -export const XMarkButton = styled.button` - display: flex; - justify-content: center; - align-items: center; - width: 22px; - height: 22px; - border-radius: 50%; background-color: #3e5879; - svg { - color: ${({ theme }) => theme.color.white}; - } - width: 30px; - height: 30px; - border-radius: 50%; -`; - -export const CareerAddButton = styled.button` - background-color: #3e5879; - width: 30px; - height: 30px; + padding: 0.5rem; border-radius: 50%; - position: absolute; - bottom: -2rem; - right: 0; svg { - color: ${({ theme }) => theme.color.white}; + stroke: white; + width: 20px; + height: 20px; } `; -export const ScrollWrapper = styled.div` +export const Container = styled.div` width: 100%; - height: 100%; - max-height: 60vh; - overflow: auto; - - &::-webkit-scrollbar { - width: 10px; - } - - &::-webkit-scrollbar-thumb { - background: #3e5879; - border-radius: 10px; - } - - &::-webkit-scrollbar-track { - background-color: transparent; - border-radius: 10px; - } - - &::-webkit-scrollbar-thumb:hover { - background: rgb(65, 100, 146); - } -`; - -export const LabelBox = styled.div` - display: flex; - overflow: visible; - position: relative; -`; - -export const ChartBox = styled.div` - width: 250px; - height: 250px; -`; - -export const TooltipContainer = styled.div` - position: relative; - display: inline-block; - margin-left: 15px; + height: 85%; `; -export const ExplainBox = styled.div` - position: relative; - margin-left: 12px; -`; - -export const TooltipBox = styled.div` - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(20%) translateY(50px); - - width: 220px; - background-color: ${({ theme }) => theme.buttonScheme.primary.bg}; - color: ${({ theme }) => theme.color.white}; - font-size: 0.65rem; - padding: 0.6rem; - border-radius: ${({ theme }) => theme.borderRadius.primary}; - - visibility: hidden; - z-index: 1000; - - ${ExplainBox}:hover & { - visibility: visible; - } -`; - -export const Explain = styled.p` - display: inline-block; - padding: 2px 8px; - background-color: ${({ theme }) => theme.buttonScheme.primary.bg}; - color: ${({ theme }) => theme.color.white}; - border-radius: ${({ theme }) => theme.borderRadius.primary}; - font-size: 0.75rem; - cursor: pointer; - user-select: none; +export const SectionContainer = styled.section` + background-color: ${({ theme }) => theme.color.lightgrey}; + border-radius: ${({ theme }) => theme.borderRadius.large} 0 0 + ${({ theme }) => theme.borderRadius.large}; + padding: 2rem; `; diff --git a/src/components/mypage/myProfile/MyProfile.tsx b/src/components/mypage/myProfile/MyProfile.tsx index 7769e0c3..08fbe80d 100644 --- a/src/components/mypage/myProfile/MyProfile.tsx +++ b/src/components/mypage/myProfile/MyProfile.tsx @@ -1,214 +1,23 @@ -import { z } from 'zod'; import * as S from './MyProfile.styled'; -import { ERROR_MESSAGES } from '../../../constants/authConstants'; -import { Controller, useFieldArray, useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect, useState } from 'react'; -import { useSearchFilteringSkillTag } from '../../../hooks/useSearchFilteringSkillTag'; -import useNickNameVerification from '../../../hooks/useNicknameVerification'; -import InputText from '../../auth/InputText'; -import Button from '../../common/Button/Button'; -import { - useEditMyProfileInfo, - useMyProfileInfo, -} from '../../../hooks/useMyInfo'; -import { - PencilIcon, - XMarkIcon, - SquaresPlusIcon, -} from '@heroicons/react/24/outline'; -import { ROUTES } from '../../../constants/routes'; -import { Link } from 'react-router-dom'; -import BeginnerIcon from '../../../assets/beginner.svg'; -import OptionBox from './OptionBox'; -import TextareaAutosize from 'react-textarea-autosize'; -import Modal from '../../common/modal/Modal'; -import { useModal } from '../../../hooks/useModal'; import Spinner from '../Spinner'; +import { useMyProfileInfo } from '../../../hooks/useMyInfo'; import NoMyInfo from './NoMyInfo'; -import 'chart.js/auto'; -import { Radar } from 'react-chartjs-2'; -import { ChartOptions } from 'chart.js'; - -const profileSchema = z.object({ - nickname: z - .string() - .nonempty(ERROR_MESSAGES.NICKNAME_REQUIRED) - .max(6, ERROR_MESSAGES.NICKNAME_LENGTH) - .regex( - /^[a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ0-9~`!@#$%^&*()\-_=+]{1,6}$/, - ERROR_MESSAGES.NICKNAME_FORMAT - ), - beginner: z.boolean(), - skillTagIds: z.array(z.number()).min(1, ERROR_MESSAGES.SKILL_REQUIRED), - positionTagIds: z.array(z.number()).min(1, ERROR_MESSAGES.POSITION_REQUIRED), - github: z - .string() - .optional() - .refine( - (val) => !val || /^https?:\/\/[^\s$.?#].[^\s]*$/.test(val), - ERROR_MESSAGES.GITHUB_SPECIAL - ) - .transform((val) => (val === '' ? '' : val || '')), - career: z - .array( - z - .object({ - name: z.string().nonempty(ERROR_MESSAGES.CAREERNAME_REQUIRED), - periodStart: z.string().nonempty(ERROR_MESSAGES.STARTPERIOD_REQUIRED), - periodEnd: z.string().nonempty(ERROR_MESSAGES.ENDPERIOD_REQUIRED), - role: z.string().nonempty(ERROR_MESSAGES.ROLE_REQUIRED), - }) - .refine( - (data) => new Date(data.periodStart) <= new Date(data.periodEnd), - { - message: ERROR_MESSAGES.ENDPERIOD_SPECIAL, - path: ['periodEnd'], - } - ) - ) - .optional(), - bio: z.string().optional(), -}); - -type ProfileFormData = z.infer; - -const chartData = { - labels: ['책임감', '기획력', '협업능력', '성실도', '문제해결', '기술력'], - datasets: [ - { - label: '팀 점수', - data: [6.6, 5.2, 9.1, 5.6, 5.5, 8.4], - backgroundColor: 'rgba(255, 108, 61, 0.2)', - }, - ], -}; - -const chartOptions: ChartOptions<'radar'> & ChartOptions = { - elements: { - //데이터 속성. - line: { - borderWidth: 2, - borderColor: '#ff0000', - }, - //데이터 꼭짓점. - // point: { - // pointBackgroundColor: '#ff0000', - // }, - }, - scales: { - r: { - ticks: { - stepSize: 2.5, - display: false, - }, - grid: { - color: '#ececec', - }, - //라벨 속성 지정. - pointLabels: { - font: { - size: 12, - weight: 200, - family: 'Pretendard', - }, - color: '#000000', - }, - angleLines: { - display: false, - }, - suggestedMin: 0, - suggestedMax: 10, - }, - }, - responsive: true, - //위에 생기는 데이터 속성 label 타이틀을 지워줍니다. - plugins: { - legend: { - display: false, - }, - }, - //기본 값은 가운데에서 펴져나가는 애니메이션 형태입니다. - animation: { - duration: 0, - }, -}; +import Modal from '../../common/modal/Modal'; +import { useModal } from '../../../hooks/useModal'; +import { Outlet, useLocation } from 'react-router-dom'; +import { ROUTES } from '../../../constants/routes'; +import { PencilIcon } from '@heroicons/react/24/outline'; +import { useRef } from 'react'; +import ScrollWrapper from '../ScrollWrapper'; const MyProfile = () => { - const [isEditing, setIsEditing] = useState(false); - const [nickname, setNickname] = useState(''); - const { skillTagsData, positionTagsData } = useSearchFilteringSkillTag(); - const { nicknameMessage, handleDuplicationNickname } = - useNickNameVerification(); - const { myData, isLoading } = useMyProfileInfo(); const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); - const { editMyProfile } = useEditMyProfileInfo(handleModalOpen); - - const { - control, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - resolver: zodResolver(profileSchema), - defaultValues: { - nickname: '', - beginner: false, - skillTagIds: [], - positionTagIds: [], - github: '', - career: [], - bio: '', - }, - mode: 'onChange', - }); - - useEffect(() => { - if (myData) { - const skillTagIds = myData.skills - .map( - (skill) => skillTagsData.find((tag) => tag.name === skill.name)?.id - ) - .filter((id): id is number => id !== undefined); - - const positionTagIds = myData.positions - .map( - (position) => - positionTagsData.find((tag) => tag.id === position.id)?.id - ) - .filter((id): id is number => id !== undefined); - - reset({ - nickname: myData.nickname, - bio: myData.bio || '', - beginner: myData.beginner, - positionTagIds, - github: myData.github || '', - skillTagIds, - career: myData.career?.length - ? myData.career.map((item) => ({ - name: item.name, - periodStart: item.periodStart.split('T')[0], - periodEnd: item.periodEnd.split('T')[0], - role: item.role, - })) - : [{ name: '', periodStart: '', periodEnd: '', role: '' }], - }); - } - }, [myData, skillTagsData, positionTagsData, reset]); - - const { fields, append, remove } = useFieldArray({ control, name: 'career' }); - - const onSubmit = (data: ProfileFormData, e?: React.BaseSyntheticEvent) => { - e?.preventDefault(); - - editMyProfile(data); - setIsEditing(false); - }; + const location = useLocation(); + const scrollRef = useRef(null); if (isLoading) { - return ; + return ; } if (!myData) { @@ -216,457 +25,24 @@ const MyProfile = () => { } return ( - + 프로필 + {!location.pathname.includes(ROUTES.mypageEdit) && ( + + + + )} - - - {!isEditing ? ( - - - - - {myData.nickname} - {Boolean(myData.beginner) && ( - - beginner - - )} - - - - - -
    - {myData.skills.map((skill) => ( -
  • - {skill.name} - {skill.name} -
  • - )) || '스킬을 선택해주세요.'} -
-
-
- - - -
- {myData.positions - .sort() - .map((position) => ( - {position.name} - )) || '포지션을 선택해주세요.'} -
-
-
- - - - {myData.github || '깃허브 링크를 올려보세요.'} - - - - - -
    - {myData && !!myData.career?.length ? ( - myData.career?.map((career) => ( -
  • - {career.name} ( - {career.periodStart.slice(0, 10)} ~{' '} - {career.periodEnd.slice(0, 10)}{' '} - - {career.role}) -
  • - )) - ) : ( -
  • 경력을 기록하세요.
  • - )} -
-
-
- - - - {myData.bio || '내 소개를 적어주세요.'} - - - - - - - - 평가도란? - - 평가도는 프로젝트 평가 단계에서 팀원들의 평가로 점수가 - 부여됩니다. -
- 공고자가 회원을 평가하는 지표로 활용될 수 있습니다. -
-
-
- - - - - -
- 비밀번호 재설정 - -
- ) : ( - - {/* 닉네임 */} - - - ( - - - { - const value = e.target.value; - field.onChange(e); - setNickname(value); - }} - /> - - {errors.nickname && ( - - {errors.nickname.message} - - )} - {!errors.nickname && ( - {nicknameMessage} - )} - - - )} - /> - - - - ( - { - const checked = e.target.checked; - field.onChange(checked); - }} - /> - )} - /> - - - {/* 스킬셋 */} - - - ( - - {skillTagsData - .filter((skill) => skill.id !== 0) - .map((skill) => ( - { - if (isChecked) { - field.onChange([...field.value, id].sort()); - } else { - field.onChange( - field.value.filter((value) => value !== id) - ); - } - }} - imgSrc={skill.img} - /> - ))} - {errors.skillTagIds && ( - - {errors.skillTagIds.message} - - )} - - )} - /> - - - {/* 포지션 */} - - - ( - - {positionTagsData - .filter((position) => position.name !== '전체') - .map((position) => ( - { - if (isChecked) { - field.onChange([...field.value, id].sort()); - } else { - field.onChange( - field.value.filter((value) => value !== id) - ); - } - }} - /> - ))} - {errors.positionTagIds && ( - - {errors.positionTagIds.message} - - )} - - )} - /> - - - {/* 깃허브 */} - - - ( - - - - - {errors.github && ( - {errors.github.message} - )} - - )} - /> - - - {/* 경력 */} - - - - {fields.map((field, index) => ( - - ( - - - - - {errors.career?.[index]?.name && ( - - {errors.career[index]?.name?.message} - - )} - - )} - /> - ( - - - field.onChange(e.target.value)} - /> - - {errors.career?.[index]?.periodStart && ( - - {errors.career[index]?.periodStart?.message} - - )} - - )} - /> - ( - - - field.onChange(e.target.value)} - /> - - {errors.career?.[index]?.periodEnd && ( - - {errors.career[index]?.periodEnd?.message} - - )} - - )} - /> - ( - - - - - - - {errors.career?.[index]?.role && ( - - {errors.career[index]?.role?.message} - - )} - - )} - /> - - remove(index)}> - - - - - ))} - - - append({ - name: '', - periodStart: '', - periodEnd: '', - role: '', - }) - } - > - - - {errors.career && ( - {errors.career.message} - )} - - - {/* 소개 */} - - - - ( - - )} - /> - {errors.bio && ( - {errors.bio.message} - )} - - - - - - - - )} -
-
+ + + + + {message} -
+ ); }; diff --git a/src/components/mypage/myProfile/MyProfileWrapper.tsx b/src/components/mypage/myProfile/MyProfileWrapper.tsx new file mode 100644 index 00000000..b994e7fb --- /dev/null +++ b/src/components/mypage/myProfile/MyProfileWrapper.tsx @@ -0,0 +1,9 @@ +import * as S from './MyPropfileWrapper.styled'; + +interface MyProfileWrapperProps { + children: React.ReactNode; +} + +export default function MyProfileWrapper({ children }: MyProfileWrapperProps) { + return {children}; +} diff --git a/src/components/mypage/myProfile/MyPropfileWrapper.styled.ts b/src/components/mypage/myProfile/MyPropfileWrapper.styled.ts new file mode 100644 index 00000000..42c11e51 --- /dev/null +++ b/src/components/mypage/myProfile/MyPropfileWrapper.styled.ts @@ -0,0 +1,48 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + + label { + font-weight: 700; + color: ${({ theme }) => theme.color.deepGrey}; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } + } + + ul { + display: flex; + flex-wrap: wrap; + gap: 13px; + + li { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 0.7rem; + color: #a1a1a1; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: 0.7rem; + } + + img { + background-color: white; + border-radius: 50%; + border: 1px solid #f0f0f0; + } + } + } + + button { + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: 0.9rem; + } + } +`; diff --git a/src/components/mypage/myProfile/editProfile/EditProfile.tsx b/src/components/mypage/myProfile/editProfile/EditProfile.tsx new file mode 100644 index 00000000..cefdd9c9 --- /dev/null +++ b/src/components/mypage/myProfile/editProfile/EditProfile.tsx @@ -0,0 +1,474 @@ +import * as S from './editProfile.styled'; +import Button from '../../../common/Button/Button'; +import OptionBox from './../OptionBox'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; +import { useEditMyProfileInfo } from '../../../../hooks/useMyInfo'; +import { useSearchFilteringSkillTag } from '../../../../hooks/useSearchFilteringSkillTag'; +import { useEffect, useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import TextareaAutosize from 'react-textarea-autosize'; +import useNickNameVerification from '../../../../hooks/useNicknameVerification'; +import InputText from '../../../auth/InputText'; +import { ERROR_MESSAGES } from '../../../../constants/authConstants'; +import { z } from 'zod'; +import { SquaresPlusIcon, XMarkIcon } from '@heroicons/react/24/outline'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { ROUTES } from '../../../../constants/routes'; +import { UserInfo } from '../../../../models/userInfo'; +import MyProfileWrapper from '../MyProfileWrapper'; + +type ProfileFormData = z.infer; + +export default function EditProfile() { + const [nickname, setNickname] = useState(''); + const { + myData, + scrollRef, + handleModalOpen, + }: { + myData: UserInfo; + scrollRef: React.RefObject; + handleModalOpen: (message: string) => void; + } = useOutletContext(); + const { skillTagsData, positionTagsData } = useSearchFilteringSkillTag(); + const { editMyProfile } = useEditMyProfileInfo(handleModalOpen); + const { nicknameMessage, handleDuplicationNickname } = + useNickNameVerification(); + const navigate = useNavigate(); + + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(profileSchema), + defaultValues: { + nickname: '', + beginner: false, + skillTagIds: [], + positionTagIds: [], + github: '', + career: [], + bio: '', + }, + mode: 'onChange', + }); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = 0; + } + }, [scrollRef]); + + useEffect(() => { + if (myData) { + const skillTagIds = myData.skills + .map( + (skill) => skillTagsData.find((tag) => tag.name === skill.name)?.id + ) + .filter((id): id is number => id !== undefined); + + const positionTagIds = myData.positions + .map( + (position) => + positionTagsData.find((tag) => tag.id === position.id)?.id + ) + .filter((id): id is number => id !== undefined); + + reset({ + nickname: myData.nickname, + bio: myData.bio || '', + beginner: myData.beginner, + positionTagIds, + github: myData.github || '', + skillTagIds, + career: myData.career?.length + ? myData.career.map((item) => ({ + name: item.name, + periodStart: item.periodStart.split('T')[0], + periodEnd: item.periodEnd.split('T')[0], + role: item.role, + })) + : [{ name: '', periodStart: '', periodEnd: '', role: '' }], + }); + } + }, [myData, skillTagsData, positionTagsData, reset]); + + const { fields, append, remove } = useFieldArray({ control, name: 'career' }); + + const onSubmit = (data: ProfileFormData, e?: React.BaseSyntheticEvent) => { + e?.preventDefault(); + + editMyProfile(data); + navigate(ROUTES.mypage); + }; + + return ( + + {/* 닉네임 */} + + + ( + + + { + const value = e.target.value; + field.onChange(e); + setNickname(value); + }} + /> + + {errors.nickname && ( + {errors.nickname.message} + )} + {!errors.nickname && ( + {nicknameMessage} + )} + + + )} + /> + + + + ( + { + const checked = e.target.checked; + field.onChange(checked); + }} + /> + )} + /> + + + {/* 스킬셋 */} + + + ( + + {skillTagsData + .filter((skill) => skill.id !== 0) + .map((skill) => ( + { + if (isChecked) { + field.onChange( + [...field.value, id].sort((a, b) => a - b) + ); + } else { + field.onChange( + field.value.filter((value) => value !== id) + ); + } + }} + imgSrc={skill.img} + /> + ))} + {errors.skillTagIds && ( + {errors.skillTagIds.message} + )} + + )} + /> + + + {/* 포지션 */} + + + ( + + {positionTagsData + .filter((position) => position.name !== '전체') + .map((position) => ( + { + if (isChecked) { + field.onChange( + [...field.value, id].sort((a, b) => a - b) + ); + } else { + field.onChange( + field.value.filter((value) => value !== id) + ); + } + }} + /> + ))} + {errors.positionTagIds && ( + {errors.positionTagIds.message} + )} + + )} + /> + + + {/* 깃허브 */} + + + ( + + + + + {errors.github && ( + {errors.github.message} + )} + + )} + /> + + + {/* 경력 */} + + + + {fields.map((field, index) => ( + + ( + + + + + {errors.career?.[index]?.name && ( + + {errors.career[index]?.name?.message} + + )} + + )} + /> + ( + + + field.onChange(e.target.value)} + /> + + {errors.career?.[index]?.periodStart && ( + + {errors.career[index]?.periodStart?.message} + + )} + + )} + /> + ( + + + field.onChange(e.target.value)} + /> + + {errors.career?.[index]?.periodEnd && ( + + {errors.career[index]?.periodEnd?.message} + + )} + + )} + /> + ( + + + + + + + {errors.career?.[index]?.role && ( + + {errors.career[index]?.role?.message} + + )} + + )} + /> + + remove(index)}> + + + + + ))} + + + append({ + name: '', + periodStart: '', + periodEnd: '', + role: '', + }) + } + > + + + {errors.career && ( + {errors.career.message} + )} + + + {/* 소개 */} + + + + ( + + )} + /> + {errors.bio && {errors.bio.message}} + + + + + + + + ); +} + +const profileSchema = z.object({ + nickname: z + .string() + .nonempty(ERROR_MESSAGES.NICKNAME_REQUIRED) + .max(6, ERROR_MESSAGES.NICKNAME_LENGTH) + .regex( + /^[a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ0-9~`!@#$%^&*()\-_=+]{1,6}$/, + ERROR_MESSAGES.NICKNAME_FORMAT + ), + beginner: z.boolean(), + skillTagIds: z.array(z.number()).min(1, ERROR_MESSAGES.SKILL_REQUIRED), + positionTagIds: z.array(z.number()).min(1, ERROR_MESSAGES.POSITION_REQUIRED), + github: z + .string() + .optional() + .refine( + (val) => !val || /^https?:\/\/[^\s$.?#].[^\s]*$/.test(val), + ERROR_MESSAGES.GITHUB_SPECIAL + ) + .transform((val) => (val === '' ? '' : val || '')), + career: z + .array( + z + .object({ + name: z.string().nonempty(ERROR_MESSAGES.CAREERNAME_REQUIRED), + periodStart: z.string().nonempty(ERROR_MESSAGES.STARTPERIOD_REQUIRED), + periodEnd: z.string().nonempty(ERROR_MESSAGES.ENDPERIOD_REQUIRED), + role: z.string().nonempty(ERROR_MESSAGES.ROLE_REQUIRED), + }) + .refine( + (data) => new Date(data.periodStart) <= new Date(data.periodEnd), + { + message: ERROR_MESSAGES.ENDPERIOD_SPECIAL, + path: ['periodEnd'], + } + ) + ) + .optional(), + bio: z.string().optional(), +}); diff --git a/src/components/mypage/myProfile/editProfile/editProfile.styled.ts b/src/components/mypage/myProfile/editProfile/editProfile.styled.ts new file mode 100644 index 00000000..2ff92941 --- /dev/null +++ b/src/components/mypage/myProfile/editProfile/editProfile.styled.ts @@ -0,0 +1,164 @@ +import styled from 'styled-components'; + +export const Form = styled.form` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +export const EditWrapper = styled.div` + display: flex; + flex-wrap: wrap; + gap: 1rem; + width: 100%; + align-items: center; + + label { + font-weight: 700; + color: ${({ theme }) => theme.color.deepGrey}; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } + } + + button { + margin-left: 1rem; + padding: 0.65rem 1rem; + min-width: 60px; + font-size: 0.9rem; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } + } +`; + +export const InputTextNickname = styled.div` + width: 20%; + min-width: 125px; +`; + +export const InputBeginner = styled.input` + accent-color: ${({ theme }) => theme.color.navy}; +`; + +export const InputTextGithub = styled.div` + width: 100%; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + width: 100%; + } +`; + +export const InputTextCareer = styled.div` + width: 100%; +`; + +export const InputWrapper = styled.div` + display: flex; + align-items: center; + position: relative; + width: 85%; + + input { + width: 100%; + } + + @media ${({ theme }) => theme.mediaQuery.tablet} { + width: 100%; + } +`; + +export const ErrorMessage = styled.span<{ message?: string }>` + position: absolute; + bottom: -1.8rem; + left: 0.5rem; + display: inline-block; + color: #d43636; + font-size: 0.8rem; + height: 1.2rem; +`; + +export const ErrorCareerMessage = styled.span<{ message?: string }>` + display: inline-block; + color: #d43636; + font-size: 0.7rem; +`; + +export const EditContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; + position: relative; + + label { + font-weight: 700; + color: ${({ theme }) => theme.color.deepGrey}; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } + } +`; + +export const EditList = styled.div` + display: flex; + flex-wrap: wrap; + gap: 1rem; + width: 100%; + position: relative; + + background-color: ${({ theme }) => theme.color.white}; + padding: 1rem; + border-radius: 20px; + border: 1px solid #ccc; +`; + +export const CareerList = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 0.2fr; + align-items: center; + width: 100%; + gap: 0.5rem; + background-color: ${({ theme }) => theme.color.lightgrey}; + border-radius: 20px; + padding: 0.5rem; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + gap: 0.4rem; + } +`; + +export const CareerWrapper = styled.div` + @media ${({ theme }) => theme.mediaQuery.tablet} { + flex: auto; + } +`; + +export const XMarkButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + width: 22px; + height: 22px; + border-radius: 50%; + background-color: #3e5879; + svg { + color: ${({ theme }) => theme.color.white}; + } +`; + +export const CareerAddButton = styled.button` + background-color: #3e5879; + width: 30px; + height: 30px; + border-radius: 50%; + position: absolute; + bottom: -2rem; + right: 0; + svg { + color: ${({ theme }) => theme.color.white}; + } +`; diff --git a/src/components/mypage/myProfile/profile/Profile.styled.ts b/src/components/mypage/myProfile/profile/Profile.styled.ts new file mode 100644 index 00000000..23176798 --- /dev/null +++ b/src/components/mypage/myProfile/profile/Profile.styled.ts @@ -0,0 +1,170 @@ +import styled from 'styled-components'; + +export const ProfileSection = styled.div` + display: flex; + flex-direction: column; + gap: 1.25rem; + + a { + width: fit-content; + display: inline-block; + padding: 0.5rem 0.7rem; + border-radius: ${({ theme }) => theme.borderRadius.large}; + background-color: #3e5879; + color: ${({ theme }) => theme.color.white}; + font-size: 0.8rem; + margin-top: 1rem; + } +`; + +export const BackgroundWrapper = styled.div` + background-color: #fff; + display: flex; + padding: 0.5rem 1.3rem; + border-radius: 15px; + + div { + width: 100%; + display: flex; + gap: 13px; + + span { + width: fit-content; + word-break: break-all; + overflow-wrap: break-word; + } + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } + } + + li { + span { + font-size: 0.7rem; + } + } +`; + +export const BackgroundBox = styled.div` + background-color: #fff; + display: flex; + padding: 1rem 1.3rem; + border-radius: 15px; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + padding: 1.2rem; + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } +`; + +export const NicknameBackgroundBox = styled.div` + background-color: #fff; + display: flex; + padding: 1rem 1.3rem; + gap: 1rem; + border-radius: 15px; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + padding: 1.2rem; + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } +`; + +export const NicknameSpan = styled.span` + display: flex; + align-items: center; +`; + +export const IconWrapper = styled.div` + width: fit-content; + height: fit-content; + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.color.white}; + padding: 0.2rem; + border-radius: 50%; + border: 1px solid #f0f0f0; +`; + +export const Bio = styled.p` + white-space: pre-line; + word-break: break-word; +`; + +export const List = styled.div` + display: flex; + gap: 1rem; + flex-direction: column; + + label { + font-weight: 700; + color: ${({ theme }) => theme.color.deepGrey}; + + @media ${({ theme }) => theme.mediaQuery.tablet} { + font-size: ${({ theme }) => theme.heading['semiSmall'].tabletFontSize}; + } + } + ul { + display: flex; + flex-direction: column; + gap: 10px; + + li { + color: #a1a1a1; + + span { + color: ${({ theme }) => theme.color.primary}; + } + } + } +`; + +export const LabelBox = styled.div` + display: flex; + overflow: visible; + position: relative; +`; + +export const ChartBox = styled.div` + width: 250px; + height: 250px; +`; + +export const ExplainBox = styled.div` + position: relative; + margin-left: 12px; +`; + +export const TooltipBox = styled.div` + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(20%) translateY(50px); + + width: 220px; + background-color: ${({ theme }) => theme.buttonScheme.primary.bg}; + color: ${({ theme }) => theme.color.white}; + font-size: 0.65rem; + padding: 0.6rem; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + + visibility: hidden; + z-index: 1000; + + ${ExplainBox}:hover & { + visibility: visible; + } +`; + +export const Explain = styled.p` + display: inline-block; + padding: 2px 8px; + background-color: ${({ theme }) => theme.buttonScheme.primary.bg}; + color: ${({ theme }) => theme.color.white}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + font-size: 0.75rem; + cursor: pointer; + user-select: none; +`; diff --git a/src/components/mypage/myProfile/profile/Profile.tsx b/src/components/mypage/myProfile/profile/Profile.tsx new file mode 100644 index 00000000..a35cda43 --- /dev/null +++ b/src/components/mypage/myProfile/profile/Profile.tsx @@ -0,0 +1,189 @@ +import * as S from './Profile.styled'; +import BeginnerIcon from '../../../../assets/beginner.svg'; +import 'chart.js/auto'; +import { ChartOptions } from 'chart.js'; +import { Link, useOutletContext } from 'react-router-dom'; +import { ROUTES } from '../../../../constants/routes'; +import { Radar } from 'react-chartjs-2'; +import { UserInfo } from '../../../../models/userInfo'; +import { useEffect } from 'react'; +import MyProfileWrapper from '../MyProfileWrapper'; + +export default function Profile() { + const { + myData, + scrollRef, + }: { myData: UserInfo; scrollRef: React.RefObject } = + useOutletContext(); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = 0; + } + }, [scrollRef]); + + return ( + + + + + {myData.nickname} + {Boolean(myData.beginner) && ( + + beginner + + )} + + + + + +
    + {myData.skills.length > 0 ? ( + myData.skills.map((skill) => ( +
  • + {skill.name} + {skill.name} +
  • + )) + ) : ( +
  • 스킬을 선택해주세요.
  • + )} +
+
+
+ + + +
+ {myData.skills.length > 0 ? ( + myData.positions + .sort() + .map((position) => ( + {position.name} + )) + ) : ( + 포지션을 선택해주세요. + )} +
+
+
+ + + + {myData.github || '깃허브 링크를 올려보세요.'} + + + + + +
    + {myData.career?.length ? ( + myData.career?.map((career) => ( +
  • + {career.name} ({career.periodStart.slice(0, 10)}{' '} + ~ {career.periodEnd.slice(0, 10)}{' '} + - {career.role}) +
  • + )) + ) : ( +
  • 경력을 기록하세요.
  • + )} +
+
+
+ + + + {myData.bio || '내 소개를 적어주세요.'} + + + + + + + + 평가도란? + + 평가도는 프로젝트 평가 단계에서 팀원들의 평가로 점수가 부여됩니다. +
+ 공고자가 회원을 평가하는 지표로 활용될 수 있습니다. +
+
+
+ + + + + +
+ 비밀번호 재설정 +
+ ); +} + +const chartData = { + labels: ['책임감', '기획력', '협업능력', '성실도', '문제해결', '기술력'], + datasets: [ + { + label: '팀 점수', + data: [6.6, 5.2, 9.1, 5.6, 5.5, 8.4], + backgroundColor: 'rgba(255, 108, 61, 0.2)', + }, + ], +}; + +const chartOptions: ChartOptions<'radar'> & ChartOptions = { + elements: { + //데이터 속성. + line: { + borderWidth: 2, + borderColor: '#ff0000', + }, + //데이터 꼭짓점. + // point: { + // pointBackgroundColor: '#ff0000', + // }, + }, + scales: { + r: { + ticks: { + stepSize: 2.5, + display: false, + }, + grid: { + color: '#ececec', + }, + //라벨 속성 지정. + pointLabels: { + font: { + size: 12, + weight: 200, + family: 'Pretendard', + }, + color: '#000000', + }, + angleLines: { + display: false, + }, + suggestedMin: 0, + suggestedMax: 10, + }, + }, + responsive: true, + //위에 생기는 데이터 속성 label 타이틀을 지워줍니다. + plugins: { + legend: { + display: false, + }, + }, + //기본 값은 가운데에서 펴져나가는 애니메이션 형태입니다. + animation: { + duration: 0, + }, +}; diff --git a/src/components/mypage/notifications/all/All.tsx b/src/components/mypage/notifications/all/All.tsx index fa0680b9..af7c190c 100644 --- a/src/components/mypage/notifications/all/All.tsx +++ b/src/components/mypage/notifications/all/All.tsx @@ -29,7 +29,7 @@ export default function All() { }; if (isLoading) { - return ; + return ; } if (!alarmListData || alarmListData.length === 0) { diff --git a/src/components/mypage/notifications/appliedProjects/AppliedProjects.tsx b/src/components/mypage/notifications/appliedProjects/AppliedProjects.tsx index 67ae7303..1d54407e 100644 --- a/src/components/mypage/notifications/appliedProjects/AppliedProjects.tsx +++ b/src/components/mypage/notifications/appliedProjects/AppliedProjects.tsx @@ -10,7 +10,7 @@ export default function AppliedProjects() { const { myAppliedStatusListData, isLoading } = useMyAppliedStatusList(); if (isLoading) { - return ; + return ; } if (!myAppliedStatusListData || myAppliedStatusListData.length === 0) { diff --git a/src/components/userPage/joinedProject/UserJoinProject.tsx b/src/components/userPage/joinedProject/UserJoinProject.tsx index faef6337..df526f74 100644 --- a/src/components/userPage/joinedProject/UserJoinProject.tsx +++ b/src/components/userPage/joinedProject/UserJoinProject.tsx @@ -13,7 +13,7 @@ const UserJoinProject = () => { ); if (isLoading) { - return ; + return ; } console.log('userJoinedProjectListData', userJoinedProjectListData); diff --git a/src/constants/modalMessage.ts b/src/constants/modalMessage.ts index ebc0c97d..36586c6a 100644 --- a/src/constants/modalMessage.ts +++ b/src/constants/modalMessage.ts @@ -1,4 +1,5 @@ export const MODAL_MESSAGE = { + isNotLoggedIn: '로그인이 필요한 서비스입니다.', pass: '지원자를 합격 리스트에 추가했습니다.', nonPass: '지원자를 불합격 리스트에 추가했습니다.', waiting: '지원자를 대기리스트에 추가했습니다', diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 155b437c..ee74a8bc 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -10,6 +10,7 @@ export const ROUTES = { manageProjectsRoot: '/manage', manageProjectsPassNonPass: '/manage/pass-nonpass', mypage: '/mypage', + mypageEdit: '/mypage/edit', mypageJoinedProjects: 'joined-projects', mypageAppliedProjects: 'apply-projects', myPageNotifications: 'notifications', diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts index 8a9c65bc..c034084e 100644 --- a/src/hooks/useModal.ts +++ b/src/hooks/useModal.ts @@ -1,18 +1,18 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; export const useModal = () => { const [isOpen, setIsOpen] = useState(false); const [message, setMessage] = useState(''); - const handleModalOpen = (newMessage: string) => { + const handleModalOpen = useCallback((newMessage: string) => { setMessage(newMessage); setIsOpen(true); - }; + }, []); - const handleModalClose = () => { + const handleModalClose = useCallback(() => { setMessage(''); setIsOpen(false); - }; + }, []); const handleOpenReportModal = () => { setIsOpen(true); diff --git a/src/hooks/useMyInfo.ts b/src/hooks/useMyInfo.ts index 82438476..435c46f3 100644 --- a/src/hooks/useMyInfo.ts +++ b/src/hooks/useMyInfo.ts @@ -44,7 +44,9 @@ export const useEditMyProfileInfo = ( onSuccess: () => { queryClient.invalidateQueries({ queryKey: myInfoKey.myProfile }); handleModalOpen(MODAL_MESSAGE.myProfileSuccess); - navigate(ROUTES.mypage); + setTimeout(() => { + navigate(ROUTES.mypage); + }, 1500); }, onError: () => { handleModalOpen(MODAL_MESSAGE.myProfileFail); diff --git a/src/pages/mypage/MyPage.styled.ts b/src/pages/mypage/MyPage.styled.ts index 2880e538..60fef053 100644 --- a/src/pages/mypage/MyPage.styled.ts +++ b/src/pages/mypage/MyPage.styled.ts @@ -14,38 +14,3 @@ export const Wrapper = styled.div` border-radius: 30px; padding: 2rem; `; - -// export const FilterWrapper = styled.div<{ $justifyContent: string }>` -// display: flex; -// padding: 1rem 1.2rem; -// justify-content: ${({ $justifyContent }) => $justifyContent}; -// `; - -// export const FilterTitle = styled.h1` -// font-size: 1.5em; -// `; - -export const ScrollWrapper = styled.div` - width: 100%; - height: 100%; - max-height: 80vh; - overflow-y: auto; - - &::-webkit-scrollbar { - width: 10px; - } - - &::-webkit-scrollbar-thumb { - background: #3e5879; - border-radius: 10px; - } - - &::-webkit-scrollbar-track { - background-color: transparent; - border-radius: 10px; - } - - &::-webkit-scrollbar-thumb:hover { - background: rgb(65, 100, 146); - } -`; diff --git a/src/pages/userpage/UserPage.tsx b/src/pages/userpage/UserPage.tsx index c610d64f..cdbd6c1d 100644 --- a/src/pages/userpage/UserPage.tsx +++ b/src/pages/userpage/UserPage.tsx @@ -5,6 +5,7 @@ import { DocumentTextIcon, UserIcon } from '@heroicons/react/24/outline'; import { ROUTES } from '../../constants/routes'; import { useUserProfileInfo } from '../../hooks/useUserInfo'; import loadingImg from '../../assets/loadingImg.svg'; +import ScrollWrapper from '../../components/mypage/ScrollWrapper'; const UserPage = () => { const { userId } = useParams(); @@ -31,9 +32,9 @@ const UserPage = () => { profileImage={isLoading ? loadingImg : userData?.profileImg} /> - + - + ); diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index a5b6c4cb..e2624297 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -39,6 +39,12 @@ const MyProjectList = lazy( const MyProfile = lazy( () => import('../components/mypage/myProfile/MyProfile') ); +const Profile = lazy( + () => import('../components/mypage/myProfile/profile/Profile') +); +const ProfileEdit = lazy( + () => import('../components/mypage/myProfile/editProfile/EditProfile') +); const MyProjectVolunteer = lazy( () => import('../pages/manage/myProjectVolunteer/MyProjectVolunteer') ); @@ -224,6 +230,13 @@ const AppRoutes = () => { { path: '', element: , + children: [ + { index: true, element: }, + { + path: ROUTES.mypageEdit, + element: , + }, + ], }, { path: ROUTES.mypageJoinedProjects,