diff --git a/src/components/display/dashboard/admin/AddAdmin.jsx b/src/components/display/dashboard/admin/AddAdmin.jsx index 0d042d0..ac7a3ce 100644 --- a/src/components/display/dashboard/admin/AddAdmin.jsx +++ b/src/components/display/dashboard/admin/AddAdmin.jsx @@ -16,7 +16,7 @@ export const AddAdmin = forwardRef(({ ...props }, ref) => { @@ -28,3 +28,5 @@ export const AddAdmin = forwardRef(({ ...props }, ref) => { ); }); + +AddAdmin.displayName = 'AddAdmin'; diff --git a/src/components/layouts/DashboardLayout.jsx b/src/components/layouts/DashboardLayout.jsx index 0730abc..5335841 100644 --- a/src/components/layouts/DashboardLayout.jsx +++ b/src/components/layouts/DashboardLayout.jsx @@ -12,6 +12,7 @@ import { DashboardNav } from '@components/navigation/DashboardNav'; import { Alert } from '@components/forms/modal/Alert'; import { Confirm } from '@components/forms/modal/Confirm'; import { Loading } from '@components/forms/modal/Loading'; +import NotFound from '../../pages/NotFound'; import '@/transitions/fade-slide.css'; @@ -75,16 +76,16 @@ export const DashboardLayout = ({ location }) => { useEffect(() => { if (isLoading) { - showLoading({ message: '대시보드를 준비하는 중...' }); + showLoading({ message: '페이지를 찾는 중...' }); } else { hideLoading(); } - - if (isError) { - navigate(-1); - } }, [isLoading, isError]); + if (isError) { + return ; + } + return ( diff --git a/src/components/navigation/AuthContext.jsx b/src/components/navigation/AuthContext.jsx index 7e2027b..46849bf 100644 --- a/src/components/navigation/AuthContext.jsx +++ b/src/components/navigation/AuthContext.jsx @@ -26,19 +26,12 @@ export const AuthProvider = ({ children }) => { setIsLoggedIn(true); }; - const logout = async () => { - try { - await API.POST('/logout'); - } catch (error) { - // console.error('Error during logout:', error); - } finally { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('user'); - setIsLoggedIn(false); - setUser(null); - window.location.href = '/'; - } + const logout = () => { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('user'); + setIsLoggedIn(false); + setUser(null); }; return ( diff --git a/src/components/navigation/Profile.jsx b/src/components/navigation/Profile.jsx index f0345bb..065ed84 100644 --- a/src/components/navigation/Profile.jsx +++ b/src/components/navigation/Profile.jsx @@ -55,7 +55,14 @@ const Profile = ({ userName, logout }) => { navigate('/mypage')}> {userName} 님 - + { + logout(); + window.location.href = '/'; + }} + > 로그아웃 diff --git a/src/pages/Article.jsx b/src/pages/Article.jsx index c0f8274..853156e 100644 --- a/src/pages/Article.jsx +++ b/src/pages/Article.jsx @@ -24,6 +24,8 @@ import 'prismjs/components/prism-jsx.min'; // JSX 언어 지원을 포함합니 import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; // 코드 블럭에 줄 번호를 추가하기 위해 이 줄을 추가합니다 import 'prismjs/plugins/line-numbers/prism-line-numbers.min'; +import { formatDate } from '@/utils/formatDate'; + const ArticleContainer = styled.div` width: 100%; margin: 0 auto; @@ -121,7 +123,7 @@ export default function Article() { - {post?.user?.name} | {post?.createdAt} + {post?.user?.name} | {formatDate(post?.created_at)} diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 61fc4e0..beb5826 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -145,14 +145,10 @@ export default function Login() { // console.log('response:', response); - // get string body - const access_token = login_res.data.access_token; - const refresh_token = login_res.data.refresh_token; - // 서버에 user 정보 요청 const student_res = await API.GET(`/users/${data.student}`, { headers: { - Authorization: access_token, + Authorization: login_res.data.access_token, }, }); @@ -161,10 +157,16 @@ export default function Login() { // console.log('token:', { access_token, refresh_token }); // console.log('userInfo:', userInfo); - alert('break!'); - - if (access_token && refresh_token && userInfo) { - login(access_token, refresh_token, userInfo); // 로그인 성공 시 AuthContext의 login 함수 호출 + if ( + login_res.data.access_token && + login_res.data.refresh_token && + userInfo + ) { + login( + login_res.data.access_token, + login_res.data.refresh_token, + userInfo, + ); // 로그인 성공 시 AuthContext의 login 함수 호출 // console.log('login user info:', userInfo); // Log user info setError(''); // 에러 초기화 navigate('/'); @@ -173,9 +175,6 @@ export default function Login() { setError('로그인에 실패했습니다. 다시 시도해주세요.'); } } catch (error) { - // 입력창 비우기 - setValue('student', ''); - setValue('password', ''); // 에러 처리 // console.error('Error:', error); setError('로그인에 실패했습니다. 다시 시도해주세요.'); @@ -206,13 +205,9 @@ export default function Login() { placeholder="2024000000" {...register('student', { required: '학번을 입력해주세요.', - minLength: { - value: 10, - message: '학번은 10자리 숫자여야 합니다.', - }, pattern: { value: /^[0-9]{10}$/, - message: '학번은 10자리 숫자여야 합니다.', + message: '올바른 학번을 적어주세요.', }, })} /> @@ -229,16 +224,6 @@ export default function Login() { placeholder="비밀번호" {...register('password', { required: '비밀번호를 입력해주세요', - minLength: { - value: 8, - message: '비밀번호는 8자리 이상이여야 합니다. ', - }, - pattern: { - value: - /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,20}$/, - message: - '비밀번호는 숫자, 영문 대문자·소문자, 특수문자를 포함해야 합니다.', - }, })} /> {errors.password && ( @@ -258,4 +243,4 @@ export default function Login() { ); -} \ No newline at end of file +} diff --git a/src/pages/MyPage.jsx b/src/pages/MyPage.jsx index 589e237..560c1fa 100644 --- a/src/pages/MyPage.jsx +++ b/src/pages/MyPage.jsx @@ -1,8 +1,8 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useForm } from 'react-hook-form'; +import { useQuery, useMutation } from 'react-query'; import styled from 'styled-components'; -import { useQuery, useMutation, useQueryClient } from 'react-query'; import useAlert from '@/hooks/modal/useAlert'; import { useAuth } from '@components/navigation/AuthContext'; @@ -161,12 +161,12 @@ const InputWrapper = styled.div` export default function MyPage() { const [userInfo, setUserInfo] = useState({ - studentNumber: '', name: '', email: '', + student_id: 0, + profile_picture: '', generation: '', major: '', - profilePic: null, }); const { isLoggedIn, logout, user } = useAuth(); const [imagePreview, setImagePreview] = useState(null); @@ -186,28 +186,25 @@ export default function MyPage() { if (!isLoggedIn) { navigate('/login'); } - }, [isLoggedIn, navigate]); + }, []); // Fetch user data - const { isLoading } = useQuery( + const { data, isLoading } = useQuery( ['userData', user?.student_id], async () => { const response = await API.GET(`/users/${user.student_id}`); - if (response.status < 200 || response.status >= 300) { - throw new Error('Failed to fetch user data'); - } return response.data; }, { enabled: isLoggedIn, onSuccess: (data) => setUserInfo({ - studentNumber: data.student_id, name: data.name, email: data.email, + student_id: data.student_id, + profile_picture: data.profile_picture || defaultProfilePic, generation: data.generation, major: data.major, - profilePic: data.profile_picture || defaultProfilePic, }), onError: () => { openAlert({ @@ -215,7 +212,6 @@ export default function MyPage() { content: ( 사용자 정보 불러오기에 실패했습니다. 다시 시도해주세요. ), - onClose: closeAlert, }); }, }, @@ -223,18 +219,69 @@ export default function MyPage() { const imageUploadMutation = useMutation( async (file) => { - // 파일을 base64로 변환 + if (file.size > 2 * 1024 * 1024) { + throw new Error('이미지 크기는 2MB를 초과할 수 없습니다.'); + } + + const compressImage = (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = (event) => { + const img = new Image(); + img.src = event.target.result; + img.onload = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const maxWidth = 800; + const maxHeight = 800; + let width = img.width; + let height = img.height; + + if (width > height) { + if (width > maxWidth) { + height *= maxWidth / width; + width = maxWidth; + } + } else { + if (height > maxHeight) { + width *= maxHeight / height; + height = maxHeight; + } + } + + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob( + (blob) => { + resolve(blob); + }, + 'image/jpeg', + 0.75, // 이미지 퀄리티 (원본 : 100%) + ); + }; + img.onerror = () => + reject(new Error('올바른 형식의 이미지가 아닌 것 같아요.')); + }; + reader.onerror = () => + reject(new Error('이미지를 읽는 중 오류가 발생했어요.')); + }); + }; + + const compressedFile = await compressImage(file); + const reader = new FileReader(); - reader.readAsDataURL(file); + reader.readAsDataURL(compressedFile); return new Promise((resolve, reject) => { reader.onloadend = async () => { const base64String = reader.result; - // userInfo 객체에 base64 인코딩된 이미지를 포함 const formData = { ...userInfo, - profile_picture: base64String, // base64 데이터 전송 + profile_picture: base64String, }; try { @@ -251,15 +298,57 @@ export default function MyPage() { }, { onSuccess: (data) => { - setUserInfo((prev) => ({ ...prev, profilePic: data.profile_picture })); + setUserInfo((prev) => ({ + ...prev, + profile_picture: data.profile_picture, + })); setImagePreview(null); }, - onError: () => { + onError: (error) => { openAlert({ title: '이미지 업로드 실패', - content: ( - 이미지 업로드에 실패했습니다. 다시 시도해주세요. - ), + content: {error.message}, + onClose: closeAlert, + }); + }, + }, + ); + + const imageDeleteMutation = useMutation( + async (default_profile_path) => { + const image = await fetch(default_profile_path); + const blob = await image.blob(); + const reader = new FileReader(); + + reader.readAsDataURL(blob); + + const base64String = await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result); + reader.onerror = () => reject(new Error('이미지 변환 실패')); + }); + + const formData = { + ...userInfo, + profile_picture: base64String, + }; + + const response = await API.PUT(`/users/${userInfo.student_id}`, { + body: formData, + }); + return response.data; + }, + { + onSuccess: (data) => { + setUserInfo((prev) => ({ + ...prev, + profile_picture: data.profile_picture, + })); + setImagePreview(null); + }, + onError: (error) => { + openAlert({ + title: '이미지 업로드 실패', + content: {error.message}, onClose: closeAlert, }); }, @@ -268,6 +357,7 @@ export default function MyPage() { const handleImageUpload = (event) => { const file = event.target.files[0]; + console.log(file); if (file) { setImagePreview(URL.createObjectURL(file)); imageUploadMutation.mutate(file); @@ -309,48 +399,46 @@ export default function MyPage() { // hash를 생성하여 data 객체를 수정 const hashData = { - currentPassword: data.currentPassword, // 기존 비밀번호 포함 - newPassword: data.newPassword, // 새 비밀번호 + user_id: user.student_id, + old_password: data.currentPassword, // 기존 비밀번호 포함 + password: data.newPassword, // 새 비밀번호 }; // 비밀번호 변경 요청 passwordChangeMutation.mutate(hashData); }; - const handleDeleteAccount = () => { + const handleDeleteAccount = async () => { const confirmDelete = window.confirm( '계정을 삭제하면 복구할 수 없습니다. 정말로 삭제하시겠습니까?', ); if (!confirmDelete) return; // 계정 삭제 요청 - API.DELETE(`/users/${user.student_id}`, { - headers: { - Authorization: localStorage.getItem('sessionStorageToken'), - }, - }) - .then(() => { - openAlert({ - title: '계정 삭제 성공', - content: 계정이 성공적으로 삭제되었습니다., - onClose: () => { - logout(); - navigate('/login'); - }, - }); - }) - .catch(() => { - openAlert({ - title: '계정 삭제 실패', - content: 계정 삭제에 실패했습니다. 다시 시도해주세요., - onClose: closeAlert, - }); + try { + await API.DELETE(`/users/${user.student_id}`, {}); + logout(); + openAlert({ + title: '계정 삭제됨', + content: 계정이 성공적으로 삭제되었습니다., + onClose: () => { + closeAlert(); + navigate('/'); + }, }); + } catch (error) { + openAlert({ + title: '계정 삭제 실패', + content: 계정 삭제에 실패했습니다. 다시 시도해주세요., + onClose: closeAlert, + }); + } }; return ( + - {/* Account Info Section */} + {/* /* Account Info Section */}
Account Info @@ -359,7 +447,7 @@ export default function MyPage() { 계정 정보 - + @@ -394,13 +484,13 @@ export default function MyPage() { - + @@ -417,28 +507,6 @@ export default function MyPage() { 비밀번호 변경
- - - - {errors.currentPassword && ( - - {errors.currentPassword.message} - - )} - @@ -485,6 +553,28 @@ export default function MyPage() { )} + + + + {errors.currentPassword && ( + + {errors.currentPassword.message} + + )} + {passwordError && {passwordError}} @@ -505,19 +595,18 @@ export default function MyPage() { 계정 삭제 - +
계정을 삭제하면 복구할 수 없습니다. 신중히 선택하세요.
-
- +
-
); diff --git a/src/pages/NewArticleEditor.jsx b/src/pages/NewArticleEditor.jsx index 140822d..53b214d 100644 --- a/src/pages/NewArticleEditor.jsx +++ b/src/pages/NewArticleEditor.jsx @@ -31,6 +31,7 @@ import { Alert } from '@/components/forms/modal/Alert'; import useLoading from '@/hooks/modal/useLoading'; import { Text } from '@components/typograph/Text'; import { Loading } from '../components/forms/modal/Loading'; +import NotFound from './NotFound'; const Container = styled.div` width: 100%; @@ -208,12 +209,12 @@ export default function NewArticle() { } else { hideLoading(); } - - if (!adminData && !isLoading) { - navigate('/board'); - } }, [isLoading, adminData]); + if (!adminData && !isLoading) { + return ; + } + return ( {!isLoading && adminData && ( diff --git a/src/pages/SignUp.jsx b/src/pages/SignUp.jsx index 965743c..eeaa2f0 100644 --- a/src/pages/SignUp.jsx +++ b/src/pages/SignUp.jsx @@ -164,13 +164,6 @@ export default function SignUp() { }) .catch((error) => { // console.error('Error:', error); - setValue('username', ''); - setValue('student', ''); - setValue('password', ''); - setValue('mail', ''); - setValue('generation', ''); - setValue('major', ''); - openAlert({ title: '회원가입 실패', content: 회원가입에 실패했습니다. 다시 시도해주세요., @@ -266,7 +259,7 @@ export default function SignUp() { ); -} \ No newline at end of file +} diff --git a/src/utils/api.js b/src/utils/api.js index bd57a66..05fdab9 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -44,8 +44,12 @@ api.interceptors.response.use( refresh_token: localStorage.getItem('refreshToken'), }); - localStorage.setItem('accessToken', data.accessToken); - localStorage.setItem('refreshToken', data.refreshToken); + if (data.accessToken) { + localStorage.setItem('accessToken', data.accessToken); + } + if (data.refreshToken) { + localStorage.setItem('refreshToken', data.refreshToken); + } // 실패했던 요청에 새로운 토큰 적용 originalRequest.headers.Authorization =