diff --git a/src/api/admin/customerService/Notice.api.ts b/src/api/admin/customerService/Notice.api.ts new file mode 100644 index 00000000..2455cc93 --- /dev/null +++ b/src/api/admin/customerService/Notice.api.ts @@ -0,0 +1,30 @@ +import type { ApiCommonBasicType } from '../../../models/apiCommon'; +import type { WriteBody } from '../../../models/customerService'; +import { httpClient } from '../../http.api'; + +export const postNotice = async (formData: WriteBody) => { + try { + await httpClient.post(`/notice`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putNotice = async (id: string, formData: WriteBody) => { + try { + await httpClient.put(`/notice/${id}`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deleteNotice = async (id: string) => { + try { + await httpClient.delete(`/notice/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/components/admin/searchBar/SearchBar.styled.ts b/src/components/admin/searchBar/SearchBar.styled.ts new file mode 100644 index 00000000..9c432908 --- /dev/null +++ b/src/components/admin/searchBar/SearchBar.styled.ts @@ -0,0 +1,62 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +export const AdminSearchBarContainer = styled.form` + width: 100%; + display: flex; + justify-content: space-evenly; + margin-bottom: 2rem; +`; + +export const AdminSearchBarWrapper = styled.div` + display: flex; + width: 60%; +`; + +export const AdminSearchBarInputWrapper = styled.div` + display: flex; + width: 100%; + justify-content: space-between; + padding: 0.5rem 0.5rem 0.5rem 1rem; + border: 1px solid ${({ theme }) => theme.color.deepGrey}; + border-radius: ${({ theme }) => theme.borderRadius.large} 0 0 + ${({ theme }) => theme.borderRadius.large}; +`; + +export const AdminSearchBarInput = styled.input` + width: 100%; + font-size: 1.3rem; +`; + +export const AdminSearchBarBackIcon = styled.button` + svg { + width: 1.5rem; + } +`; + +export const AdminSearchBarButton = styled.button` + width: 15%; + min-width: 5rem; + border: 1px solid ${({ theme }) => theme.color.navy}; + background: ${({ theme }) => theme.color.navy}; + border-radius: 0 ${({ theme }) => theme.borderRadius.large} + ${({ theme }) => theme.borderRadius.large} 0; + font-size: 1.3rem; + color: ${({ theme }) => theme.color.white}; + padding: 0.5rem 1rem 0.5rem 0.5rem; +`; + +export const WriteLink = styled(Link)` + border: 1px solid ${({ theme }) => theme.color.navy}; + background: ${({ theme }) => theme.color.navy}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + font-size: 1rem; + color: ${({ theme }) => theme.color.white}; + padding: 0.5rem 1rem; + transition: all 300ms ease-in-out; + + &:hover { + background: ${({ theme }) => theme.color.white}; + color: ${({ theme }) => theme.color.navy}; + } +`; diff --git a/src/components/admin/searchBar/SearchBar.tsx b/src/components/admin/searchBar/SearchBar.tsx new file mode 100644 index 00000000..cd5babc2 --- /dev/null +++ b/src/components/admin/searchBar/SearchBar.tsx @@ -0,0 +1,84 @@ +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../constants/user/customerService'; +import * as S from './SearchBar.styled'; +import { useState } from 'react'; +import { useLocation, useSearchParams } from 'react-router-dom'; +import { useModal } from '../../../hooks/useModal'; +import Modal from '../../common/modal/Modal'; +import { ADMIN_ROUTE } from '../../../constants/routes'; + +interface SearchBarProps { + onGetKeyword: (keyword: string) => void; + value: string; +} + +export default function SearchBar({ onGetKeyword, value }: SearchBarProps) { + const [inputValue, setInputValue] = useState(''); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const [searchParams, setSearchParams] = useSearchParams(); + const location = useLocation(); + const keyword = inputValue ? inputValue : value; + + const handleKeyword = (inputValue: string) => { + const newSearchParams = new URLSearchParams(searchParams); + if (inputValue === '') { + newSearchParams.delete('keyword'); + } else { + newSearchParams.set('keyword', inputValue); + } + setSearchParams(newSearchParams); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (inputValue.trim() === '') { + return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword); + } else { + onGetKeyword(inputValue); + handleKeyword(inputValue); + return; + } + }; + + const handleChangeKeyword = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + }; + + const handleClickSearchDefault = () => { + setInputValue(''); + onGetKeyword(''); + handleKeyword(''); + }; + + return ( + + + + + {keyword && ( + + + + )} + + 검색 + + + 작성하기 + + + {message} + + + ); +} diff --git a/src/components/common/admin/sidebar/AdminSidebar.styled.ts b/src/components/common/admin/sidebar/AdminSidebar.styled.ts index bdbea3d5..726be486 100644 --- a/src/components/common/admin/sidebar/AdminSidebar.styled.ts +++ b/src/components/common/admin/sidebar/AdminSidebar.styled.ts @@ -1,6 +1,7 @@ import styled from 'styled-components'; export const LayoutContainer = styled.div` + max-width: 1440px; height: 100vh; display: flex; `; diff --git a/src/components/common/admin/title/AdminTitle.styled.ts b/src/components/common/admin/title/AdminTitle.styled.ts index 6a12e940..fcaa1c05 100644 --- a/src/components/common/admin/title/AdminTitle.styled.ts +++ b/src/components/common/admin/title/AdminTitle.styled.ts @@ -1,6 +1,8 @@ import styled from 'styled-components'; -export const TitleContainer = styled.header``; +export const TitleContainer = styled.header` + margin-bottom: 1rem; +`; export const TitleWrapper = styled.div` margin-bottom: 2rem; diff --git a/src/components/common/dropDown/DropDown.tsx b/src/components/common/dropDown/DropDown.tsx index 32c40a6e..2293abc4 100644 --- a/src/components/common/dropDown/DropDown.tsx +++ b/src/components/common/dropDown/DropDown.tsx @@ -7,14 +7,14 @@ interface DropDownProps { children: React.ReactNode; toggleButton: React.ReactNode; isOpen?: boolean; - comment: boolean; + comment?: boolean; } const DropDown = ({ children, toggleButton, isOpen = false, - comment, + comment = false, ...props }: DropDownProps) => { const [open, setOpen] = useState(isOpen); diff --git a/src/components/user/customerService/CustomerServiceHeader.tsx b/src/components/user/customerService/CustomerServiceHeader.tsx index 59e79f47..2105d4b7 100644 --- a/src/components/user/customerService/CustomerServiceHeader.tsx +++ b/src/components/user/customerService/CustomerServiceHeader.tsx @@ -15,12 +15,13 @@ interface CustomerServiceHeaderProps { export default function CustomerServiceHeader({ title, - keyword, + keyword: value, onGetKeyword, }: CustomerServiceHeaderProps) { const [inputValue, setInputValue] = useState(''); const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); const [searchParams, setSearchParams] = useSearchParams(); + const keyword = value ? value : inputValue; const handleKeyword = (inputValue: string) => { const newSearchParams = new URLSearchParams(searchParams); @@ -64,7 +65,7 @@ export default function CustomerServiceHeader({ diff --git a/src/components/user/customerService/noticeDetail/NoticeDetailBundle.styled.ts b/src/components/user/customerService/noticeDetail/NoticeDetailBundle.styled.ts index c9b9e6aa..c02c8f6c 100644 --- a/src/components/user/customerService/noticeDetail/NoticeDetailBundle.styled.ts +++ b/src/components/user/customerService/noticeDetail/NoticeDetailBundle.styled.ts @@ -3,8 +3,8 @@ import { SpinnerWrapperStyled } from '../../mypage/Spinner.styled'; export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; -export const Container = styled.section` - width: 75%; +export const Container = styled.section<{ $width: string }>` + width: ${({ $width }) => $width}; margin: 0 auto; margin-bottom: 2rem; `; diff --git a/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx b/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx index f9f6dd0a..f8f8abc6 100644 --- a/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx +++ b/src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx @@ -7,11 +7,18 @@ import NoticeDetailHeader from './header/NoticeDetailHeader'; import Spinner from '../../mypage/Spinner'; import ListButton from './bottom/button/ListButton'; -export default function NoticeDetailBundle() { +interface NoticeDetailBundleProps { + $width: string; +} + +export default function NoticeDetailBundle({ + $width, +}: NoticeDetailBundleProps) { const location = useLocation(); const { noticeId } = useParams(); const id = noticeId || String(location.state.id); const keyword = location.state?.keyword ?? ''; + const includesAdmin = location.pathname.includes('admin') ?? false; const { noticeDetail: noticeDetailData, isLoading } = useGetNoticeDetail(id); @@ -25,7 +32,7 @@ export default function NoticeDetailBundle() { if (!noticeDetailData) { return ( - + - + + {!includesAdmin && } 목록 diff --git a/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.styled.ts b/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.styled.ts index 25ac54d6..70e12b74 100644 --- a/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.styled.ts +++ b/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.styled.ts @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import styled from 'styled-components'; export const Container = styled.div` @@ -33,6 +34,52 @@ export const InfoWrapper = styled.div` gap: 1rem; `; +export const AdminAuthWrapper = styled.div` + display: flex; + justify-content: space-between; +`; + +export const AdminAuthButton = styled.button` + height: fit-content; + display: flex; + justify-content: center; + align-items: center; + + svg { + width: 1rem; + height: 1rem; + } +`; + +export const AdminDropdownWrapper = styled.div` + position: relative; +`; + +export const AdminLinkWrapper = styled.nav` + position: absolute; + top: -1rem; + left: -5.5rem; + width: 5.5rem; + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.color.white}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + overflow: hidden; +`; + +export const AdminLink = styled(Link)` + width: 100%; + padding: 0.5rem; + display: flex; + justify-content: center; + + &:hover { + background: ${({ theme }) => theme.color.lightgrey}; + color: ${({ theme }) => theme.color.deepGrey}; + } +`; + export const NoticeContentDate = styled.span` font-size: 0.8rem; `; diff --git a/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.tsx b/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.tsx index d85e94a2..66a375f0 100644 --- a/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.tsx +++ b/src/components/user/customerService/noticeDetail/content/NoticeDetailContent.tsx @@ -1,8 +1,15 @@ -import { EyeIcon } from '@heroicons/react/24/outline'; +import { EllipsisVerticalIcon, EyeIcon } from '@heroicons/react/24/outline'; import { formatDate } from '../../../../../util/format'; import * as S from './NoticeDetailContent.styled'; import logo from '../../../../../assets/mainlogo.svg'; import ContentBorder from '../../../../common/contentBorder/ContentBorder'; +import useAuthStore from '../../../../../store/authStore'; +import DropDown from '../../../../common/dropDown/DropDown'; +import { ADMIN_ROUTE } from '../../../../../constants/routes'; +import { useLocation, useParams } from 'react-router-dom'; +import { useAdminNotice } from '../../../../../hooks/admin/useAdminNotice'; +import Modal from '../../../../common/modal/Modal'; +import { useModal } from '../../../../../hooks/useModal'; interface NoticeDetailContentProps { id: number; @@ -19,6 +26,20 @@ export default function NoticeDetailContent({ createdAt, viewCount, }: NoticeDetailContentProps) { + const isAdmin = useAuthStore((state) => state.userData?.admin) ?? false; + const { noticeId } = useParams() ?? ''; + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const location = useLocation(); + const pathname = `${ADMIN_ROUTE.admin}/${ADMIN_ROUTE.notice}`; + const { deleteNoticeMutate } = useAdminNotice({ handleModalOpen, pathname }); + + const handleClickDeleteNotice = (e: React.MouseEvent) => { + e.preventDefault(); + if (noticeId) { + deleteNoticeMutate.mutate(noticeId); + } + }; + return ( @@ -27,22 +48,50 @@ export default function NoticeDetailContent({ DevPals - {Boolean(id) && ( - - {formatDate(createdAt)} - - - - - {viewCount} - - - )} + + {Boolean(id) && ( + + {formatDate(createdAt)} + + + + + {viewCount} + + + )} + {isAdmin && ( + + + + + } + > + + + 수정하기 + + + 삭제하기 + + + + + )} + {content} + + {message} + ); } diff --git a/src/constants/admin/adminModal.ts b/src/constants/admin/adminModal.ts new file mode 100644 index 00000000..a4f8082f --- /dev/null +++ b/src/constants/admin/adminModal.ts @@ -0,0 +1,7 @@ +export const ADMIN_MODAL_MESSAGE = { + writeSuccess: '성공적으로 작성되었습니다.', + writeFail: '작성이 실패하였습니다.', + writeDeleteSuccess: '삭제되었습니다.', + writeDeleteFail: '삭제가 실패하였습니다.', + writeError: '알수없는 에러가 발생했습니다.', +}; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index fe64fdfd..f65de3b6 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -41,4 +41,7 @@ export const ADMIN_ROUTE = { reports: 'reports', inquiries: 'inquiries', manage: 'manage', + detail: 'detail', + write: 'write', + modification: 'modification', }; diff --git a/src/hooks/admin/useAdminNotice.ts b/src/hooks/admin/useAdminNotice.ts new file mode 100644 index 00000000..fb352eb2 --- /dev/null +++ b/src/hooks/admin/useAdminNotice.ts @@ -0,0 +1,102 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + deleteNotice, + postNotice, + putNotice, +} from '../../api/admin/customerService/Notice.api'; +import { AxiosError } from 'axios'; +import { CustomerService } from '../queries/user/keys'; +import { useNavigate } from 'react-router-dom'; +import { ADMIN_MODAL_MESSAGE } from '../../constants/admin/adminModal'; +import { WriteBody } from '../../models/customerService'; + +type State = 'success' | 'fail'; + +export const useAdminNotice = ({ + handleModalOpen, + formDefault, + pathname, +}: { + handleModalOpen: (message: string) => void; + formDefault?: () => void; + pathname: string; +}) => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + + const handleButtonState = (state: State, isDeleteApi: boolean = false) => { + switch (state) { + case 'success': + if (!isDeleteApi) { + handleModalOpen(ADMIN_MODAL_MESSAGE.writeSuccess); + formDefault?.(); + } else { + handleModalOpen(ADMIN_MODAL_MESSAGE.writeDeleteSuccess); + } + setTimeout(() => { + if (pathname) { + return navigate(pathname); + } else { + return navigate(-1); + } + }, 1000); + break; + case 'fail': + if (!isDeleteApi) { + handleModalOpen(ADMIN_MODAL_MESSAGE.writeFail); + } else { + handleModalOpen(ADMIN_MODAL_MESSAGE.writeDeleteFail); + } + break; + default: + handleModalOpen(ADMIN_MODAL_MESSAGE.writeError); + break; + } + }; + + const postNoticeMutate = useMutation({ + mutationFn: (formData) => postNotice(formData), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CustomerService.notice], + }); + handleButtonState('success'); + }, + onError: () => { + handleButtonState('fail'); + }, + }); + + const putNoticeMutate = useMutation< + void, + AxiosError, + { id: string; formDataObj: WriteBody } + >({ + mutationFn: ({ id, formDataObj }: { id: string; formDataObj: WriteBody }) => + putNotice(id, formDataObj), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CustomerService.notice], + }); + handleButtonState('success'); + }, + onError: () => { + handleButtonState('fail'); + }, + }); + + const deleteNoticeMutate = useMutation({ + mutationFn: (id: string) => deleteNotice(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CustomerService.notice], + }); + handleButtonState('success', true); + }, + onError: () => { + handleButtonState('fail', true); + }, + }); + + return { postNoticeMutate, putNoticeMutate, deleteNoticeMutate }; +}; diff --git a/src/hooks/user/useGetNoticeDetail.ts b/src/hooks/user/useGetNoticeDetail.ts index 5b662c2d..47193b7c 100644 --- a/src/hooks/user/useGetNoticeDetail.ts +++ b/src/hooks/user/useGetNoticeDetail.ts @@ -6,7 +6,12 @@ export const useGetNoticeDetail = (id: string) => { const { data: noticeDetailData, isLoading } = useQuery({ queryKey: [CustomerService.noticeDetail, id], queryFn: () => getNoticeDetail(id), + enabled: !!id, }); + if (!id) { + return { noticeDetail: undefined, isLoading: false }; + } + return { noticeDetail: noticeDetailData, isLoading }; }; diff --git a/src/hooks/user/useMyInfo.ts b/src/hooks/user/useMyInfo.ts index c0ebecbc..a73ec422 100644 --- a/src/hooks/user/useMyInfo.ts +++ b/src/hooks/user/useMyInfo.ts @@ -24,8 +24,10 @@ export const useMyProfileInfo = () => { const { data, isLoading } = useQuery({ queryKey: myInfoKey.myProfile, queryFn: () => getMyInfo(), - staleTime: Infinity, - gcTime: Infinity, + staleTime: 1000 * 60 * 30, + gcTime: 1000 * 60 * 60, + refetchInterval: 1000 * 60 * 60, + refetchIntervalInBackground: false, enabled: isLoggedIn, }); diff --git a/src/main.tsx b/src/main.tsx index 32585982..fc6a5ab3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,10 +2,10 @@ import { createRoot } from 'react-dom/client'; import App from './App.tsx'; async function mountApp() { - if (process.env.NODE_ENV === 'development') { - const { worker } = await import('./mock/browser'); - await worker.start(); - } + // if (process.env.NODE_ENV === 'development') { + // const { worker } = await import('./mock/browser'); + // await worker.start(); + // } createRoot(document.getElementById('root')!).render( <> diff --git a/src/models/customerService.ts b/src/models/customerService.ts index 7d058a0a..6f5cf87e 100644 --- a/src/models/customerService.ts +++ b/src/models/customerService.ts @@ -46,3 +46,9 @@ export interface SearchKeyword { export interface NoticeSearch extends SearchKeyword { page: number; } + +// admin +export interface WriteBody { + title: string; + content: string; +} diff --git a/src/pages/admin/adminMain/AdminMain.tsx b/src/pages/admin/adminMain/AdminMain.tsx index 85afe1dc..fd043238 100644 --- a/src/pages/admin/adminMain/AdminMain.tsx +++ b/src/pages/admin/adminMain/AdminMain.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import * as S from './AdminMain.styled'; import MainCard from '../../../components/admin/mainCard/MainCard'; import { cardList } from '../../../constants/admin/mainItems'; diff --git a/src/pages/admin/adminNotice/AdminNotice.styled.ts b/src/pages/admin/adminNotice/AdminNotice.styled.ts index c3389834..1bbefc66 100644 --- a/src/pages/admin/adminNotice/AdminNotice.styled.ts +++ b/src/pages/admin/adminNotice/AdminNotice.styled.ts @@ -1,3 +1,11 @@ import styled from 'styled-components'; +import { SpinnerWrapperStyled } from '../../../components/user/mypage/Spinner.styled'; -export const Container = styled.div``; +export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; + +export const AdminNoticeContainer = styled.div``; + +export const NoticeItemWrapper = styled.section` + display: flex; + justify-content: center; +`; diff --git a/src/pages/admin/adminNotice/AdminNotice.tsx b/src/pages/admin/adminNotice/AdminNotice.tsx index 9a760a49..a3639e8c 100644 --- a/src/pages/admin/adminNotice/AdminNotice.tsx +++ b/src/pages/admin/adminNotice/AdminNotice.tsx @@ -1,12 +1,12 @@ +import { Outlet } from 'react-router-dom'; import AdminTitle from '../../../components/common/admin/title/AdminTitle'; import * as S from './AdminNotice.styled'; export default function AdminNotice() { - console.log('공지사항 렌더'); - return ( - + - + + ); } diff --git a/src/pages/admin/adminNotice/adminNoticeList/AdminNoticeList.styled.ts b/src/pages/admin/adminNotice/adminNoticeList/AdminNoticeList.styled.ts new file mode 100644 index 00000000..c0db96bb --- /dev/null +++ b/src/pages/admin/adminNotice/adminNoticeList/AdminNoticeList.styled.ts @@ -0,0 +1,9 @@ +import styled from 'styled-components'; +import { SpinnerWrapperStyled } from '../../../../components/user/mypage/Spinner.styled'; + +export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; + +export const NoticeItemWrapper = styled.section` + display: flex; + justify-content: center; +`; diff --git a/src/pages/admin/adminNotice/adminNoticeList/AdminNoticeList.tsx b/src/pages/admin/adminNotice/adminNoticeList/AdminNoticeList.tsx new file mode 100644 index 00000000..62378d87 --- /dev/null +++ b/src/pages/admin/adminNotice/adminNoticeList/AdminNoticeList.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; +import SearchBar from '../../../../components/admin/searchBar/SearchBar'; +import NoticeItem from '../../../user/customerService/notice/noticeItem/NoticeItem'; +import * as S from './AdminNoticeList.styled'; +import type { NoticeSearch } from '../../../../models/customerService'; +import { useGetNotice } from '../../../../hooks/user/useGetNotice'; +import { useSearchParams } from 'react-router-dom'; +import { Spinner } from '../../../../components/common/loadingSpinner/LoadingSpinner.styled'; +import Pagination from '../../../../components/common/pagination/Pagination'; + +export default function AdminNoticeList() { + const [noticeSearch, setNoticeSearch] = useState({ + keyword: '', + page: 1, + }); + const [value, setValue] = useState(''); + const { noticeData, isLoading } = useGetNotice(noticeSearch); + const [searchParams] = useSearchParams(); + + useEffect(() => { + const searchKeyword = searchParams.get('keyword'); + + if (searchKeyword) { + setNoticeSearch((prev) => ({ ...prev, keyword: searchKeyword })); + setValue((prev) => (searchKeyword ? searchKeyword : prev)); + } + }, [searchParams]); + + const handleGetKeyword = (keyword: string) => { + setNoticeSearch((prev) => ({ ...prev, keyword })); + setValue(keyword); + }; + const handleChangePagination = (page: number) => { + setNoticeSearch((prev) => ({ ...prev, page })); + }; + + if (isLoading) { + return ( + + + + ); + } + + if (!noticeData) return; + + const lastPage = Number(noticeData.totalPages); + + return ( + <> + + + + + + + ); +} diff --git a/src/pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite.styled.ts b/src/pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite.styled.ts new file mode 100644 index 00000000..96d78368 --- /dev/null +++ b/src/pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite.styled.ts @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import { + Container, + InputInquiryTitle, + InquiryForm, + InquiryWrapper, + Nav, + ContentWrapper, + Content, + SendButtonWrapper, + SendButton, +} from '../../../../components/user/customerService/inquiry/Inquiry.styled'; +import { SpinnerWrapperStyled } from '../../../../components/user/mypage/Spinner.styled'; + +export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; + +export const AdminNoticeContainer = styled(Container)``; + +export const AdminNoticeForm = styled(InquiryForm)``; + +export const AdminNoticeWrapper = styled(InquiryWrapper)` + width: 90%; +`; + +export const AdminNoticeNav = styled(Nav)``; + +export const AdminNoticeInputTitle = styled(InputInquiryTitle)` + width: 100%; +`; + +export const AdminNoticeContentWrapper = styled(ContentWrapper)``; + +export const AdminNoticeContent = styled(Content)` + height: 60vh; +`; + +export const AdminNoticeSendButtonWrapper = styled(SendButtonWrapper)``; + +export const AdminNoticeSendButton = styled(SendButton)``; diff --git a/src/pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite.tsx b/src/pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite.tsx new file mode 100644 index 00000000..7c8789df --- /dev/null +++ b/src/pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite.tsx @@ -0,0 +1,125 @@ +import { INQUIRY_MESSAGE } from '../../../../constants/user/customerService'; +import * as S from './AdminNoticeWrite.styled'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { useModal } from '../../../../hooks/useModal'; +import Modal from '../../../../components/common/modal/Modal'; +import type { WriteBody } from '../../../../models/customerService'; +import { useAdminNotice } from '../../../../hooks/admin/useAdminNotice'; +import { useGetNoticeDetail } from '../../../../hooks/user/useGetNoticeDetail'; +import Spinner from '../../../../components/user/mypage/Spinner'; + +export default function AdminNoticeWrite() { + const location = useLocation(); + const { + isOpen: isModalOpen, + message, + handleModalOpen, + handleModalClose, + } = useModal(); + const { noticeId } = useParams(); + const id = noticeId ? noticeId : ''; + const { noticeDetail, isLoading } = useGetNoticeDetail(id); + const pathname = location.state.from || ''; + + const formDefault = () => { + setForm({ + title: '', + content: '', + }); + }; + + const { postNoticeMutate, putNoticeMutate } = useAdminNotice({ + handleModalOpen, + formDefault, + pathname, + }); + const [form, setForm] = useState({ + title: '', + content: '', + }); + + useEffect(() => { + if (!noticeDetail) return; + setForm({ title: noticeDetail.title, content: noticeDetail.content }); + }, [noticeDetail]); + + const handleSubmitInquiry = (e: React.FormEvent) => { + e.preventDefault(); + + const isValid = { + title: form.title.trim() !== '', + content: form.content.trim() !== '', + }; + + if (!isValid.title) { + return handleModalOpen(INQUIRY_MESSAGE.writeTitle); + } + if (!isValid.content) { + return handleModalOpen(INQUIRY_MESSAGE.writeContent); + } + + const formData = new FormData(e.currentTarget as HTMLFormElement); + + const formDataObj: WriteBody = { + title: formData.get('title') as string, + content: formData.get('content') as string, + }; + + if (!id) { + return postNoticeMutate.mutate(formDataObj); + } else { + return putNoticeMutate.mutate({ id, formDataObj }); + } + }; + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + + + + setForm((prev) => ({ ...prev, title: e.target.value })) + } + /> + + + + setForm((prev) => ({ ...prev, content: e.target.value })) + } + > + + + + 제출 + + + + + + {message} + + + ); +} diff --git a/src/pages/admin/adminNoticeDetail/AdminNoticeDetail.styled.ts b/src/pages/admin/adminNoticeDetail/AdminNoticeDetail.styled.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/admin/adminNoticeDetail/AdminNoticeDetail.tsx b/src/pages/admin/adminNoticeDetail/AdminNoticeDetail.tsx new file mode 100644 index 00000000..f61dc2b2 --- /dev/null +++ b/src/pages/admin/adminNoticeDetail/AdminNoticeDetail.tsx @@ -0,0 +1,5 @@ +import NoticeDetailBundle from '../../../components/user/customerService/noticeDetail/NoticeDetailBundle'; + +export default function AdminNoticeDetail() { + return ; +} diff --git a/src/pages/user/customerService/notice/Notice.tsx b/src/pages/user/customerService/notice/Notice.tsx index d52184aa..e78bf6cc 100644 --- a/src/pages/user/customerService/notice/Notice.tsx +++ b/src/pages/user/customerService/notice/Notice.tsx @@ -4,12 +4,9 @@ import type { NoticeSearch } from '../../../../models/customerService'; import { useGetNotice } from '../../../../hooks/user/useGetNotice'; import { Spinner } from '../../../../components/common/loadingSpinner/LoadingSpinner.styled'; import CustomerServiceHeader from '../../../../components/user/customerService/CustomerServiceHeader'; -import ContentBorder from '../../../../components/common/contentBorder/ContentBorder'; -import { ROUTES } from '../../../../constants/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'; +import { useSearchParams } from 'react-router-dom'; +import NoticeItem from './noticeItem/NoticeItem'; export default function Notice() { const [noticeSearch, setNoticeSearch] = useState({ @@ -18,17 +15,16 @@ export default function Notice() { }); const [value, setValue] = useState(''); const { noticeData, isLoading } = useGetNotice(noticeSearch); - const location = useLocation(); - const hasKeyword = location.search - ? decodeURI(location.search.split('=')[1]) - : ''; + const [searchParams] = useSearchParams(); useEffect(() => { - if (hasKeyword) { - setNoticeSearch((prev) => ({ ...prev, keyword: hasKeyword })); - setValue(hasKeyword); + const searchKeyword = searchParams.get('keyword'); + + if (searchKeyword) { + setNoticeSearch((prev) => ({ ...prev, keyword: searchKeyword })); + setValue((prev) => (searchKeyword ? searchKeyword : prev)); } - }, [hasKeyword]); + }, [searchParams]); const handleGetKeyword = (keyword: string) => { setNoticeSearch((prev) => ({ ...prev, keyword })); @@ -58,23 +54,11 @@ export default function Notice() { onGetKeyword={handleGetKeyword} /> - - {noticeData.notices.length > 0 && } - {noticeData.notices.length > 0 ? ( - noticeData.notices.map((list) => ( - - - - - )) - ) : ( - - )} - + ` + width: ${({ $width }) => $width}; + display: flex; + flex-direction: column; +`; + +export const NoticeDetailLink = styled(Link)``; diff --git a/src/pages/user/customerService/notice/noticeItem/NoticeItem.tsx b/src/pages/user/customerService/notice/noticeItem/NoticeItem.tsx new file mode 100644 index 00000000..1806031a --- /dev/null +++ b/src/pages/user/customerService/notice/noticeItem/NoticeItem.tsx @@ -0,0 +1,46 @@ +import * as S from './NoticeItem.styled'; +import ContentBorder from '../../../../../components/common/contentBorder/ContentBorder'; +import { ADMIN_ROUTE, ROUTES } from '../../../../../constants/routes'; +import NoticeList from '../../../../../components/user/customerService/notice/NoticeList'; +import NoResult from '../../../../../components/common/noResult/NoResult'; +import type { NoticeList as TNoticeList } from '../../../../../models/customerService'; +import { useLocation } from 'react-router-dom'; + +interface NoticeItemProps { + noticeData: TNoticeList[]; + value: string; + $width: string; +} + +export default function NoticeItem({ + noticeData, + value, + $width = '75%', +}: NoticeItemProps) { + const location = useLocation(); + const includesAdmin = location.pathname.includes('admin'); + + return ( + + {noticeData.length > 0 && } + {noticeData.length > 0 ? ( + noticeData.map((list) => ( + + + + + )) + ) : ( + + )} + + ); +} diff --git a/src/pages/user/customerService/noticeDetail/NoticeDetail.tsx b/src/pages/user/customerService/noticeDetail/NoticeDetail.tsx index 8cea59fe..7acc5121 100644 --- a/src/pages/user/customerService/noticeDetail/NoticeDetail.tsx +++ b/src/pages/user/customerService/noticeDetail/NoticeDetail.tsx @@ -1,5 +1,5 @@ import NoticeDetailBundle from '../../../../components/user/customerService/noticeDetail/NoticeDetailBundle'; export default function NoticeDetail() { - return ; + return ; } diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index 29431060..539ce3cd 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -8,6 +8,15 @@ const Sidebar = lazy( ); const Main = lazy(() => import('../pages/admin/adminMain/AdminMain')); const Notice = lazy(() => import('../pages/admin/adminNotice/AdminNotice')); +const NoticeList = lazy( + () => import('../pages/admin/adminNotice/adminNoticeList/AdminNoticeList') +); +const NoticeWrite = lazy( + () => import('../pages/admin/adminNotice/adminNoticeWrite/AdminNoticeWrite') +); +const NoticeDetail = lazy( + () => import('../pages/admin/adminNoticeDetail/AdminNoticeDetail') +); const Banner = lazy(() => import('../pages/admin/adminBanner/AdminBanner')); const Tags = lazy(() => import('../pages/admin/adminTags/AdminTags')); const AllUser = lazy(() => import('../pages/admin/adminAllUser/AdminAllUser')); @@ -27,10 +36,28 @@ export const AdminRoutes = () => { ), children: [ - { index: true, element:
}, + { + index: true, + element: , + }, { path: ADMIN_ROUTE.notice, element: , + children: [ + { index: true, element: }, + { + path: ADMIN_ROUTE.write, + element: , + }, + { + path: `${ADMIN_ROUTE.detail}/:noticeId`, + element: , + }, + { + path: `${ADMIN_ROUTE.detail}/:noticeId/${ADMIN_ROUTE.modification}`, + element: , + }, + ], }, { path: ADMIN_ROUTE.banner, diff --git a/src/routes/MergeRoutes.tsx b/src/routes/MergeRoutes.tsx index 421c2895..48f0f910 100644 --- a/src/routes/MergeRoutes.tsx +++ b/src/routes/MergeRoutes.tsx @@ -3,7 +3,6 @@ import AdminRoutes from './AdminRoutes'; import AppRoutes from './AppRoutes'; import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import { ToastProvider } from '../components/common/Toast/ToastProvider'; -import ProtectAdminRoute from './ProtectAdminRoute'; import { NotificationProvider } from '../components/user/notificationLive/NotificationProvider'; import NotificationInitializer from '../components/user/notificationLive/NotificationInitializer'; @@ -21,11 +20,7 @@ export default function MergeRoutes() { children: [...AppRoutes()], }, { - element: ( - - - - ), + element: , children: [...AdminRoutes()], }, { path: '*', element: }, diff --git a/src/routes/ProtectAdminRoute.tsx b/src/routes/ProtectAdminRoute.tsx index cfe01688..660c2e69 100644 --- a/src/routes/ProtectAdminRoute.tsx +++ b/src/routes/ProtectAdminRoute.tsx @@ -5,6 +5,7 @@ import { ReactNode, useEffect } from 'react'; import { useModal } from '../hooks/useModal'; import Modal from '../components/common/modal/Modal'; import { MODAL_MESSAGE } from '../constants/user/modalMessage'; +import { useMyProfileInfo } from '../hooks/user/useMyInfo'; interface ProtectAdminRouteProps { children: ReactNode; @@ -21,13 +22,34 @@ export default function ProtectAdminRoute({ useAuthStore((state) => state.redirectAdmin) || false; const navigate = useNavigate(); const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const { myData, isLoading } = useMyProfileInfo(); useEffect(() => { + let timer: NodeJS.Timeout; + + const handleStorageChange = () => { + const authStorage = localStorage.getItem('auth-storage'); + if (!authStorage || (!myData && !isLoading)) { + handleModalOpen(MODAL_MESSAGE.needAuth); + timer = setTimeout(() => { + logout(); + navigate(ROUTES.main); + }, 1000); + } + }; + + if (!isLoggedIn) { + handleModalOpen(MODAL_MESSAGE.needAuth); + timer = setTimeout(() => { + navigate(ROUTES.main); + }, 1000); + return; + } if (isLoggedIn && !isAdmin) { handleModalOpen(MODAL_MESSAGE.needAuth); - setTimeout(() => { + timer = setTimeout(() => { navigate(ROUTES.main); - }, 200); + }, 1000); return; } if (isLoggedIn && isAdmin && !redirectAdminBool) { @@ -35,7 +57,15 @@ export default function ProtectAdminRoute({ replace(); return; } + + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + if (timer) clearTimeout(timer); + }; }, [ + myData, redirectAdminBool, isLoggedIn, isAdmin,