diff --git a/src/api/auth.api.ts b/src/api/auth.api.ts index 31692e2d..0bc320ea 100644 --- a/src/api/auth.api.ts +++ b/src/api/auth.api.ts @@ -1,4 +1,4 @@ -import type { ApiVerifyNickname, VerifyEmail } from '../models/auth'; +import type { ApiOauth, ApiVerifyNickname, VerifyEmail } from '../models/auth'; import { httpClient } from './http.api'; import { loginFormValues } from '../pages/login/Login'; import { registerFormValues } from '../pages/user/register/Register'; @@ -17,6 +17,7 @@ export const postVerificationEmail = async (email: string) => { export const postVerifyEmailCode = async (data: VerifyEmail) => { try { const response = await httpClient.post('/authenticode/verify', data); + return response; } catch (error) { console.error('verifiyEmailCode:', error); @@ -42,6 +43,7 @@ export const postSignUp = async ( ) => { try { const response = await httpClient.post('/auth/sign-up', data); + return response; } catch (error) { console.error('signup:', error); @@ -54,6 +56,7 @@ export const postResetPassword = async ( ) => { try { const response = await httpClient.post('/auth/password/reset', data); + return response; } catch (error) { console.error('resetpassword:', error); @@ -64,9 +67,36 @@ export const postResetPassword = async ( export const postLogin = async (data: loginFormValues) => { try { const response = await httpClient.post('/auth/login', data); + return response.data; } catch (error) { console.error('login:', error); throw error; } }; + +export const postRefresh = async () => { + try { + const response = await httpClient.post('/auth/refresh'); + + return response.data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const getOauthLogin = async (oauthAccessToken: string) => { + try { + const response = await httpClient.get(`/auth/oauth-login`, { + headers: { + Authorization: `Bearer ${oauthAccessToken}`, + }, + }); + + return response.data; + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/api/http.api.ts b/src/api/http.api.ts index 51b981b8..7df96e74 100644 --- a/src/api/http.api.ts +++ b/src/api/http.api.ts @@ -1,29 +1,23 @@ import axios, { AxiosRequestConfig } from 'axios'; -import useAuthStore, { getTokens } from '../store/authStore'; +import useAuthStore from '../store/authStore'; +import { postRefresh } from './auth.api'; export const BASE_URL = `${import.meta.env.VITE_APP_API_BASE_URL}`; const DEFAULT_TIMEOUT = 15000; export const createClient = (config?: AxiosRequestConfig) => { - const { storeLogin, storeLogout } = useAuthStore.getState(); + const { login, logout } = useAuthStore.getState(); const axiosInstance = axios.create({ baseURL: BASE_URL, timeout: DEFAULT_TIMEOUT, - headers: { - 'content-type': 'application/json', - authorization: - getTokens().accessToken || getTokens().refreshToken - ? `Bearer ${getTokens().accessToken}` - : '', - }, withCredentials: true, ...config, }); axiosInstance.interceptors.request.use( (config) => { - const { accessToken } = getTokens(); + const accessToken = useAuthStore.getState().accessToken; if (accessToken) { config.headers['Authorization'] = `Bearer ${accessToken}`; } @@ -42,31 +36,40 @@ export const createClient = (config?: AxiosRequestConfig) => { async (error) => { const originalRequest = error.config; + if (!originalRequest.retryCount) { + originalRequest.retryCount = 0; + } + if ( error.response && error.response.status === 401 && - !originalRequest._retry + originalRequest.retryCount < 5 ) { - originalRequest._retry = true; + originalRequest.retryCount += 1; - try { - const refreshToken = getTokens().refreshToken; + /** + * http 로컬 환경이라 httpOnly인 refresh 토큰 전송 불가 + * 배포 후 사용 (주석처리) + * 5회 시도후 안되면 로그아웃 처리하기 + */ + // try { + // const refreshResponse = await postRefresh() - const refreshResponse = await axios.post(`${BASE_URL}auth/refresh`, { - refreshToken, - }); + // const { accessToken: newAccessToken } = refreshResponse.data; - const { accessToken: newAccessToken, refreshToken: newRefreshToken } = - refreshResponse.data; + // login(newAccessToken,null); + // originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; - storeLogin(newAccessToken, newRefreshToken); - originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + // return await axiosInstance(originalRequest); + // } catch (refreshError) { + // logout(); + // return Promise.reject(refreshError); + // } - return await axios(originalRequest); - } catch (refreshError) { - storeLogout(); - return Promise.reject(refreshError); - } + logout(); + useAuthStore.persist.clearStorage(); + window.location.href = '/login'; + return Promise.reject(error); } return Promise.reject(error); } diff --git a/src/assets/githubIcon.svg b/src/assets/githubIcon.svg new file mode 100644 index 00000000..190d1ba7 --- /dev/null +++ b/src/assets/githubIcon.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/src/components/common/error/QueryErrorBoundary.tsx b/src/components/common/error/QueryErrorBoundary.tsx index 38e70e22..62e08ba3 100644 --- a/src/components/common/error/QueryErrorBoundary.tsx +++ b/src/components/common/error/QueryErrorBoundary.tsx @@ -2,9 +2,11 @@ import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import type { PropsWithChildren } from 'react'; import ErrorBoundary from './ErrorBoundary'; import ErrorFallback from './ErrorFallback'; +import { useLocation } from 'react-router-dom'; export default function QueryErrorBoundary({ children }: PropsWithChildren) { const { reset } = useQueryErrorResetBoundary(); + const location = useLocation(); return ( ; }} + key={location.pathname} > {children} diff --git a/src/components/common/noContent/NoContent.styled.ts b/src/components/common/noContent/NoContent.styled.ts index 21bbe938..917368d3 100644 --- a/src/components/common/noContent/NoContent.styled.ts +++ b/src/components/common/noContent/NoContent.styled.ts @@ -3,6 +3,7 @@ import styled from 'styled-components'; export const Wrapper = styled.div` width: 100%; height: 100%; + min-height: 100%; display: flex; flex-direction: column; justify-content: center; @@ -14,6 +15,7 @@ export const Wrapper = styled.div` `; export const NoContentText = styled.h1` + height: 100%; font-size: 2rem; font-weight: 600; color: ${({ theme }) => theme.color.navy}; diff --git a/src/components/user/customerService/CustomerServiceHeader.styled.ts b/src/components/user/customerService/CustomerServiceHeader.styled.ts index 76e659d4..cf7ee146 100644 --- a/src/components/user/customerService/CustomerServiceHeader.styled.ts +++ b/src/components/user/customerService/CustomerServiceHeader.styled.ts @@ -37,10 +37,13 @@ export const SearchBarInput = styled.input` export const ButtonWrapper = styled.div` display: flex; + justify-content: center; + align-items: center; gap: 0.5rem; `; -export const UturnButton = styled.button` +export const XButton = styled.button` + height: fit-content; &:hover { color: ${({ theme }) => theme.color.lightnavy}; } diff --git a/src/components/user/customerService/CustomerServiceHeader.tsx b/src/components/user/customerService/CustomerServiceHeader.tsx index 5d3e9939..2eca5341 100644 --- a/src/components/user/customerService/CustomerServiceHeader.tsx +++ b/src/components/user/customerService/CustomerServiceHeader.tsx @@ -1,8 +1,11 @@ import { MagnifyingGlassIcon, XCircleIcon } from '@heroicons/react/24/outline'; import * as S from './CustomerServiceHeader.styled'; import MovedInquiredLink from './MoveInquiredLink'; -import { Outlet } from 'react-router-dom'; -import { useState } from 'react'; +import { Outlet, useLocation, useSearchParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import Modal from '../../common/modal/Modal'; +import { useModal } from '../../../hooks/useModal'; +import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../constants/user/customerService'; interface CustomerServiceHeaderProps { title: string; @@ -16,10 +19,28 @@ export default function CustomerServiceHeader({ onGetKeyword, }: CustomerServiceHeaderProps) { const [inputValue, setInputValue] = useState(''); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const [searchParams, setSearchParams] = useSearchParams(); + + const handleKeyword = (inputValue: string) => { + const newSearchParams = new URLSearchParams(searchParams); + if (inputValue === '') { + newSearchParams.delete('keyword'); + } else { + newSearchParams.set('keyword', inputValue); + } + setSearchParams(newSearchParams); + }; const handleSubmitKeyword = (e: React.FormEvent) => { e.preventDefault(); - onGetKeyword(inputValue); + if (inputValue.trim() === '') { + return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword); + } else { + onGetKeyword(inputValue); + handleKeyword(inputValue); + return; + } }; const handleChangeValue = (e: React.ChangeEvent) => { @@ -30,6 +51,7 @@ export default function CustomerServiceHeader({ const handleReset = () => { onGetKeyword(''); setInputValue(''); + handleKeyword(''); }; return ( @@ -42,18 +64,18 @@ export default function CustomerServiceHeader({ {keyword !== '' && ( - - + )} @@ -63,6 +85,9 @@ export default function CustomerServiceHeader({ + + {message} + ); } diff --git a/src/components/user/customerService/faq/FAQContent.styled.ts b/src/components/user/customerService/faq/FAQContent.styled.ts index ae35aeab..2d7026f5 100644 --- a/src/components/user/customerService/faq/FAQContent.styled.ts +++ b/src/components/user/customerService/faq/FAQContent.styled.ts @@ -32,12 +32,34 @@ export const ListPlusIcon = styled.div<{ $isOpen: boolean }>` } `; -export const ListContentWrapper = styled.div` - cursor: auto; - background: ${({ theme }) => theme.color.lightgrey}; - padding: 1.5rem 1rem; - display: flex; - gap: 0.5rem; +export const ListContentWrapper = styled.div<{ $isShowContent: boolean }>` + max-height: 0; + overflow: hidden; + + @keyframes slice-show { + 0% { + opacity: 0; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } + } + + ${({ $isShowContent }) => + $isShowContent && + css` + max-height: 100vh; + opacity: 1; + cursor: auto; + background: ${({ theme }) => theme.color.lightgrey}; + padding: 1.5rem 1rem; + display: flex; + gap: 0.5rem; + animation: slice-show 300ms; + `} `; export const ListButtonWrapper = styled.div` diff --git a/src/components/user/customerService/faq/FAQContent.tsx b/src/components/user/customerService/faq/FAQContent.tsx index 4788dfcd..bf972785 100644 --- a/src/components/user/customerService/faq/FAQContent.tsx +++ b/src/components/user/customerService/faq/FAQContent.tsx @@ -21,14 +21,12 @@ export default function FAQContent({ list }: FAQContentProps) { - {isFAQContentOpen && ( - - - - - {list.content} - - )} + + + + + {list.content} + ); } diff --git a/src/components/user/customerService/inquiry/Inquiry.styled.ts b/src/components/user/customerService/inquiry/Inquiry.styled.ts index ad912bb5..2a7d2d31 100644 --- a/src/components/user/customerService/inquiry/Inquiry.styled.ts +++ b/src/components/user/customerService/inquiry/Inquiry.styled.ts @@ -112,11 +112,41 @@ export const Content = styled.textarea` font-size: 1rem; `; +export const InquiryFileContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; + min-height: 50px; + position: relative; +`; + export const InquiryFileWrapper = styled.div` display: flex; height: 40px; `; +export const InquirySelectFile = styled(InquiryFileWrapper)` + position: absolute; + right: 0; +`; + +export const InquiryShowFile = styled.span` + display: flex; + justify-content: start; + align-items: center; + overflow: hidden; + padding: 0.5rem; + border: 1px solid ${({ theme }) => theme.color.border}; + width: 50%; + color: ${({ theme }) => theme.color.navy}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; +`; + +export const InquiryDefault = styled(InquiryShowFile)` + padding: 0; +`; + export const InquiryFileLabel = styled.label` display: flex; justify-content: center; @@ -127,8 +157,7 @@ export const InquiryFileLabel = styled.label` background: ${({ theme }) => theme.color.navy}; color: ${({ theme }) => theme.color.white}; border: 1px solid ${({ theme }) => theme.color.navy}; - border-radius: ${({ theme }) => theme.borderRadius.primary} 0 0 - ${({ theme }) => theme.borderRadius.primary}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; &:hover { background: ${({ theme }) => theme.color.lightgrey}; @@ -138,18 +167,6 @@ export const InquiryFileLabel = styled.label` } `; -export const InquiryShowFile = styled.span` - display: flex; - justify-content: start; - align-items: center; - padding: 0.5rem; - border: 1px solid ${({ theme }) => theme.color.border}; - width: 40%; - color: ${({ theme }) => theme.color.navy}; - border-radius: 0 ${({ theme }) => theme.borderRadius.primary} - ${({ theme }) => theme.borderRadius.primary} 0; -`; - export const InquiryFile = styled.input` position: absolute; width: 0; @@ -163,6 +180,22 @@ export const FileImg = styled.img` height: 40px; `; +export const FileDeleteXButton = styled.button` + position: relative; + color: ${({ theme }) => theme.color.navy}; + z-index: 1; + + svg { + border-radius: 50%; + background: ${({ theme }) => theme.color.white}; + border: 1px solid ${({ theme }) => theme.color.navy}; + top: -0.2rem; + left: -0.5rem; + position: absolute; + width: 1rem; + } +`; + export const SendButtonWrapper = styled.div` width: 100%; display: flex; diff --git a/src/components/user/customerService/inquiry/Inquiry.tsx b/src/components/user/customerService/inquiry/Inquiry.tsx index e0982b51..f4ba22f2 100644 --- a/src/components/user/customerService/inquiry/Inquiry.tsx +++ b/src/components/user/customerService/inquiry/Inquiry.tsx @@ -5,7 +5,7 @@ import { My_INQUIRIES_MESSAGE, } from '../../../../constants/user/customerService'; import * as S from './Inquiry.styled'; -import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import { ChevronDownIcon, XMarkIcon } from '@heroicons/react/24/outline'; import React, { useEffect, useState } from 'react'; import type { InquiryFormData } from '../../../../models/inquiry'; import { usePostInquiry } from '../../../../hooks/user/usePostInquiry'; @@ -17,8 +17,6 @@ interface FormStateType { category: string; title: string; content: string; - fileValue: string; - fileImage: string | null; } export default function Inquiry() { @@ -34,13 +32,17 @@ export default function Inquiry() { location.state.from || '' ); const [isCategoryOpen, setIsCategoryOpen] = useState(false); + const [imageFiles, setImageFiles] = useState< + { fileValue: string; preview: string; image: File | null }[] + >([ + { fileValue: My_INQUIRIES_MESSAGE.fileDefault, preview: '', image: null }, + ]); const [form, setForm] = useState({ category: My_INQUIRIES_MESSAGE.categoryDefault, title: '', content: '', - fileValue: My_INQUIRIES_MESSAGE.fileDefault, - fileImage: null, }); + const MAX_FILE_COUNT = 3; const handleSubmitInquiry = (e: React.FormEvent) => { e.preventDefault(); @@ -65,6 +67,17 @@ export default function Inquiry() { content: form.content.trim() !== '', }; + imageFiles.forEach((file) => { + if ( + file.fileValue === My_INQUIRIES_MESSAGE.fileDefault || + file.image === null + ) { + return; + } else { + formData.append('images', file.image); + } + }); + if (!isValid.category) { return handleModalOpen(INQUIRY_MESSAGE.selectCategory); } @@ -80,8 +93,6 @@ export default function Inquiry() { category: My_INQUIRIES_MESSAGE.categoryDefault, title: '', content: '', - fileValue: My_INQUIRIES_MESSAGE.fileDefault, - fileImage: null, }); }; @@ -89,9 +100,10 @@ export default function Inquiry() { setForm((prev) => ({ ...prev, category })); setIsCategoryOpen((prev) => !prev); }; + const handleChangeFile = (e: React.ChangeEvent) => { const fileValue = e.target.value; - const image = e.target.files?.[0]; + const image = e.target.files?.[0] ?? null; const MAX_FILE_SIZE = 5 * 1024 * 1024; if (image && image.size > MAX_FILE_SIZE) { @@ -100,17 +112,63 @@ export default function Inquiry() { return; } - const fileImage = image ? URL.createObjectURL(image) : null; - setForm((prev) => ({ ...prev, fileValue, fileImage })); + const actualFileCount = imageFiles.filter( + (file) => file.fileValue !== My_INQUIRIES_MESSAGE.fileDefault + ).length; + if (actualFileCount >= MAX_FILE_COUNT) { + handleModalOpen(INQUIRY_MESSAGE.maxFileCount); + e.target.value = ''; + return; + } + + const preview = image ? URL.createObjectURL(image) : ''; + setImageFiles((prev) => { + if ( + fileValue.trim() === '' || + prev.some((file) => file.fileValue === fileValue) || + preview === '' + ) + return prev; + if ( + prev.some((file) => file.fileValue === My_INQUIRIES_MESSAGE.fileDefault) + ) { + const removeDefault = prev.filter( + (file) => file.fileValue !== My_INQUIRIES_MESSAGE.fileDefault + ); + + return [...removeDefault, { fileValue, preview, image }]; + } + return [...prev, { fileValue, preview, image }]; + }); + + e.target.value = ''; + }; + + const handleDeleteFile = (e: React.MouseEvent) => { + const deleteFileValue = e.currentTarget.id; + setImageFiles((prev) => { + if (prev.length === 1) { + return [ + { + fileValue: My_INQUIRIES_MESSAGE.fileDefault, + preview: '', + image: null, + }, + ]; + } + return prev.filter((file) => file.fileValue !== deleteFileValue); + }); }; useEffect(() => { return () => { - if (form.fileImage) { - URL.revokeObjectURL(form.fileImage); - } + imageFiles.forEach((file) => { + if (file.preview && file.preview !== '') { + URL.revokeObjectURL(file.preview); + } + }); }; - }, [form.fileImage]); + }, [imageFiles]); return ( @@ -174,18 +232,35 @@ export default function Inquiry() { setForm((prev) => ({ ...prev, content: e.target.value })) } > - - 파일찾기 - {form.fileValue} - handleChangeFile(e)} - /> - {form.fileImage && } - + + + + 파일찾기 + + handleChangeFile(e)} + /> + + {imageFiles.map((list) => ( + + {list.fileValue} + {list.preview && } + {list.preview && ( + + + + )} + + ))} + 제출 diff --git a/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx b/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx index e9bdcc44..cf77fa0b 100644 --- a/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx +++ b/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx @@ -11,6 +11,7 @@ export default function NoticeDetailBundle() { const location = useLocation(); const { noticeId } = useParams(); const id = noticeId || String(location.state.id); + const keyword = location.state?.keyword ?? ''; const { noticeDetail: noticeDetailData, isLoading } = useGetNoticeDetail(id); @@ -57,7 +58,7 @@ export default function NoticeDetailBundle() { createdAt={createdAt} viewCount={viewCount} /> - + ); } diff --git a/src/components/user/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx b/src/components/user/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx index 370bf5d3..71929605 100644 --- a/src/components/user/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx +++ b/src/components/user/customerService/noticeDetail/bottom/NoticeDetailBottom.tsx @@ -7,11 +7,13 @@ import * as S from './NoticeDetailBottom.styled'; interface NoticeDetailBottomProps { prev: OtherNotice | null; next: OtherNotice | null; + keyword: string; } export default function NoticeDetailBottom({ prev, next, + keyword, }: NoticeDetailBottomProps) { return ( @@ -37,7 +39,7 @@ export default function NoticeDetailBottom({ ) : ( 다음 공지사항이 없습니다. )} - + ); } diff --git a/src/components/user/customerService/noticeDetail/bottom/button/ListButton.tsx b/src/components/user/customerService/noticeDetail/bottom/button/ListButton.tsx index 34a15300..1cae25b3 100644 --- a/src/components/user/customerService/noticeDetail/bottom/button/ListButton.tsx +++ b/src/components/user/customerService/noticeDetail/bottom/button/ListButton.tsx @@ -2,12 +2,20 @@ import { ROUTES } from '../../../../../../constants/user/routes'; import ContentBorder from '../../../../../common/contentBorder/ContentBorder'; import * as S from './ListButton.styled'; -export default function ListButton() { +interface ListButtonProps { + keyword: string; +} + +export default function ListButton({ keyword }: ListButtonProps) { + const isKeyword = keyword ? `?keyword=${keyword}` : ``; + return ( <> - + 목록 diff --git a/src/components/user/mypage/ContentTab.styled.ts b/src/components/user/mypage/ContentTab.styled.ts index 974acd21..84361fef 100644 --- a/src/components/user/mypage/ContentTab.styled.ts +++ b/src/components/user/mypage/ContentTab.styled.ts @@ -74,7 +74,13 @@ export const WrapperButton = styled.div<{ $height: string }>` export const FilterContainer = styled.div` width: 100%; - height: 100%; background-color: ${({ theme }) => theme.color.lightgrey}; border-radius: ${({ theme }) => theme.borderRadius.large}; + + &:has(> div[data-type='noContent']) { + min-height: 100%; + display: flex; + justify-content: center; + align-items: center; + } `; diff --git a/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.styled.ts b/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.styled.ts index a423a17f..43576d0d 100644 --- a/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.styled.ts +++ b/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.styled.ts @@ -1,14 +1,11 @@ import styled from 'styled-components'; +import { WrapperNoContent } from '../../notifications/all/All.styled'; export const Container = styled.div` height: 100%; `; -export const WrapperNoContent = styled.div` - height: 100%; - display: flex; - align-items: center; -`; +export const WrapperNoContentAppliedProjects = styled(WrapperNoContent)``; export const CommentsWrapper = styled.div` padding: 1.5rem; diff --git a/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.tsx b/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.tsx index d935e8e5..2b7ad066 100644 --- a/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.tsx +++ b/src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.tsx @@ -9,14 +9,18 @@ export default function CommentsActivity() { const { myCommentsData, isLoading } = useGetMyComments(); if (isLoading) { - return ; + return ( + + + + ); } if (!myCommentsData || myCommentsData.length === 0) { return ( - + - + ); } diff --git a/src/components/user/mypage/activityLog/inquiries/Inquiries.styled.ts b/src/components/user/mypage/activityLog/inquiries/Inquiries.styled.ts index 080866c5..4bb756a6 100644 --- a/src/components/user/mypage/activityLog/inquiries/Inquiries.styled.ts +++ b/src/components/user/mypage/activityLog/inquiries/Inquiries.styled.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { WrapperNoContent } from '../../notifications/all/All.styled'; export const container = styled.div` height: 100%; @@ -50,8 +51,4 @@ export const InquiriesWrapper = styled.div` gap: 1.5rem; `; -export const WrapperNoContent = styled.div` - height: 95%; - display: flex; - align-items: center; -`; +export const WrapperNoContentAppliedProjects = styled(WrapperNoContent)``; diff --git a/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx b/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx index ecb5ba25..338eaa9e 100644 --- a/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx +++ b/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx @@ -9,14 +9,18 @@ export default function Inquiries() { const { myInquiriesData, isLoading } = useGetMyInquiries(); if (isLoading) { - return ; + return ( + + + + ); } if (!myInquiriesData || myInquiriesData?.length === 0) return ( - + - + ); return ( diff --git a/src/components/user/mypage/activityLog/inquiries/inquiry/Inquiry.styled.ts b/src/components/user/mypage/activityLog/inquiries/inquiry/Inquiry.styled.ts index ab572644..b9312cf9 100644 --- a/src/components/user/mypage/activityLog/inquiries/inquiry/Inquiry.styled.ts +++ b/src/components/user/mypage/activityLog/inquiries/inquiry/Inquiry.styled.ts @@ -91,4 +91,7 @@ export const ModalImgMessageWrapper = styled.div` padding: 0.2rem; `; -export const ModalImg = styled.img``; +export const ModalImg = styled.img` + max-width: 90vw; + max-height: 90vh; +`; diff --git a/src/components/user/mypage/joinedProject/MyJoinProjects.styled.ts b/src/components/user/mypage/joinedProject/MyJoinProjects.styled.ts index 28cb3c6c..d3adc831 100644 --- a/src/components/user/mypage/joinedProject/MyJoinProjects.styled.ts +++ b/src/components/user/mypage/joinedProject/MyJoinProjects.styled.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { WrapperNoContent } from '../notifications/all/All.styled'; export const Container = styled.section` width: 100%; @@ -20,11 +21,9 @@ export const Section = styled.div` flex-direction: column; `; -export const NoWrapper = styled.div` - width: 100%; - height: 80%; - padding: 2rem 0 5rem; -`; +export const ContainerNoContentMyJoinedProjects = styled(WrapperNoContent)``; + +export const WrapperNoContentMyJoinedProjects = styled.div``; export const Wrapper = styled.div` display: flex; @@ -39,7 +38,7 @@ export const WrapperProject = styled.div` flex-wrap: wrap; justify-content: space-between; gap: 2rem; - height: 100%; + min-height: 100%; @media ${({ theme }) => theme.mediaQuery.tablet} { padding: 1rem; diff --git a/src/components/user/mypage/joinedProject/MyJoinProjects.tsx b/src/components/user/mypage/joinedProject/MyJoinProjects.tsx index 85f6babb..4774c168 100644 --- a/src/components/user/mypage/joinedProject/MyJoinProjects.tsx +++ b/src/components/user/mypage/joinedProject/MyJoinProjects.tsx @@ -15,29 +15,36 @@ const MyJoinProjects = () => { } return ( - - - 참여한 프로젝트 리스트 - - {myJoinedProjectListData && myJoinedProjectListData?.length > 0 ? ( + <> + + + 참여한 프로젝트 리스트 + - - {myJoinedProjectListData?.map((project) => ( - - - - ))} - + {myJoinedProjectListData && myJoinedProjectListData?.length > 0 ? ( + + {myJoinedProjectListData?.map((project) => ( + + + + ))} + + ) : ( + + + + + + )} - ) : ( - - - - )} - + + ); }; diff --git a/src/components/user/mypage/joinedProject/Project.styled.ts b/src/components/user/mypage/joinedProject/Project.styled.ts index 0c7e22aa..db2726d1 100644 --- a/src/components/user/mypage/joinedProject/Project.styled.ts +++ b/src/components/user/mypage/joinedProject/Project.styled.ts @@ -115,11 +115,14 @@ export const State = styled.span` export const Skill = styled.div` display: flex; - flex-wrap: wrap; - justify-content: space-between; align-items: center; - gap: 0.6rem; + justify-content: space-between; +`; +export const SkillArea = styled.div` + display: flex; + flex-wrap: wrap; + gap: 0.6rem; @media ${({ theme }) => theme.mediaQuery.tablet} { gap: 0.2rem; } @@ -141,8 +144,6 @@ export const Skill = styled.div` } `; -export const SkillArea = styled.div``; - export const EvaluateButton = styled(Link)` display: inline-flex; flex-shrink: 0; diff --git a/src/components/user/mypage/myProfile/editProfile/editProfile.styled.ts b/src/components/user/mypage/myProfile/editProfile/EditProfile.styled.ts similarity index 96% rename from src/components/user/mypage/myProfile/editProfile/editProfile.styled.ts rename to src/components/user/mypage/myProfile/editProfile/EditProfile.styled.ts index 2ff92941..4761f6fd 100644 --- a/src/components/user/mypage/myProfile/editProfile/editProfile.styled.ts +++ b/src/components/user/mypage/myProfile/editProfile/EditProfile.styled.ts @@ -44,11 +44,13 @@ export const InputBeginner = styled.input` `; export const InputTextGithub = styled.div` - width: 100%; + width: 70%; +`; - @media ${({ theme }) => theme.mediaQuery.tablet} { - width: 100%; - } +export const GithubImg = styled.img` + width: 1.5rem; + margin-right: 0.3rem; + filter: invert(1); `; export const InputTextCareer = styled.div` diff --git a/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx b/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx index 6c1fecb9..6ebceb14 100644 --- a/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx +++ b/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx @@ -1,4 +1,4 @@ -import * as S from './editProfile.styled'; +import * as S from './EditProfile.styled'; import OptionBox from './../OptionBox'; import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { useEffect, useState } from 'react'; @@ -15,18 +15,22 @@ import { useEditMyProfileInfo } from '../../../../../hooks/user/useMyInfo'; import useNickNameVerification from '../../../../../hooks/user/useNicknameVerification'; import { ROUTES } from '../../../../../constants/user/routes'; import Button from '../../../../common/Button/Button'; -import { ERROR_MESSAGES } from '../../../../../constants/user/authConstants'; +import { + ERROR_MESSAGES, + OAUTH_PROVIDERS, +} from '../../../../../constants/user/authConstants'; +import githubIcon from '../../../../../assets/githubIcon.svg'; type ProfileFormData = z.infer; export default function EditProfile() { const [nickname, setNickname] = useState(''); const { - myData, + userInfoData, scrollRef, handleModalOpen, }: { - myData: UserInfo; + userInfoData: UserInfo; scrollRef: React.RefObject; handleModalOpen: (message: string) => void; } = useOutletContext(); @@ -35,6 +39,14 @@ export default function EditProfile() { const { nicknameMessage, handleDuplicationNickname } = useNickNameVerification(); const navigate = useNavigate(); + const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; + const github = { + ...OAUTH_PROVIDERS.filter((oauth) => oauth.name.includes('github'))[0], + }; + + const handleClickGithubValidation = () => { + window.location.href = `${BASE_URL}/${github.url}`; + }; const { control, @@ -62,14 +74,14 @@ export default function EditProfile() { }, [scrollRef]); useEffect(() => { - if (myData) { - const skillTagIds = myData.skills + if (userInfoData) { + const skillTagIds = userInfoData.skills .map( (skill) => skillTagsData.find((tag) => tag.name === skill.name)?.id ) .filter((id): id is number => id !== undefined); - const positionTagIds = myData.positions + const positionTagIds = userInfoData.positions .map( (position) => positionTagsData.find((tag) => tag.id === position.id)?.id @@ -77,14 +89,14 @@ export default function EditProfile() { .filter((id): id is number => id !== undefined); reset({ - nickname: myData.nickname, - bio: myData.bio || '', - beginner: myData.beginner, + nickname: userInfoData.nickname, + bio: userInfoData.bio || '', + beginner: userInfoData.beginner, positionTagIds, - github: myData.github || '', + github: userInfoData.github || '', skillTagIds, - career: myData.career?.length - ? myData.career.map((item) => ({ + career: userInfoData.career?.length + ? userInfoData.career.map((item) => ({ name: item.name, periodStart: item.periodStart.split('T')[0], periodEnd: item.periodEnd.split('T')[0], @@ -93,7 +105,7 @@ export default function EditProfile() { : [{ name: '', periodStart: '', periodEnd: '', role: '' }], }); } - }, [myData, skillTagsData, positionTagsData, reset]); + }, [userInfoData, skillTagsData, positionTagsData, reset]); const { fields, append, remove } = useFieldArray({ control, name: 'career' }); @@ -264,6 +276,16 @@ export default function EditProfile() { {errors.github && ( {errors.github.message} )} + )} /> diff --git a/src/components/user/mypage/notifications/all/All.styled.ts b/src/components/user/mypage/notifications/all/All.styled.ts index 547fbadc..43762987 100644 --- a/src/components/user/mypage/notifications/all/All.styled.ts +++ b/src/components/user/mypage/notifications/all/All.styled.ts @@ -4,6 +4,7 @@ export const WrapperNoContent = styled.div` height: 100%; display: flex; align-items: center; + justify-content: center; `; export const container = styled.section` diff --git a/src/components/user/mypage/notifications/all/All.tsx b/src/components/user/mypage/notifications/all/All.tsx index e9d0da55..2855a368 100644 --- a/src/components/user/mypage/notifications/all/All.tsx +++ b/src/components/user/mypage/notifications/all/All.tsx @@ -29,7 +29,11 @@ export default function All() { }; if (isLoading) { - return ; + return ( + + + + ); } const filterLength = alarmListData?.filter((list) => { @@ -43,7 +47,7 @@ export default function All() { if (!alarmListData || alarmListData.length === 0 || filterLength === 0) { return ( - + ); diff --git a/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.styled.ts b/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.styled.ts index 2b173b19..9b77cb71 100644 --- a/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.styled.ts +++ b/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.styled.ts @@ -1,4 +1,7 @@ import styled from 'styled-components'; +import { WrapperNoContent } from '../all/All.styled'; + +export const WrapperNoContentAppliedProjects = styled(WrapperNoContent)``; export const container = styled.div` width: 100%; diff --git a/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.tsx b/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.tsx index 610c0a19..f84c37eb 100644 --- a/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.tsx +++ b/src/components/user/mypage/notifications/appliedProjects/AppliedProjects.tsx @@ -10,11 +10,19 @@ export default function AppliedProjects() { const { myAppliedStatusListData, isLoading } = useMyAppliedStatusList(); if (isLoading) { - return ; + return ( + + + + ); } if (!myAppliedStatusListData || myAppliedStatusListData.length === 0) { - return ; + return ( + + + + ); } return ( diff --git a/src/components/user/userPage/userProfile/UserProfile.tsx b/src/components/user/userPage/userProfile/UserProfile.tsx index fb30315d..86c14ffe 100644 --- a/src/components/user/userPage/userProfile/UserProfile.tsx +++ b/src/components/user/userPage/userProfile/UserProfile.tsx @@ -15,7 +15,7 @@ const UserProfile = () => { const scrollRef = useRef(null); if (isLoading) { - return ; + return ; } if (!userData) { diff --git a/src/components/user/userPage/userProjectList/UserProjectList.tsx b/src/components/user/userPage/userProjectList/UserProjectList.tsx index d356a3d8..9742d612 100644 --- a/src/components/user/userPage/userProjectList/UserProjectList.tsx +++ b/src/components/user/userPage/userProjectList/UserProjectList.tsx @@ -19,8 +19,8 @@ export default function UserProjects() { {title} - {userProjectData && userProjectData.length > 0 ? ( - + + {userProjectData && userProjectData.length > 0 ? ( {userProjectData?.map((project) => ( ))} - - ) : ( - - - - )} + ) : ( + + + + + + )} + ); } diff --git a/src/constants/user/customerService.ts b/src/constants/user/customerService.ts index be80ebcc..039f5f1a 100644 --- a/src/constants/user/customerService.ts +++ b/src/constants/user/customerService.ts @@ -8,9 +8,13 @@ export const INQUIRY_CATEGORY = [ export const EMPTY_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' as const; +export const MODAL_MESSAGE_CUSTOMER_SERVICE = { + noKeyword: '검색어를 입력하세요.', +}; + export const My_INQUIRIES_MESSAGE = { categoryDefault: '카테고리', - fileDefault: '선택된 파일이 없습니다.', + fileDefault: '파일을 선택해 주세요. (최대 3개)', blowUpMessage: '클릭하면 이미지를 크게 볼 수 있습니다.', isImageOpenMessage: '이미지를 클릭하면 사라집니다.', }; @@ -22,4 +26,5 @@ export const INQUIRY_MESSAGE = { inquiredSuccess: '문의글이 작성되었습니다.', inquiredError: '문의글 작성에 실패하였습니다.', validationFile: '파일 크기는 5MB 이하만 가능합니다.', + maxFileCount: '최대 3개까지만 업로드 할 수 있습니다.', }; diff --git a/src/constants/user/routes.ts b/src/constants/user/routes.ts index a2618850..020f9f6e 100644 --- a/src/constants/user/routes.ts +++ b/src/constants/user/routes.ts @@ -10,7 +10,7 @@ export const ROUTES = { manageProjectsRoot: '/manage', manageProjectsPassNonPass: '/manage/pass-nonpass', mypage: '/mypage', - mypageEdit: '/mypage/edit', + mypageEdit: 'edit', joinedProjects: 'joined-projects', managedProjects: 'managed-projects', myPageNotifications: 'notifications', @@ -28,5 +28,5 @@ export const ROUTES = { noticeDetail: 'notice-detail', inquiry: '/inquiry', evaluation: '/evaluation', - loginSuccess: '/login/oauth2/code', + loginSuccess: '/oauth-redirect', } as const; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index aa2b5684..259e4883 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -13,7 +13,7 @@ import { changePasswordFormValues } from '../pages/user/changePassword/ChangePas export const useAuth = (handleModalOpen: (message: string) => void) => { const navigate = useNavigate(); - const { storeLogin, storeLogout } = useAuthStore.getState(); + const { login, logout } = useAuthStore.getState(); const queryClient = useQueryClient(); const signupMutation = useMutation< @@ -61,15 +61,15 @@ export const useAuth = (handleModalOpen: (message: string) => void) => { >({ mutationFn: async ({ email, password }) => { const response = await postLogin({ email, password }); - const { accessToken, refreshToken } = response.data; + const { accessToken } = response.data; const userData = response.user; - return { accessToken, refreshToken, userData }; + return { accessToken, userData }; }, onSuccess: async (data) => { - const { accessToken, refreshToken, userData } = data; + const { accessToken, userData } = data; handleModalOpen(MODAL_MESSAGE.loginSuccess); setTimeout(() => { - storeLogin(accessToken, refreshToken, userData); + login(accessToken, userData); navigate(ROUTES.main); }, 1000); }, @@ -95,8 +95,9 @@ export const useAuth = (handleModalOpen: (message: string) => void) => { }; const userLogout = () => { - storeLogout(); + logout(); queryClient.removeQueries({ queryKey: myInfoKey.myProfile }); + useAuthStore.persist.clearStorage(); handleModalOpen(MODAL_MESSAGE.logout); setTimeout(() => { navigate(ROUTES.main); diff --git a/src/hooks/useOauth.ts b/src/hooks/useOauth.ts new file mode 100644 index 00000000..5c615298 --- /dev/null +++ b/src/hooks/useOauth.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getOauthLogin } from '../api/auth.api'; + +export const useOauth = () => { + const { data, isLoading, isError } = useQuery({ + queryKey: [], + queryFn: () => getOauthLogin(), + }); +}; diff --git a/src/hooks/user/useAlarmDelete.ts b/src/hooks/user/useAlarmDelete.ts index 16f437f7..cefa4a5c 100644 --- a/src/hooks/user/useAlarmDelete.ts +++ b/src/hooks/user/useAlarmDelete.ts @@ -4,7 +4,7 @@ import { AlarmList } from '../queries/user/keys'; import useAuthStore from '../../store/authStore'; export const useAlarmDelete = () => { - const userId = useAuthStore((state) => state.userData?.id); + const userId = useAuthStore.getState().userData?.id; const queryClient = useQueryClient(); const mutation = useMutation({ diff --git a/src/hooks/user/useAlarmList.ts b/src/hooks/user/useAlarmList.ts index c0a5a33b..19f4241a 100644 --- a/src/hooks/user/useAlarmList.ts +++ b/src/hooks/user/useAlarmList.ts @@ -4,7 +4,7 @@ import { getAlarmList } from '../../api/alarm.api'; import useAuthStore from '../../store/authStore'; const useAlarmList = () => { - const userId = useAuthStore((state) => state.userData?.id); + const userId = useAuthStore.getState().userData?.id; const { data: alarmListData, diff --git a/src/hooks/user/useAlarmPatch.ts b/src/hooks/user/useAlarmPatch.ts index e7e5cafc..d063a40b 100644 --- a/src/hooks/user/useAlarmPatch.ts +++ b/src/hooks/user/useAlarmPatch.ts @@ -4,7 +4,7 @@ import { AlarmList } from '../queries/user/keys'; import useAuthStore from '../../store/authStore'; export const useAlarmPatch = () => { - const userId = useAuthStore((state) => state.userData?.id); + const userId = useAuthStore.getState().userData?.id; const queryClient = useQueryClient(); const mutation = useMutation({ diff --git a/src/hooks/user/useGetMyComments.ts b/src/hooks/user/useGetMyComments.ts index 59d8b83e..188b7e8a 100644 --- a/src/hooks/user/useGetMyComments.ts +++ b/src/hooks/user/useGetMyComments.ts @@ -4,7 +4,7 @@ import useAuthStore from '../../store/authStore'; import { ActivityLog } from '../queries/user/keys'; export const useGetMyComments = () => { - const userId = useAuthStore((state) => state.userData?.id); + const userId = useAuthStore.getState().userData?.id; const { data: myCommentsData, isLoading } = useQuery({ queryKey: [ActivityLog.myComments, userId], diff --git a/src/hooks/user/useGetMyInquiries.ts b/src/hooks/user/useGetMyInquiries.ts index d7d0de9b..ec38dc8d 100644 --- a/src/hooks/user/useGetMyInquiries.ts +++ b/src/hooks/user/useGetMyInquiries.ts @@ -4,7 +4,7 @@ import useAuthStore from '../../store/authStore'; import { ActivityLog } from '../queries/user/keys'; export const useGetMyInquiries = () => { - const userId = useAuthStore((state) => state.userData?.id); + const userId = useAuthStore.getState().userData?.id; const { data: myInquiriesData, isLoading } = useQuery({ queryKey: [ActivityLog.myInquiries, userId], diff --git a/src/hooks/user/useMyInfo.ts b/src/hooks/user/useMyInfo.ts index 20e1504f..08ce923f 100644 --- a/src/hooks/user/useMyInfo.ts +++ b/src/hooks/user/useMyInfo.ts @@ -19,7 +19,7 @@ import type { } from '../../models/userProject'; export const useMyProfileInfo = () => { - const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const isLoggedIn = useAuthStore.getState().isLoggedIn; const { data, isLoading } = useQuery({ queryKey: myInfoKey.myProfile, @@ -87,7 +87,7 @@ export const useUploadProfileImg = ( }; export const useMyJoinedProjectList = () => { - const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const isLoggedIn = useAuthStore.getState().isLoggedIn; const { data, isLoading } = useQuery({ queryKey: ProjectListKey.myJoinedList, @@ -99,7 +99,7 @@ export const useMyJoinedProjectList = () => { }; export const useMyAppliedStatusList = () => { - const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const isLoggedIn = useAuthStore.getState().isLoggedIn; const { data, isLoading } = useQuery({ queryKey: ProjectListKey.myAppliedStatusList, diff --git a/src/hooks/user/useNotification.ts b/src/hooks/user/useNotification.ts index 54f5c343..4dccf75f 100644 --- a/src/hooks/user/useNotification.ts +++ b/src/hooks/user/useNotification.ts @@ -3,13 +3,14 @@ import { useEffect, useRef, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { AlarmList } from '../queries/user/keys'; import type { AlarmLive } from '../../models/alarm'; -import useAuthStore, { getTokens } from '../../store/authStore'; +import useAuthStore from '../../store/authStore'; import { useToast } from '../useToast'; const useNotification = () => { const [signalData, setSignalData] = useState(null); const queryClient = useQueryClient(); - const userId = useAuthStore((state) => state.userData?.id); + const accessToken = useAuthStore.getState().accessToken; + const userId = useAuthStore.getState().userData?.id; const { showToast } = useToast(); const eventSourceRef = useRef(null); @@ -33,10 +34,7 @@ const useNotification = () => { `${import.meta.env.VITE_APP_API_BASE_URL}user/sse`, { headers: { - Authorization: - getTokens().accessToken || getTokens().refreshToken - ? `Bearer ${getTokens().accessToken}` - : '', + Authorization: accessToken ? `Bearer ${accessToken}` : '', 'Content-Type': 'application/json', }, heartbeatTimeout: 12 * 60 * 1000, @@ -64,7 +62,7 @@ const useNotification = () => { eventSource.onerror = (e) => { console.error(e); }; - }, [queryClient, userId]); + }, [queryClient, userId, accessToken, EventSourceImpl]); useEffect(() => { if (signalData) { diff --git a/src/hooks/user/usePostInquiry.ts b/src/hooks/user/usePostInquiry.ts index 6a38f521..bca5d3a0 100644 --- a/src/hooks/user/usePostInquiry.ts +++ b/src/hooks/user/usePostInquiry.ts @@ -10,7 +10,7 @@ export const usePostInquiry = ( handleModalOpen: (message: string) => void, pathname: string = '' ) => { - const userId = useAuthStore((state) => state.userData?.id); + const userId = useAuthStore.getState().userData?.id; const navigate = useNavigate(); const queryClient = useQueryClient(); diff --git a/src/hooks/user/useUserInfo.ts b/src/hooks/user/useUserInfo.ts index be09dc80..71e128d6 100644 --- a/src/hooks/user/useUserInfo.ts +++ b/src/hooks/user/useUserInfo.ts @@ -5,7 +5,7 @@ import type { ApiUserInfo } from '../../models/userInfo'; import { getUserInfo } from '../../api/userpage.api'; export const useUserProfileInfo = (id: number) => { - const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const isLoggedIn = useAuthStore.getState().isLoggedIn; const { data, isLoading } = useQuery({ queryKey: [userInfoKey.userProfile, id], diff --git a/src/models/auth.ts b/src/models/auth.ts index a0a12380..458676e5 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -1,6 +1,4 @@ //model - -import { UserData } from '../store/authStore'; import { ApiCommonType } from './apiCommon'; export interface VerifyEmail { @@ -14,6 +12,17 @@ export interface ApiVerifyNickname extends ApiCommonType { export interface LoginResponse { accessToken: string; - refreshToken: string; userData: UserData; } + +export interface UserData { + id: number; + email: string; + nickname: string; + admin: boolean; +} + +export interface ApiOauth extends ApiCommonType { + data: Pick; + user: UserData; +} diff --git a/src/pages/login/LoginApi.tsx b/src/pages/login/LoginApi.tsx new file mode 100644 index 00000000..502dd79a --- /dev/null +++ b/src/pages/login/LoginApi.tsx @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import useAuthStore from '../../store/authStore'; +import { ROUTES } from '../../constants/user/routes'; +import * as S from './Login.styled'; +import { Spinner } from '../../components/common/loadingSpinner/LoadingSpinner.styled'; +import Modal from '../../components/common/modal/Modal'; +import { useModal } from '../../hooks/useModal'; +import { AUTH_MESSAGE } from '../../constants/user/authConstants'; +import { getOauthLogin } from '../../api/auth.api'; + +export default function LoginApi() { + const login = useAuthStore((state) => state.login); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + + useEffect(() => { + (async () => { + const oauthAccessToken = searchParams.get('accessToken'); + + if (oauthAccessToken) { + const result = await getOauthLogin(oauthAccessToken); + const { data, user } = result; + const { accessToken } = data; + + login(accessToken, user); + navigate(ROUTES.main); + } else { + handleModalOpen(AUTH_MESSAGE.isNotToken); + setTimeout(() => { + navigate(ROUTES.login); + }, 1000); + } + })(); + }, [searchParams, login, handleModalOpen, navigate]); + + return ( + + + + {message} + + + ); +} diff --git a/src/pages/login/LoginSuccess.tsx b/src/pages/login/LoginSuccess.tsx index 53a33a2b..9bc05d1d 100644 --- a/src/pages/login/LoginSuccess.tsx +++ b/src/pages/login/LoginSuccess.tsx @@ -10,16 +10,16 @@ import { AUTH_MESSAGE } from '../../constants/user/authConstants'; function LoginSuccess() { const [searchParams] = useSearchParams(); - const { storeLogin } = useAuthStore.getState(); + const login = useAuthStore((state) => state.login); const navigate = useNavigate(); const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); useEffect(() => { const accessToken = searchParams.get('accessToken'); + console.log('accessToken', accessToken); if (accessToken) { - storeLogin(accessToken); - localStorage.setItem('accessToken', accessToken); + login(accessToken, null); navigate(ROUTES.main); } else { handleModalOpen(AUTH_MESSAGE.isNotToken); @@ -27,7 +27,7 @@ function LoginSuccess() { navigate(ROUTES.login); }, 1000); } - }, [searchParams, storeLogin, handleModalOpen, navigate]); + }, [searchParams, login, handleModalOpen, navigate]); return ( diff --git a/src/pages/user/apply/Apply.tsx b/src/pages/user/apply/Apply.tsx index c6d4371e..043074c4 100644 --- a/src/pages/user/apply/Apply.tsx +++ b/src/pages/user/apply/Apply.tsx @@ -23,6 +23,7 @@ const Apply = () => { const { data: projectData, isLoading, isFetching } = useGetProjectData(id); const { applyProject } = useApplyProject({ id, handleModalOpen }); const userEmail = useAuthStore((state) => state.userData?.email); + const { handleSubmit: onSubmitHandler, formState: { errors }, diff --git a/src/pages/user/customerService/notice/Notice.tsx b/src/pages/user/customerService/notice/Notice.tsx index c53b4b3f..c46ffcba 100644 --- a/src/pages/user/customerService/notice/Notice.tsx +++ b/src/pages/user/customerService/notice/Notice.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import * as S from './Notice.styled'; import type { NoticeSearch } from '../../../../models/customerService'; import { useGetNotice } from '../../../../hooks/user/useGetNotice'; @@ -9,21 +9,33 @@ import { ROUTES } from '../../../../constants/user/routes'; import NoticeList from '../../../../components/user/customerService/notice/NoticeList'; import NoResult from '../../../../components/common/noResult/NoResult'; import Pagination from '../../../../components/common/pagination/Pagination'; +import { useLocation } from 'react-router-dom'; export default function Notice() { - const [keyword, setKeyword] = useState({ + const [noticeSearch, setNoticeSearch] = useState({ keyword: '', page: 1, }); const [value, setValue] = useState(''); - const { noticeData, isLoading } = useGetNotice(keyword); + const { noticeData, isLoading } = useGetNotice(noticeSearch); + const location = useLocation(); + const hasKeyword = location.search + ? decodeURI(location.search.split('=')[1]) + : ''; + + useEffect(() => { + if (hasKeyword) { + setNoticeSearch((prev) => ({ ...prev, keyword: hasKeyword })); + setValue(hasKeyword); + } + }, [hasKeyword]); const handleGetKeyword = (keyword: string) => { - setKeyword((prev) => ({ ...prev, keyword })); + setNoticeSearch((prev) => ({ ...prev, keyword })); setValue(keyword); }; const handleChangePagination = (page: number) => { - setKeyword((prev) => ({ ...prev, page })); + setNoticeSearch((prev) => ({ ...prev, page })); }; if (isLoading) { @@ -52,7 +64,7 @@ export default function Notice() { noticeData.notices.map((list) => ( @@ -64,7 +76,7 @@ export default function Notice() { )} diff --git a/src/pages/user/manage/myProjectList/MyProjectList.styled.ts b/src/pages/user/manage/myProjectList/MyProjectList.styled.ts index ead5e9a5..55d9d393 100644 --- a/src/pages/user/manage/myProjectList/MyProjectList.styled.ts +++ b/src/pages/user/manage/myProjectList/MyProjectList.styled.ts @@ -1,9 +1,16 @@ import styled from 'styled-components'; +import { WrapperNoContent } from '../../../../components/user/mypage/notifications/all/All.styled'; + +export const WrapperNoContentMyProjectList = styled(WrapperNoContent)` + height: 80vh; + justify-content: center; +`; export const ManageProjectsContainer = styled.div` width: 100%; display: flex; justify-content: center; + margin-bottom: 2rem; `; export const ManageProjectsWrapper = styled.div` diff --git a/src/pages/user/manage/myProjectList/MyProjectList.tsx b/src/pages/user/manage/myProjectList/MyProjectList.tsx index 08945980..2a234b15 100644 --- a/src/pages/user/manage/myProjectList/MyProjectList.tsx +++ b/src/pages/user/manage/myProjectList/MyProjectList.tsx @@ -1,10 +1,19 @@ +import { Spinner } from '../../../../components/common/loadingSpinner/LoadingSpinner.styled'; import Title from '../../../../components/common/title/Title'; import CardList from '../../../../components/user/manageProjects/CardList'; import { useManagedProjects } from '../../../../hooks/user/useManagedProjects'; import * as S from './MyProjectList.styled'; const MyProjectList = () => { - const { managedProjects } = useManagedProjects(); + const { managedProjects, isLoading } = useManagedProjects(); + + if (isLoading) { + return ( + + + + ); + } return ( diff --git a/src/pages/user/projectDetail/ProjectDetail.tsx b/src/pages/user/projectDetail/ProjectDetail.tsx index 21062da5..95efed20 100644 --- a/src/pages/user/projectDetail/ProjectDetail.tsx +++ b/src/pages/user/projectDetail/ProjectDetail.tsx @@ -23,7 +23,7 @@ const ProjectDetail = () => { const { isOpen, message, handleModalClose, handleModalOpen, handleConfirm } = useModal(); const { data, isLoading, isFetching } = useGetProjectData(id); - const { userData } = useAuthStore((state) => state); + const userData = useAuthStore((state) => state.userData); useEffect(() => { if (!isLoading && !isFetching && !data) { diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 2f4b65ca..12365b63 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -15,6 +15,7 @@ import { ToastProvider } from '../components/common/Toast/ToastProvider'; import { ROUTES } from '../constants/user/routes'; const Login = lazy(() => import('../pages/login/Login')); const LoginSuccess = lazy(() => import('../pages/login/LoginSuccess')); +const LoginApi = lazy(() => import('../pages/login/LoginApi')); const Register = lazy(() => import('../pages/user/register/Register')); const ChangePassword = lazy( () => import('../pages/user/changePassword/ChangePassword') @@ -114,7 +115,7 @@ const AppRoutes = () => { }, { path: ROUTES.loginSuccess, - element: , + element: , }, { path: ROUTES.signup, diff --git a/src/store/authStore.ts b/src/store/authStore.ts index c08f4a72..25416c9a 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -1,32 +1,16 @@ import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { createJSONStorage, persist } from 'zustand/middleware'; import { decryptData, encryptData } from '../util/cryptoUtils'; - -export interface UserData { - id: number; - email: string; - nickname: string; - hasRequiredTags: boolean; -} +import type { UserData } from '../models/auth'; interface AuthState { isLoggedIn: boolean; userData: UserData | null; - storeLogin: ( - accessToken: string, - refreshToken?: string, - userData?: UserData - ) => void; - storeLogout: () => void; + accessToken: string | null; + login: (accessToken: string, userData: UserData | null) => void; + logout: () => void; } -const initialUserData: UserData = { - id: 0, - email: '', - nickname: '', - hasRequiredTags: true, -}; - export const getStoredUserData = () => { const encryptedData = localStorage.getItem('userData'); return encryptedData ? decryptData(encryptedData) : null; @@ -39,40 +23,27 @@ export const getTokens = () => { return { accessToken, refreshToken }; }; -const setTokens = (accessToken: string, refreshToken?: string) => { - localStorage.setItem('accessToken', accessToken); - if (refreshToken) { - localStorage.setItem('refreshToken', refreshToken); - } -}; - -const removeTokens = () => { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); -}; - const useAuthStore = create( persist( (set) => ({ - isLoggedIn: getTokens()?.accessToken ? true : false, - userData: getTokens()?.accessToken ? initialUserData : null, - - storeLogin: ( - accessToken: string, - refreshToken?: string, - userData?: UserData - ) => { - setTokens(accessToken, refreshToken); - localStorage.setItem('userData', encryptData(userData)); - set({ isLoggedIn: true, userData }); + isLoggedIn: false, + accessToken: null, + userData: null, + + login: (accessToken: string, userData: UserData | null) => { + set({ + isLoggedIn: true, + accessToken, + userData: userData ? userData : null, + }); }, - storeLogout: () => { - removeTokens(); - set({ isLoggedIn: false, userData: null }); + logout: () => { + set({ isLoggedIn: false, accessToken: null, userData: null }); }, }), { name: 'auth-storage', // 로컬스토리지에 저장될 이름 + storage: createJSONStorage(() => localStorage), } ) ); diff --git a/src/util/cryptoUtils.ts b/src/util/cryptoUtils.ts index 480c06cf..5e03dfbb 100644 --- a/src/util/cryptoUtils.ts +++ b/src/util/cryptoUtils.ts @@ -1,5 +1,5 @@ import CryptoJS from 'crypto-js'; -import { UserData } from '../store/authStore'; +import type { UserData } from '../models/auth'; export const encryptData = (data: UserData | undefined) => { return CryptoJS.AES.encrypt(