diff --git a/index.html b/index.html index 6d4c8f3..b41f49f 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + ThinkTank diff --git a/package.json b/package.json index 9fbfe3d..57378d2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc && vite build", "preview": "vite preview", "format": "prettier --check ./src", diff --git a/src/App.tsx b/src/App.tsx index b2f172e..1431cda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,28 @@ import { Route, Routes, useNavigate } from 'react-router-dom'; import { routers } from './routes'; import Layout from './routes/Layout'; -import { Suspense } from 'react'; -import { ErrorBoundary } from './components/shared'; import ProtectedRoute from './routes/protectedRoute'; +import { ErrorBoundary } from './components/shared'; function App() { const navigate = useNavigate(); return ( - loading}> - - }> - {routers.map(({ path, element: Component, isProtected }) => - isProtected ? ( - } />} - /> - ) : ( - } /> - ), - )} - - - + + }> + {routers.map(({ path, element: Component, isProtected }) => + isProtected ? ( + } />} + /> + ) : ( + } /> + ), + )} + + ); } diff --git a/src/apis/article.ts b/src/apis/article.ts index 1856c0a..dff19fc 100644 --- a/src/apis/article.ts +++ b/src/apis/article.ts @@ -1,6 +1,5 @@ import { ArticleDetail, ArticleList } from '@/types'; import instance from './instance'; - export const getArticle = async (postId: string) => { const response = await instance.get(`/api/posts/${postId}`); return response.data as ArticleDetail; @@ -31,15 +30,20 @@ export const postArticle = async ( return response?.data; }; -export const postCheck = async ( - formData: Pick, - postId: string, -) => { - const newFormData = { - code: formData.code, - language: formData.language, - postId, - }; - const response = await instance.post('api/posts/submit', newFormData); +interface postCheckState { + code: string; + language: string; + postId: string; +} + +export const postCheck = async (formData: postCheckState) => { + const response = await instance.post('api/posts/submit', formData); + return response.data; +}; + +export const deleteArticle = async (postId: number): Promise => { + const response = await instance.delete('/api/posts', { + data: { postId: postId }, + }); return response.data; }; diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 30dfe57..1b62f36 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -1,19 +1,8 @@ - - import { getAccess } from '@/hooks/auth/useLocalStorage'; import instance from './instance'; export const getNewToken = async () => { const expiredToken = getAccess(); - - try { - const response = await instance.post('/api/reissue', { expiredToken: expiredToken }); - const newAccessToken = response.data.accessToken; - console.log('토큰 재발급 성공', newAccessToken); - return newAccessToken; - } catch (error) { - console.error('토큰 재발급 실패', error); - throw error; - } + const response = await instance.post('/api/reissue', { expiredToken: expiredToken }); + return response.data.accessToken; }; - diff --git a/src/apis/comment.ts b/src/apis/comment.ts index a6b9df3..1822ca2 100644 --- a/src/apis/comment.ts +++ b/src/apis/comment.ts @@ -3,10 +3,17 @@ import instance from './instance'; export const getComments = async (postId: string) => { const response = await instance.get(`api/posts/${postId}/comments`); - return response.data as CommentDate[]; + return response.data as CommentDate; }; export const postComment = async (comment: string, postId: string) => { const response = await instance.post('/api/comments', { postId, content: comment }); return response.data; }; + +export const deleteComment = async (postId: string, commentId: number) => { + const response = await instance.delete('/api/comments', { + data: { postId, commentId }, + }); + return response.data; +}; diff --git a/src/apis/instance.ts b/src/apis/instance.ts index d577abc..72ba1f0 100644 --- a/src/apis/instance.ts +++ b/src/apis/instance.ts @@ -2,7 +2,6 @@ import axios from 'axios'; import { getNewToken } from './auth'; import { getAccess, setAccess } from '@/hooks/auth/useLocalStorage'; - const instance = axios.create({ baseURL: import.meta.env.VITE_BASE_URL, headers: { @@ -24,23 +23,16 @@ instance.interceptors.response.use( const originalRequest = error.config; const access = getAccess(); if (error.response?.status === 401 && !originalRequest._alreadyRefreshed && access) { - originalRequest._alreadyRefreshed = true; // 무한 요청 방지 - try { - const newAccessToken = await getNewToken(); - if (newAccessToken) { - setAccess(newAccessToken); - originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; - return instance(originalRequest); - } - } catch (refreshError) { - console.error('토큰 발급 실패', refreshError); - // window.location.href = '/login'; - throw refreshError; + const newAccessToken = await getNewToken(); + if (newAccessToken) { + setAccess(newAccessToken); + originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + return instance(originalRequest); } } throw error; }, ); -export default instance; \ No newline at end of file +export default instance; diff --git a/src/apis/mypage.ts b/src/apis/mypage.ts index 2ffcb33..004e822 100644 --- a/src/apis/mypage.ts +++ b/src/apis/mypage.ts @@ -1,7 +1,7 @@ import { MypageArticles } from '@/types/mypage'; import instance from './instance'; -/** MYPAGE 게시글 조회 */ +/* MYPAGE 게시글 조회 */ export const getMypageArticles = async ({ page, size, value, email }: MypageArticles) => { try { const response = await instance.get('/api/users/profile', { @@ -21,7 +21,7 @@ export const getMypageArticles = async ({ page, size, value, email }: MypageArti } }; -/** 타인 프로필 조회 */ +/* 타인 프로필 조회 */ export const getOthersProfile = async (email: string | null) => { try { const response = await instance.get('/api/users/profile', { diff --git a/src/apis/user.ts b/src/apis/user.ts index c6c2b56..c2e7de6 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,20 +1,32 @@ import { Login, SignUp, User } from '@/types'; import instance from './instance'; +import { AxiosError } from 'axios'; -/** 로그인 **/ -export const postLogin = async ({ email, password }: Login) => { - const data = { email, password }; - const response = await instance.post('/api/login', data); - return response.data; -}; - -/** 회원가입 */ +/* 회원가입 */ export const postSignup = async (data: SignUp) => { const response = await instance.post('/api/signup', data); return response?.data; }; -/** User 정보 불러오기 */ +/* 로그인 */ +export const postLogin = async (data: Login) => { + try { + const response = await instance.post('/api/login', data); + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + throw error; + } + } +}; + +/* 카카오 로그인 */ +export const getKakaoLogin = async () => { + const response = await instance.get('/api/login/oauth2/kakao'); + return response.data; +}; + +/* User 정보 불러오기 */ export const getUserInfo = async () => { try { const response = await instance.get('/api/mypage/users'); @@ -24,8 +36,14 @@ export const getUserInfo = async () => { } }; -/** User 정보 수정 */ +/* User 정보 수정 */ export const putUser = async (data: Omit) => { const response = await instance.put('/api/mypage/users', data); return response.data; }; + +/* 탈퇴 */ +export const deleteUser = async () => { + const response = await instance.delete('/api/mypage/users'); + return response.data; +}; diff --git a/src/assets/images/TT.svg b/src/assets/images/TT.svg new file mode 100644 index 0000000..8859fba --- /dev/null +++ b/src/assets/images/TT.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/detail/commentItem/index.tsx b/src/components/detail/commentItem/index.tsx index e07aa6e..0f44eaa 100644 --- a/src/components/detail/commentItem/index.tsx +++ b/src/components/detail/commentItem/index.tsx @@ -1,24 +1,67 @@ -import { Icon, Text } from '@/components/shared'; +import { Text } from '@/components/shared'; import { Comment } from '@/types/comment'; import getTimeDifference from '@/utils/getTimeDifference'; -import { ForwardedRef, forwardRef } from 'react'; +import { ForwardedRef, forwardRef, useState } from 'react'; +import UserCircle from '@/components/shared/UserCircle'; import * as S from './styles'; -import UserCircle from '@/components/shared/UserCircle'; +import { animationMap } from '@/styles/framerMotion'; +import { AnimatePresence } from 'framer-motion'; +import { deleteComment } from '@/apis/comment'; +import { postIdStore } from '@/stores/post'; +import { useQueryClient } from '@tanstack/react-query'; const CommentItem = forwardRef( - ({ content, createdAt, user }: Comment, ref: ForwardedRef) => { + ( + { content, createdAt, user, commentId }: Comment, + ref: ForwardedRef, + ) => { + const [isExpand, setIsExpand] = useState(false); + const postId = postIdStore((state) => state.postId); + const queryClient = useQueryClient(); return ( - {user.nickname} - + + {user.nickname} + + {getTimeDifference(createdAt)} - {content} - + + {content} + + setIsExpand((prev) => !prev)} + /> + + {isExpand && ( + + + deleteComment(postId, commentId).then(() => { + queryClient.invalidateQueries({ queryKey: ['comments', postId] }); + setIsExpand(false); + }) + } + > + 삭제 + + + )} + ); }, diff --git a/src/components/detail/commentItem/styles.ts b/src/components/detail/commentItem/styles.ts index 4628303..cb86984 100644 --- a/src/components/detail/commentItem/styles.ts +++ b/src/components/detail/commentItem/styles.ts @@ -1,11 +1,15 @@ +import { Icon } from '@/components/shared'; import { layoutMap } from '@/styles/layout'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; export const Container = styled.div` ${layoutMap.shadowBox} flex-direction: row; gap: 30px; + align-items: center; + position: relative; `; export const UserBox = styled.div` @@ -23,9 +27,17 @@ export const contentCss = css` flex: 1; `; -export const threedotCss = css` +export const ThreedotIcon = styled(Icon)` ${layoutMap.shadowBox} margin-left: auto; border-radius: 100%; - padding: 10px; + padding: 5px; +`; + +export const Test = styled(motion.div)` + ${layoutMap.shadowBox} + transform: translateX(-100%); + position: absolute; + right: 0; + padding: 20px; `; diff --git a/src/components/detail/commentList/index.tsx b/src/components/detail/commentList/index.tsx index a899b2a..40184eb 100644 --- a/src/components/detail/commentList/index.tsx +++ b/src/components/detail/commentList/index.tsx @@ -9,9 +9,9 @@ const CommentList = () => { const { data, ref } = useGetComments(postId); return ( - {data?.pages.comments.map((comment, index) => { - const commentLength = data.pages.comments.length; - if (commentLength - 2 === index && !data.pages.pageInfo.isDone) { + {data?.pages.map((comment, index) => { + const commentLength = data.pages.length; + if (commentLength - 2 === index && !data.pageParams.isDone) { return ; } else { return ; diff --git a/src/components/loader/skeleton/index.tsx b/src/components/loader/skeleton/index.tsx index ed2333d..160defc 100644 --- a/src/components/loader/skeleton/index.tsx +++ b/src/components/loader/skeleton/index.tsx @@ -6,20 +6,24 @@ import * as S from './styles'; const SkeletonBox = () => { return ( <> - - - - - -

-

-
-
- - - + + + + + +

+ +

+

+ +

+
+
+ + + - ) -} + ); +}; -export default SkeletonBox; \ No newline at end of file +export default SkeletonBox; diff --git a/src/components/loader/skeleton/styles.ts b/src/components/loader/skeleton/styles.ts index c99b154..485a852 100644 --- a/src/components/loader/skeleton/styles.ts +++ b/src/components/loader/skeleton/styles.ts @@ -1,48 +1,46 @@ -import { colors } from "@/styles/colorPalette"; -import styled from "@emotion/styled"; +import { colors } from '@/styles/colorPalette'; +import styled from '@emotion/styled'; export const UserBox = styled.div` - width : 100%; - height : 50px; - display : flex; - align-items : center; - justify-content : flex-start; - gap : 20px; - margin: 50px 0 0 0; + width: 600px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 20px; + margin: 50px 0 0 0; `; export const AvatarBlock = styled.div` - display : flex; - align-items : center; - justify-content : center; - border-radius : 50%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; - &.avatar { - border : none; - outline : none; - } + &.avatar { + border: none; + outline: none; + } `; export const InfoBlock = styled.div` - width : 100%; - height : 100px; - display : flex; - flex-direction : column; - justify-content : space-between; + height: 100px; + display: flex; + flex-direction: column; + justify-content: space-between; - h3 { - font-size : 20px; - font-weight : 500; - color : black; - } + h3 { + font-size: 20px; + font-weight: 500; + color: black; + } - p { - font-size : 16px; - font-weight : 500; - color : ${colors.gray200}; - } + p { + font-size: 16px; + font-weight: 500; + color: ${colors.gray200}; + } `; export const ContentBox = styled.div` - margin : 50px 0 0 0; -`; \ No newline at end of file + margin: 50px 0 0 0; +`; diff --git a/src/components/loginForm/index.tsx b/src/components/loginForm/index.tsx index 47e73fc..3acdc23 100644 --- a/src/components/loginForm/index.tsx +++ b/src/components/loginForm/index.tsx @@ -6,6 +6,7 @@ import loginImage from '@/assets/images/loginImage.jpg'; import { postLogin } from '@/apis/user'; import { Login } from '@/types/auth'; import { setAccess } from '@/hooks/auth/useLocalStorage'; +import { AxiosError } from 'axios'; export default function LoginForm() { const navigate = useNavigate(); @@ -23,16 +24,39 @@ export default function LoginForm() { const response = await postLogin(data); const accessToken = response.accessToken; setAccess(accessToken); - console.log('로그인 성공:', response); navigate(-1); } catch (error) { - setError('email', { - type: 'manual', - message: '이메일이 존재하지 않거나 비밀번호가 일치하지 않습니다.', - }); + if (error instanceof AxiosError && error.response) { + const status = error.response.status; + if (status === 404) { + setError('email', { + type: 'manual', + message: '요청하신 회원을 찾을 수 없습니다.', + }); + } else if (status === 400) { + setError('password', { + type: 'manual', + message: '입력하신 비밀번호가 정확하지 않습니다.', + }); + } + } + throw error; } }; + // const kakaoLogin = async () => { + // window.open(import.meta.env.VITE_KAKAO_URL); + // try { + // const response = await getKakaoLogin(); + // const accessToken = response.accessToken; + // setAccess(accessToken); + // console.log('로그인 성공:', response); + // navigate(-1); + // } catch (error) { + // console.log(error); + // } + // }; + return ( @@ -44,10 +68,6 @@ export default function LoginForm() { ,.?/-]).{6,20}$/, - message: '6~20자의 영문, 숫자, 특수문자를 모두 포함해주세요.', - }, })} label="비밀번호" type="password" @@ -74,13 +89,13 @@ export default function LoginForm() { 로그인 - + {/* 간편하게 로그인하세요! - +

카카오 로그인

-
+
*/}
); diff --git a/src/components/main/MainListItem/index.tsx b/src/components/main/MainListItem/index.tsx index d7d5b22..f268252 100644 --- a/src/components/main/MainListItem/index.tsx +++ b/src/components/main/MainListItem/index.tsx @@ -3,6 +3,8 @@ import * as S from './styles'; import Article from '@/components/shared/article'; import { ArticleType } from '@/types'; import getTimeDifference from '@/utils/getTimeDifference'; +import { Link } from 'react-router-dom'; +import { useEffect, useState } from 'react'; interface ListItemProps { listItem: ArticleType; @@ -12,6 +14,27 @@ const MainListItem = ({ listItem }: ListItemProps) => { // 구조 분해 할당으로 author과 그 외 나머지 정보들로 분리 const { user, ...articleDetails } = listItem; const createDate = getTimeDifference(listItem.createdAt); + const [iconSize, setIconSize] = useState(105); + + useEffect(() => { + console.log(window.innerWidth); + const handleResize = () => { + if (window.innerWidth <= 900) { + setIconSize(70); + } else { + setIconSize(105); + } + }; + + window.addEventListener('resize', handleResize); + + // 초기 크기 설정 + handleResize(); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); return ( @@ -20,15 +43,21 @@ const MainListItem = ({ listItem }: ListItemProps) => { {user.profileImage ? ( ) : ( - + )} - {user.nickname} - {createDate} + + + {user.nickname} + + + + {createDate} + -
+
); }; diff --git a/src/components/main/MainListItem/styles.ts b/src/components/main/MainListItem/styles.ts index f668bb2..38c703a 100644 --- a/src/components/main/MainListItem/styles.ts +++ b/src/components/main/MainListItem/styles.ts @@ -1,4 +1,5 @@ import { colors } from "@/styles/colorPalette"; +import { typographyMap } from "@/styles/typography"; import styled from "@emotion/styled"; export const MltContainer = styled.div` @@ -11,6 +12,11 @@ export const MltContainer = styled.div` align-items : center; justify-content : center; gap : 30px; + + @media (max-width: 570px) { + width : 90vw; + margin : 30px 0 0 0; + } `; export const MlUserBox = styled.div` @@ -20,6 +26,7 @@ export const MlUserBox = styled.div` align-items : center; justify-content : flex-start; gap : 20px; + margin : 15px 0 0 0; `; export const AvatarBlock = styled.div` @@ -38,4 +45,21 @@ export const MlInfoBlock = styled.div` display : flex; flex-direction : column; justify-content : space-between; + + a { + &:hover{ + text-decoration: underline; + } + } + + @media (max-width : 900px) { + height : 80%; + #userP { + ${typographyMap.t4}; + } + + #createP { + ${typographyMap.t6}; + } + } `; \ No newline at end of file diff --git a/src/components/main/mainExtra/MainExtra.tsx b/src/components/main/mainExtra/MainExtra.tsx index 4882eac..b2e45cb 100644 --- a/src/components/main/mainExtra/MainExtra.tsx +++ b/src/components/main/mainExtra/MainExtra.tsx @@ -1,22 +1,37 @@ import { Icon, Text } from '@/components/shared'; import * as S from './styles'; import { bestMakers, mostSolvedProblems } from '@/consts/mainExtraData'; +import { useModalContext } from '@/contexts/ModalContext'; const MainExtra = () => { + const { open } = useModalContext(); return ( - - + + + open({ + title: '아직 구현되지 않은 서비스입니다.', + description: '추후 구현 예정입니다.', + onButtonClick: () => {}, + buttonLabel: '확인', + }) + } + /> - - Best Maker + + + Best Maker + {bestMakers.map((maker) => ( - + {maker.id}. {maker.name} ))} @@ -24,11 +39,13 @@ const MainExtra = () => { - Most Solved + + Most Solved + {mostSolvedProblems.map((problem) => ( - + {problem.id}. {problem.title} ))} diff --git a/src/components/main/mainExtra/styles.ts b/src/components/main/mainExtra/styles.ts index f04a4af..52aee69 100644 --- a/src/components/main/mainExtra/styles.ts +++ b/src/components/main/mainExtra/styles.ts @@ -9,7 +9,7 @@ export const MeContainer = styled.div` left : 0; width : 380px; height : 100%; - padding : 30px; + padding : 50px 30px; box-sizing : border-box; display : flex; flex-direction : column; @@ -55,7 +55,7 @@ export const MeBox = styled.div` width : 100%; height : auto; margin : 50px 0 30px 0; - padding : 0 20px; + padding : 0 5px; box-sizing : border-box; display : flex; flex-direction : column; diff --git a/src/components/main/mainList/MainList.tsx b/src/components/main/mainList/MainList.tsx index 629fa0b..abf3720 100644 --- a/src/components/main/mainList/MainList.tsx +++ b/src/components/main/mainList/MainList.tsx @@ -1,8 +1,8 @@ import * as S from './styles'; import { ArticleType } from '@/types/article'; import React from 'react'; -import Loading from '@/components/loader'; import useGetPosts from '@/hooks/post/useGetPosts'; +import CircleLoader from '@/components/loader/circleLoader'; import MainListItem from '../MainListItem'; const MainList = () => { @@ -16,13 +16,16 @@ const MainList = () => { {data?.pages.map((page, index) => ( - {page.posts.map((item: ArticleType) => ( - - ))} + {page.posts + .slice() + .reverse() + .map((item: ArticleType) => ( + + ))} ))}
- {isFetchingNextPage && } + {isFetchingNextPage && }
); diff --git a/src/components/main/mainList/styles.ts b/src/components/main/mainList/styles.ts index e815dd6..08773ee 100644 --- a/src/components/main/mainList/styles.ts +++ b/src/components/main/mainList/styles.ts @@ -3,7 +3,7 @@ import { colors } from "../../../styles/colorPalette"; export const MlContainer = styled.div` flex-grow : 1; - padding : 0 50px; + padding : 20px 50px; box-sizing : border-box; display : flex; flex-direction : column; @@ -11,8 +11,9 @@ export const MlContainer = styled.div` border-left : 1px solid ${colors.gray50}; border-right : 1px solid ${colors.gray50}; - @media (max-width : 1050px) { + @media (max-width : 1300px) { border : none; - width : 75vw; + width : 95vw; + overflow-x : hidden; } `; \ No newline at end of file diff --git a/src/components/mypage/articles/index.tsx b/src/components/mypage/articles/index.tsx index 31ede4a..7bd50ec 100644 --- a/src/components/mypage/articles/index.tsx +++ b/src/components/mypage/articles/index.tsx @@ -47,12 +47,11 @@ const ArticlesMenu = ({ value }: Pick) => { ) : ( data?.pages.map((page) => - page.posts.map((post: ArticleType) => { - console.log('포스트', post); -
+ page.posts.map((post: ArticleType) => ( +
-
; - }), + + )), ) )}
diff --git a/src/components/mypage/articles/styles.ts b/src/components/mypage/articles/styles.ts index 9cc8532..c28d83b 100644 --- a/src/components/mypage/articles/styles.ts +++ b/src/components/mypage/articles/styles.ts @@ -4,11 +4,17 @@ import styled from '@emotion/styled'; export const Container = styled.div` ${layoutMap.flexCenter} - gap: 20px; + gap: 40px; + margin-top: 40px; `; -export const Block = styled.div` - ${layoutMap.shadowBox} +export const Box = styled.div` + @media (min-width: 1050px) { + min-width: 950px; + } + @media (max-width: 1049px) { + min-width: 750px; + } `; export const Title = styled.div` diff --git a/src/components/mypage/profileTable/index.tsx b/src/components/mypage/profileTable/index.tsx index 7ae9fcd..eec2bfd 100644 --- a/src/components/mypage/profileTable/index.tsx +++ b/src/components/mypage/profileTable/index.tsx @@ -1,27 +1,62 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import * as S from './styles'; import { getUserInfo, putUser } from '@/apis/user'; -import { User } from '@/types/auth'; import { UserCircle } from '@/components/shared'; import { SubmitHandler, useForm } from 'react-hook-form'; -interface UserModify extends Omit {} +import { UserModify } from '@/types'; +import { useQuery } from '@tanstack/react-query'; +import { useModalContext } from '@/contexts/ModalContext'; +import { AxiosError } from 'axios'; + const ProfileTable = () => { - const { register, handleSubmit, reset } = useForm(); + const { register, handleSubmit, reset } = useForm({}); + const initialValues = useRef(null); + const { open } = useModalContext(); + + const { data: userData } = useQuery({ + queryKey: ['userData'], + queryFn: async () => { + return await getUserInfo(); + }, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }); useEffect(() => { - const fetchUserData = async () => { - const userData = await getUserInfo(); - reset(userData); - }; - fetchUserData(); - }, []); + if (userData) { + initialValues.current = userData as UserModify; + reset(userData as UserModify); + } + }, [userData, reset]); const onSubmit: SubmitHandler = async (data: UserModify) => { + if (initialValues.current?.nickname === data.nickname) { + data.nickname = null; + } + try { await putUser(data); - alert('수정 완료'); + open({ + title: '프로필 수정이 완료되었습니다.', + onButtonClick: () => {}, + hasCancelButton: false, + buttonLabel: '확인', + }); } catch (error) { - alert('수정 실패'); + let errorMessage = '알 수 없는 에러가 발생했습니다. 다시 시도해주세요.'; + if (error instanceof AxiosError && error.response) { + errorMessage = error.response.data.message; + } + + open({ + title: '프로필 수정 실패', + description: errorMessage, + onButtonClick: () => {}, + hasCancelButton: false, + buttonLabel: '확인', + }); + + reset(userData as UserModify); } }; @@ -29,17 +64,18 @@ const ProfileTable = () => { - +
프로필 사진 별명 깃허브 블로그 - + 자기소개 +
- +
- + @@ -50,14 +86,16 @@ const ProfileTable = () => { - + + + +
적용 - reset()}>취소 + reset(userData)}>취소 -
); }; diff --git a/src/components/mypage/profileTable/styles.ts b/src/components/mypage/profileTable/styles.ts index fe7308e..b34a46b 100644 --- a/src/components/mypage/profileTable/styles.ts +++ b/src/components/mypage/profileTable/styles.ts @@ -1,5 +1,6 @@ import { colors } from '@/styles/colorPalette'; import { layoutMap } from '@/styles/layout'; +import { typographyMap } from '@/styles/typography'; import styled from '@emotion/styled'; export const Container = styled.div` @@ -12,22 +13,23 @@ export const Container = styled.div` export const Table = styled.div` display: flex; - height: 550px; + height: 700px; border-top: 1px solid ${colors.gray200}; `; export const Thead = styled.div` + ${layoutMap.flexCenter} flex: 1; + min-width: 150px; background: ${colors.yellow}; - text-align: center; - font-size: 24px; - font-weight: 500; + ${typographyMap.semibold} + ${typographyMap.t4} `; export const Tbody = styled.div` + display: table; flex: 4; - font-size: 20px; - font-weight: 400; + ${typographyMap.t5} `; export const Th = styled.div<{ height: number }>` @@ -62,6 +64,16 @@ export const Input = styled.input` padding: 10px 20px; border-radius: 5px; border: 1px solid ${colors.gray}; - font-size: 20px; + ${typographyMap.t5} + outline: none; +`; + +export const TextArea = styled.textarea` + padding: 10px 20px; + border-radius: 5px; + border: 1px solid ${colors.gray}; + ${typographyMap.t5} outline: none; + width: 70%; + height: 60%; `; diff --git a/src/components/mypage/solved/SolvedBox.tsx b/src/components/mypage/solved/SolvedBox.tsx deleted file mode 100644 index 3d0ea7e..0000000 --- a/src/components/mypage/solved/SolvedBox.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { SolvedArticles } from '@/types'; -import * as S from './styles'; - -export const SolvedBox = ({ data }: { data: SolvedArticles }) => { - return ( - -
{data.postId}
-
{data.title}
-
- ); -}; - -export default SolvedBox; diff --git a/src/components/mypage/solved/index.tsx b/src/components/mypage/solved/index.tsx index f6e3459..f2f266f 100644 --- a/src/components/mypage/solved/index.tsx +++ b/src/components/mypage/solved/index.tsx @@ -1,10 +1,11 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import * as S from '../articles/styles'; +import * as S from './styles'; import { getMypageArticles } from '@/apis/mypage'; import { useEffect, useRef } from 'react'; import { MypageArticles, SolvedArticles } from '@/types'; import SkeletonBox from '@/components/loader/skeleton'; -import SolvedBox from './SolvedBox'; +import { Text } from '@/components/shared'; +import { CodeBox } from '@/components/post'; const SolvedMenu = ({ value }: Pick) => { const queryEmail = new URLSearchParams(location.search).get('user'); @@ -47,9 +48,17 @@ const SolvedMenu = ({ value }: Pick) => { ) : ( data?.pages.map((page) => page.posts.map((post: SolvedArticles) => ( -
- -
+ + + {post.postNumber}. {post.title} + + + )), ) )} diff --git a/src/components/mypage/solved/styles.ts b/src/components/mypage/solved/styles.ts index d0aad1c..d6b19e7 100644 --- a/src/components/mypage/solved/styles.ts +++ b/src/components/mypage/solved/styles.ts @@ -1,9 +1,29 @@ +import { layoutMap } from '@/styles/layout'; +import { typographyMap } from '@/styles/typography'; import styled from '@emotion/styled'; export const Container = styled.div` + ${layoutMap.flexCenter} + gap: 40px; + margin-top: 40px; +`; + +export const Block = styled.div` + ${layoutMap.shadowBox} display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - gap: 43px; + gap: 30px; + @media (min-width: 1050px) { + min-width: 950px; + } + @media (max-width: 1049px) { + min-width: 750px; + } +`; + +export const Title = styled.div` + ${typographyMap.t1} +`; + +export const Content = styled.div` + ${typographyMap.t3} `; diff --git a/src/components/mypage/tabMenu/index.tsx b/src/components/mypage/tabMenu/index.tsx index 6c80321..7343e6d 100644 --- a/src/components/mypage/tabMenu/index.tsx +++ b/src/components/mypage/tabMenu/index.tsx @@ -1,5 +1,5 @@ import { ArticlesMenu, SolvedMenu } from '@/components/mypage'; -import * as S from './styles.ts'; +import * as S from './styles'; import { useState } from 'react'; const TabMenu = () => { diff --git a/src/components/mypage/tabMenu/styles.ts b/src/components/mypage/tabMenu/styles.ts index 20df9e1..c82a917 100644 --- a/src/components/mypage/tabMenu/styles.ts +++ b/src/components/mypage/tabMenu/styles.ts @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; export const Container = styled.div` width: 100%; + height: 100vh; `; export const TabMenu = styled.ul` @@ -12,6 +13,14 @@ export const TabMenu = styled.ul` justify-content: space-around; border-bottom: 1px solid ${colors.gray}; ${typographyMap.t2} + + @media (max-width: 900px) { + ${typographyMap.t4} + } + + @media (max-width: 570px) { + ${typographyMap.t4} + } `; export const TabBox = styled.li<{ active: boolean }>` diff --git a/src/components/mypage/userInfo/index.tsx b/src/components/mypage/userInfo/index.tsx index 3805804..6a5c55d 100644 --- a/src/components/mypage/userInfo/index.tsx +++ b/src/components/mypage/userInfo/index.tsx @@ -1,5 +1,4 @@ -import { Icon, UserCircle } from '@/components/shared'; -import * as S from './styles'; +import { Icon, UserCircle, Text } from '@/components/shared'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { User } from '@/types/auth.ts'; @@ -7,9 +6,12 @@ import { IconValues } from '@/components/shared/icon'; import { getUserInfo } from '@/apis/user'; import { getOthersProfile } from '@/apis/mypage'; +import * as S from './styles'; + const UserInfo = () => { const navigate = useNavigate(); const [data, setData] = useState(null); + const [isOwner, setIsOwner] = useState(false); const updateUserData = (userData: User) => { setData(userData); @@ -33,8 +35,13 @@ const UserInfo = () => { updateUserData(userData); } }; + + if (location.pathname.includes('mypage')) { + setIsOwner(true); + } + fetchUserData(); - }, [location.search, navigate]); + }, []); const contactInfo = [ { key: 'email', value: data?.email, icon: 'email' }, @@ -48,17 +55,21 @@ const UserInfo = () => { - navigate('modify')}> - - - {data?.nickname} + {isOwner && ( + navigate('modify')}> + + + )} + + {data?.nickname} + {contactInfo.map( (info) => info.value && ( - - {info.value} + + {info.value} ), )} diff --git a/src/components/mypage/userInfo/styles.ts b/src/components/mypage/userInfo/styles.ts index 46c5f57..cddee5e 100644 --- a/src/components/mypage/userInfo/styles.ts +++ b/src/components/mypage/userInfo/styles.ts @@ -12,6 +12,7 @@ export const Container = styled.div` export const LeftBox = styled.div` flex: 1; + margin-right: 30px; `; export const RightBox = styled.div` @@ -25,11 +26,6 @@ export const ProfileImage = styled.img` border-radius: 10em; `; -export const UserName = styled.div` - ${typographyMap.t1} - ${typographyMap.bold} -`; - export const Contact = styled.div` display: flex; align-items: center; @@ -41,17 +37,17 @@ export const Block = styled.div` display: flex; align-items: center; gap: 8px; + ${typographyMap.t4}; `; -export const UserEmail = styled.div` - ${typographyMap.t3} - color: ${colors.gray}; +export const Info = styled.div` + margin-bottom: 5px; `; export const UserIntro = styled.div` margin-top: 28px; ${typographyMap.t2} - color: ${colors.gray300}; + color: ${colors.gray400}; `; export const Edit = styled.button` diff --git a/src/components/nav/Nav.tsx b/src/components/nav/Nav.tsx index 3278a19..4c11675 100644 --- a/src/components/nav/Nav.tsx +++ b/src/components/nav/Nav.tsx @@ -3,28 +3,86 @@ import { navItems } from '@/consts/navItems'; import { Link } from 'react-router-dom'; import * as S from './styles'; +import { useEffect, useState } from 'react'; +import { ButtonSize } from '@/styles/button'; +import { useModalContext } from '@/contexts/ModalContext'; + +const notComplete = ['Search', 'Problem', 'Setting']; const Nav = () => { + const [iconSize, setIconSize] = useState(48); + const [buttonSize, setButtonSize] = useState('large'); + const [buttonContent, setButtonContent] = useState('post'); + const { open } = useModalContext(); + useEffect(() => { + const handleResize = () => { + if (window.innerWidth <= 570) { + setIconSize(36); + setButtonSize('medium'); + setButtonContent('+'); + } else if (window.innerWidth <= 900) { + setIconSize(48); + setButtonSize('medium'); + setButtonContent('+'); + } else { + setIconSize(48); + setButtonSize('large'); + setButtonContent('post'); + } + }; + + window.addEventListener('resize', handleResize); + + // 초기 크기 설정 + handleResize(); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + const onClickNonComplete = () => { + open({ + title: '아직 구현되지 않은 서비스입니다.', + description: '추후 구현 예정입니다.', + onButtonClick: () => {}, + buttonLabel: '확인', + }); + }; + return ( - - - + - {navItems.map((item) => ( - - - - {item.label} - - - ))} + {navItems.map((item) => { + if (notComplete.includes(item.label)) { + return ( +
+ + + {item.label} + +
+ ); + } else { + return ( + + + + {item.label} + + + ); + } + })}
- - Post - + + + {buttonContent} + +
); }; diff --git a/src/components/nav/styles.ts b/src/components/nav/styles.ts index 8cd9e16..207ed1f 100644 --- a/src/components/nav/styles.ts +++ b/src/components/nav/styles.ts @@ -8,7 +8,7 @@ export const NavContainer = styled.div` left: 0; width: 420px; height: 100%; - padding: 50px 50px; + padding: 0 50px; box-sizing: border-box; display: flex; flex-direction: column; @@ -17,33 +17,37 @@ export const NavContainer = styled.div` gap: 50px; ${typographyMap.t1} - @media (max-width : 1050px) { + @media (max-width : 900px) { + top: 60px; + left: 30px; + width: 100px; + padding: 0 20px; + gap: 30px; + } + + @media (max-width: 570px) { + position: fixed; + top: 0; + left: 0; flex-direction: row; - padding : 20px 50px; background-color: ${colors.white}; - border-radius : 0 0 30px 30px; - box-shadow : 0 15px 20px rgba(0, 0, 0, 0.05); - width : 100%; - height : 150px; - z-index : 999; + border-radius: 0 0 30px 30px; + box-shadow: 0 15px 20px rgba(0, 0, 0, 0.05); + width: 100%; + height: 80px; + z-index: 999; } `; export const NavLogoBox = styled.div` - width : 100%; - height : 150px; + width: 100%; + height: 150px; display: flex; align-items: center; justify-content: center; - img { - width: 100%; - height: 100%; - object-fit: contain; - } - - @media (max-width : 1050px) { - display : none; + @media (max-width: 900px) { + display: none; } `; @@ -56,17 +60,31 @@ export const NavItemBox = styled.div` justify-content: center; gap: 30px; + .disabledItem { + width: 100%; + display: flex; + justify-content: flex-start; + align-items: center; + gap: 20px; + border-radius: 50px; + + &:hover { + cursor: pointer; + background-color: ${colors.gray50}; + } + } + a { text-decoration: none; width: 100%; height: 50px; - padding: 0 0 0 30px; box-sizing: border-box; display: flex; align-items: center; justify-content: flex-start; gap: 20px; transition: all 0.1s ease; + border-radius: 50px; &:hover { cursor: pointer; @@ -74,21 +92,40 @@ export const NavItemBox = styled.div` } } - @media (max-width : 1050px) { - flex-direction: row; + @media (max-width: 900px) { + .disabledItem { + p { + display: none; + } - a { - display : flex; - flex-direction: column; - width : 100%; - height : 100%; + &:hover { + background-color: transparent; + } + } + a { + margin: 0; p { - ${typographyMap.t4}; + display: none; } &:hover { - background-color : transparent; + background-color: transparent; + } + } + } + + @media (max-width: 570px) { + flex-direction: row; + + a { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + + p { + ${typographyMap.t6}; } } } diff --git a/src/components/post/codeBox/index.tsx b/src/components/post/codeBox/index.tsx index b036689..367eeae 100644 --- a/src/components/post/codeBox/index.tsx +++ b/src/components/post/codeBox/index.tsx @@ -19,7 +19,7 @@ const CodeBox = ({ layout = true, readOnly = false, code, language }: CodeBoxPro return ( - 언어 + 언어 {language ? ( <>{language} ) : ( diff --git a/src/components/post/posttestCaseBox/index.tsx b/src/components/post/posttestCaseBox/index.tsx index 61ee581..5fc0834 100644 --- a/src/components/post/posttestCaseBox/index.tsx +++ b/src/components/post/posttestCaseBox/index.tsx @@ -4,16 +4,14 @@ import { postFormStore } from '@/stores/post'; import { TestCase } from '@/types'; import * as S from './styles'; +import TestCaseItem from '../tesCaseItem'; const defalutValue = [{ example: '', result: '' }]; const PostTestCaseBox = () => { const updatePostForm = postFormStore((state) => state.updatePostForm); const [testCases, setTestCases] = useState(defalutValue); - useEffect(() => { - updatePostForm({ testCases }); - }, [testCases, updatePostForm]); - const handleChange = ({ + const onChangeTestCase = ({ index, event, }: { @@ -30,8 +28,19 @@ const PostTestCaseBox = () => { setTestCases(updateTestCase); updatePostForm({ testCases: updateTestCase }); }; + const onDeleteTestCase = (index: number) => { + if (testCases.length > 1) { + const updateTestCase = testCases.filter((_, i) => i !== index); + setTestCases(updateTestCase); + updatePostForm({ testCases: updateTestCase }); + } + }; + useEffect(() => { + updatePostForm({ testCases }); + }, [testCases, updatePostForm]); + return ( - + 테스트 케이스 { /> - Example - - {testCases.map((testCase, index) => ( -