diff --git a/src/api/admin/customerService/FAQ.api.ts b/src/api/admin/customerService/FAQ.api.ts new file mode 100644 index 00000000..85bf8995 --- /dev/null +++ b/src/api/admin/customerService/FAQ.api.ts @@ -0,0 +1,47 @@ +import { ApiCommonBasicType } from '../../../models/apiCommon'; +import type { ApiFAQDetail, WriteBody } from '../../../models/customerService'; +import { httpClient } from '../../http.api'; + +export const getFAQDetail = async (id: string) => { + try { + const response = await httpClient.get(`/faq/${id}`); + + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const postFAQ = async (formData: WriteBody) => { + try { + await httpClient.post(`faq`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putFAQ = async ({ + id, + formData, +}: { + id: string; + formData: WriteBody; +}) => { + try { + await httpClient.put(`faq/${id}`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deleteFAQ = async (id: string) => { + try { + await httpClient.delete(`faq/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/api/auth.api.ts b/src/api/auth.api.ts index 6a236128..06639a73 100644 --- a/src/api/auth.api.ts +++ b/src/api/auth.api.ts @@ -1,15 +1,15 @@ -import { +import type { ApiGetAllUsers, ApiGetAllUsersPreview, - type ApiOauth, - type ApiVerifyNickname, - type VerifyEmail, + ApiOauth, + ApiVerifyNickname, + VerifyEmail, } from '../models/auth'; import { httpClient } from './http.api'; import { loginFormValues } from '../pages/login/Login'; import { registerFormValues } from '../pages/user/register/Register'; import { changePasswordFormValues } from '../pages/user/changePassword/ChangePassword'; -import { type SearchType } from '../models/search'; +import type { SearchType } from '../models/search'; export const postVerificationEmail = async (email: string) => { try { diff --git a/src/components/admin/adminFAQ/AdminFAQList.styled.ts b/src/components/admin/adminFAQ/AdminFAQList.styled.ts new file mode 100644 index 00000000..307f52df --- /dev/null +++ b/src/components/admin/adminFAQ/AdminFAQList.styled.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components'; +import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled'; +import { SearchBarFixedWrapperStyled } from '../../common/admin/searchBar/SearchBar.styled'; +import { GAP_HEIGHT } from '../../../constants/admin/adminGap'; + +export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; + +export const SearchBarFixedWrapper = styled(SearchBarFixedWrapperStyled)``; + +export const FAQItemWrapper = styled.div` + margin-top: calc(${GAP_HEIGHT.headerTitleTop} + ${GAP_HEIGHT.sectionTop}); +`; diff --git a/src/components/admin/adminFAQ/AdminFAQList.tsx b/src/components/admin/adminFAQ/AdminFAQList.tsx new file mode 100644 index 00000000..5634b9d8 --- /dev/null +++ b/src/components/admin/adminFAQ/AdminFAQList.tsx @@ -0,0 +1,33 @@ +import * as S from './AdminFAQList.styled'; +import SearchBar from '../../../components/common/admin/searchBar/SearchBar'; +import FAQItem from '../../user/customerService/faq/FAQItem'; +import { useGetFAQ } from '../../../hooks/user/useGetFAQ'; +import Spinner from '../../user/mypage/Spinner'; +import useSearchBar from '../../../hooks/admin/useSearchBar'; + +export default function AdminFAQList() { + const { searchUnit, value, handleGetKeyword } = useSearchBar(); + const keyword = searchUnit.keyword; + const { faqData, isLoading } = useGetFAQ({ keyword }); + + if (isLoading) { + return ( + + + + ); + } + + if (!faqData) return; + + return ( + <> + + + + + + + + ); +} diff --git a/src/components/admin/adminFAQ/AdminFAQWrite.tsx b/src/components/admin/adminFAQ/AdminFAQWrite.tsx new file mode 100644 index 00000000..1b0ad550 --- /dev/null +++ b/src/components/admin/adminFAQ/AdminFAQWrite.tsx @@ -0,0 +1,124 @@ +import { INQUIRY_MESSAGE } from '../../../constants/user/customerService'; +import * as S from './../../admin/adminNotice/AdminNoticeWrite.styled'; +import React, { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useModal } from '../../../hooks/useModal'; +import Modal from '../../../components/common/modal/Modal'; +import type { WriteBody } from '../../../models/customerService'; +import Spinner from '../../../components/user/mypage/Spinner'; +import { useAdminFAQ } from '../../../hooks/admin/useAdminFAQ'; + +export default function AdminFAQWrite() { + const location = useLocation(); + const { + isOpen: isModalOpen, + message, + handleModalOpen, + handleModalClose, + } = useModal(); + const pathname = location.state?.from || ''; + const id = location.state?.id || ''; + + const formDefault = () => { + setForm({ + title: '', + content: '', + }); + }; + + const { getFAQDetailData, postFAQMutate, putFAQMutate } = useAdminFAQ({ + handleModalOpen, + formDefault, + pathname, + id, + }); + const [form, setForm] = useState({ + title: '', + content: '', + }); + const { data: FAQDetailData, isLoading } = getFAQDetailData; + + useEffect(() => { + if (!FAQDetailData) return; + setForm({ title: FAQDetailData.title, content: FAQDetailData.content }); + }, [FAQDetailData]); + + 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 postFAQMutate.mutate(formDataObj); + } else { + return putFAQMutate.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/components/admin/adminNotice/AdminNoticeList.styled.ts b/src/components/admin/adminNotice/AdminNoticeList.styled.ts index 41e66ab5..e1ff8360 100644 --- a/src/components/admin/adminNotice/AdminNoticeList.styled.ts +++ b/src/components/admin/adminNotice/AdminNoticeList.styled.ts @@ -1,11 +1,21 @@ +import { GAP_HEIGHT } from './../../../constants/admin/adminGap'; import styled from 'styled-components'; import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled'; +import { SearchBarFixedWrapperStyled } from '../../common/admin/searchBar/SearchBar.styled'; export const SpinnerWrapper = styled(SpinnerWrapperStyled)` width: 100%; `; -export const NoticeItemWrapper = styled.section` +export const SearchBarFixedWrapper = styled(SearchBarFixedWrapperStyled)``; + +export const NoticeItemContainer = styled.section` + margin-top: calc( + ${GAP_HEIGHT.headerTitleTop} + ${GAP_HEIGHT.sectionTop} + 1rem + ); +`; + +export const NoticeItemWrapper = styled.div` display: flex; justify-content: center; `; diff --git a/src/components/admin/adminNotice/AdminNoticeList.tsx b/src/components/admin/adminNotice/AdminNoticeList.tsx index acbf1ea0..fa5f91d4 100644 --- a/src/components/admin/adminNotice/AdminNoticeList.tsx +++ b/src/components/admin/adminNotice/AdminNoticeList.tsx @@ -3,8 +3,8 @@ import * as S from './AdminNoticeList.styled'; import { useGetNotice } from '../../../hooks/user/useGetNotice'; import Pagination from '../../../components/common/pagination/Pagination'; import Spinner from '../../../components/user/mypage/Spinner'; -import NoticeItem from '../../../pages/user/customerService/notice/noticeItem/NoticeItem'; import useSearchBar from '../../../hooks/admin/useSearchBar'; +import NoticeItem from '../../user/customerService/notice/noticeItem/NoticeItem'; export default function AdminNoticeList() { const { searchUnit, value, handleGetKeyword, handleChangePagination } = @@ -25,23 +25,23 @@ export default function AdminNoticeList() { return ( <> - - - + + + + + + + - - + ); } diff --git a/src/components/common/admin/searchBar/SearchBar.styled.ts b/src/components/common/admin/searchBar/SearchBar.styled.ts index 9c432908..eb20e79a 100644 --- a/src/components/common/admin/searchBar/SearchBar.styled.ts +++ b/src/components/common/admin/searchBar/SearchBar.styled.ts @@ -1,11 +1,12 @@ import { Link } from 'react-router-dom'; import styled from 'styled-components'; +import { GAP_HEIGHT } from '../../../../constants/admin/adminGap'; export const AdminSearchBarContainer = styled.form` width: 100%; display: flex; justify-content: space-evenly; - margin-bottom: 2rem; + margin-bottom: 1rem; `; export const AdminSearchBarWrapper = styled.div` @@ -60,3 +61,13 @@ export const WriteLink = styled(Link)` color: ${({ theme }) => theme.color.navy}; } `; + +export const SearchBarFixedWrapperStyled = styled.div` + max-width: calc(1440px - 19rem); + width: calc(100vw - 19rem); + position: fixed; + top: 0; + padding-top: ${GAP_HEIGHT.headerTitleTop}; + background: ${({ theme }) => theme.color.white}; + z-index: 10; +`; diff --git a/src/components/common/admin/searchBar/SearchBar.tsx b/src/components/common/admin/searchBar/SearchBar.tsx index ecd605a4..1142b7fe 100644 --- a/src/components/common/admin/searchBar/SearchBar.tsx +++ b/src/components/common/admin/searchBar/SearchBar.tsx @@ -10,13 +10,13 @@ import { ADMIN_ROUTE } from '../../../../constants/routes'; interface SearchBarProps { onGetKeyword: (value: string) => void; value: string; - isNotice?: boolean; + canWrite?: boolean; } export default function SearchBar({ onGetKeyword, value, - isNotice, + canWrite = true, }: SearchBarProps) { const [keyword, setKeyword] = useState(value); const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); @@ -75,7 +75,7 @@ export default function SearchBar({ 검색 - {isNotice && ( + {canWrite && ( 작성하기 diff --git a/src/components/common/admin/sidebar/AdminSidebar.styled.ts b/src/components/common/admin/sidebar/AdminSidebar.styled.ts index 726be486..70bd5356 100644 --- a/src/components/common/admin/sidebar/AdminSidebar.styled.ts +++ b/src/components/common/admin/sidebar/AdminSidebar.styled.ts @@ -8,10 +8,13 @@ export const LayoutContainer = styled.div` export const ContainerArea = styled.section` flex: 1; + width: 100%; padding: 2rem; + margin-left: 15rem; `; export const SidebarContainer = styled.section` + position: fixed; padding: 1rem; width: 15rem; border-right: 1px solid ${({ theme }) => theme.color.grey}; diff --git a/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx b/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx index 2544768d..19caf835 100644 --- a/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx +++ b/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx @@ -5,6 +5,7 @@ import { EnvelopeIcon, ExclamationTriangleIcon, HomeIcon, + LightBulbIcon, MegaphoneIcon, PhotoIcon, TagIcon, @@ -15,6 +16,7 @@ const iconMap = { mainPage: , movedSite: , notice: , + faq: , banner: , tags: , allUser: , diff --git a/src/components/common/admin/title/AdminTitle.styled.ts b/src/components/common/admin/title/AdminTitle.styled.ts index fcaa1c05..a2a6ef56 100644 --- a/src/components/common/admin/title/AdminTitle.styled.ts +++ b/src/components/common/admin/title/AdminTitle.styled.ts @@ -1,7 +1,12 @@ import styled from 'styled-components'; export const TitleContainer = styled.header` - margin-bottom: 1rem; + width: calc(100vw - 20rem); + position: fixed; + top: 0; + padding: 2.5rem 0 0 0; + background: ${({ theme }) => theme.color.white}; + z-index: 100; `; export const TitleWrapper = styled.div` diff --git a/src/components/user/customerService/faq/FAQContent.tsx b/src/components/user/customerService/faq/FAQContent.tsx deleted file mode 100644 index bf972785..00000000 --- a/src/components/user/customerService/faq/FAQContent.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ChevronRightIcon, PlusIcon } from '@heroicons/react/24/outline'; -import type { FAQ } from '../../../../models/customerService'; -import * as S from './FAQContent.styled'; -import { useState } from 'react'; - -interface FAQContentProps { - list: FAQ; -} - -export default function FAQContent({ list }: FAQContentProps) { - const [isFAQContentOpen, setIsFAQContentOpen] = useState(false); - - return ( - - setIsFAQContentOpen((prev) => !prev)} - > - {list.title} - - - - - - - - - {list.content} - - - ); -} diff --git a/src/components/user/customerService/faq/FAQItem.styled.ts b/src/components/user/customerService/faq/FAQItem.styled.ts new file mode 100644 index 00000000..b1fe555d --- /dev/null +++ b/src/components/user/customerService/faq/FAQItem.styled.ts @@ -0,0 +1,62 @@ +import styled from 'styled-components'; +import { SpinnerWrapperStyled } from '../../../../components/user/mypage/Spinner.styled'; + +export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; + +export const Container = styled.section` + margin-top: 2rem; + width: 100%; + display: flex; + justify-content: center; +`; + +export const Wrapper = styled.div<{ $isAdmin: boolean }>` + width: ${({ $isAdmin }) => ($isAdmin ? '90%' : '75%')}; + display: flex; + flex-direction: column; + margin-bottom: 5rem; +`; + +export const ToggleWrapper = styled.div``; + +export const ShowMoreFAQWrapper = styled.div``; + +export const ShowMoreFAQ = styled.button` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 1rem; + + svg { + width: 1rem; + } + + &:hover { + background: ${({ theme }) => theme.color.lightgrey}; + } +`; + +export const ShowMoreSpan = styled.span` + width: 100%; + padding: 1.2rem 0; + display: flex; + justify-content: center; + gap: 0.5rem; + @keyframes bounce { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-3px); + } + 100% { + transform: translateY(0); + } + } + + &:hover { + animation: bounce 0.4s infinite; + } +`; diff --git a/src/components/user/customerService/faq/FAQItem.tsx b/src/components/user/customerService/faq/FAQItem.tsx new file mode 100644 index 00000000..86bd5841 --- /dev/null +++ b/src/components/user/customerService/faq/FAQItem.tsx @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import * as S from './FAQItem.styled'; +import type { FAQ } from '../../../../models/customerService'; +import FAQContent from './faqContent/FAQContent'; +import ContentBorder from '../../../common/contentBorder/ContentBorder'; +import NoResult from '../../../common/noResult/NoResult'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; + +interface FAQItemProps { + faqData: FAQ[]; + $isAdmin?: boolean; +} + +export default function FAQItem({ faqData, $isAdmin = false }: FAQItemProps) { + const [showFAQ, setShowFAQ] = useState(10); + + return ( + + + {faqData.length > 0 ? ( + faqData + .filter((_, index) => index < showFAQ) + .map((list) => ( + + + + + )) + ) : ( + + )} + {faqData.length > showFAQ && ( + <> + setShowFAQ((prev) => prev + 10)} + > + + 더보기 + + + + + + )} + + + ); +} diff --git a/src/components/user/customerService/faq/FAQContent.styled.ts b/src/components/user/customerService/faq/faqContent/FAQContent.styled.ts similarity index 69% rename from src/components/user/customerService/faq/FAQContent.styled.ts rename to src/components/user/customerService/faq/faqContent/FAQContent.styled.ts index 2d7026f5..3b92db03 100644 --- a/src/components/user/customerService/faq/FAQContent.styled.ts +++ b/src/components/user/customerService/faq/faqContent/FAQContent.styled.ts @@ -1,4 +1,9 @@ import styled, { css } from 'styled-components'; +import { + AdminDropdownWrapper, + AdminLink, + AdminLinkWrapper, +} from '../../noticeDetail/content/NoticeDetailContent.styled'; export const ListContainer = styled.div` width: 100%; @@ -33,6 +38,7 @@ export const ListPlusIcon = styled.div<{ $isOpen: boolean }>` `; export const ListContentWrapper = styled.div<{ $isShowContent: boolean }>` + position: relative; max-height: 0; overflow: hidden; @@ -74,3 +80,27 @@ export const ListContent = styled.div` padding-right: 1.5rem; white-space: pre-wrap; `; + +export const AdminFAQDropdownWrapper = styled(AdminDropdownWrapper)` + position: absolute; + right: 1.5rem; + width: fit-content; + height: fit-content; +`; + +export const AdminFAQAuthButton = styled.button` + width: fit-content; + height: fit-content; + svg { + width: 1rem; + height: 1rem; + } +`; + +export const AdminFAQLinkWrapper = styled(AdminLinkWrapper)` + top: -2.5rem; + left: -6.7rem; + flex-direction: row; +`; + +export const AdminFAQLink = styled(AdminLink)``; diff --git a/src/components/user/customerService/faq/faqContent/FAQContent.tsx b/src/components/user/customerService/faq/faqContent/FAQContent.tsx new file mode 100644 index 00000000..e131dbc5 --- /dev/null +++ b/src/components/user/customerService/faq/faqContent/FAQContent.tsx @@ -0,0 +1,87 @@ +import { + ChevronRightIcon, + EllipsisVerticalIcon, + PlusIcon, +} from '@heroicons/react/24/outline'; +import type { FAQ } from '../../../../../models/customerService'; +import * as S from './FAQContent.styled'; +import { useState } from 'react'; +import { ADMIN_ROUTE } from '../../../../../constants/routes'; +import DropDown from '../../../../common/dropDown/DropDown'; +import { useAdminFAQ } from '../../../../../hooks/admin/useAdminFAQ'; +import { useLocation } from 'react-router-dom'; +import { useModal } from '../../../../../hooks/useModal'; +import Modal from '../../../../common/modal/Modal'; + +interface FAQContentProps { + list: FAQ; + isAdmin: boolean; +} + +export default function FAQContent({ list, isAdmin }: FAQContentProps) { + const [isFAQContentOpen, setIsFAQContentOpen] = useState(false); + const location = useLocation(); + const id = String(list.id) || ''; + const pathname = location.pathname; + const { isOpen, message, handleConfirm, handleModalOpen, handleModalClose } = + useModal(); + const { deleteFAQMutate } = useAdminFAQ({ + handleModalOpen, + pathname, + handleConfirm, + }); + + const handleClickDeleteFAQ = () => { + deleteFAQMutate.mutate(id); + }; + + return ( + + setIsFAQContentOpen((prev) => !prev)} + > + {list.title} + + + + + + + + + {list.content} + {isAdmin && ( + + + + + } + > + + + 수정 + + + 삭제 + + + + + )} + + + {message} + + + ); +} diff --git a/src/pages/user/customerService/notice/noticeItem/NoticeItem.styled.ts b/src/components/user/customerService/notice/noticeItem/NoticeItem.styled.ts similarity index 100% rename from src/pages/user/customerService/notice/noticeItem/NoticeItem.styled.ts rename to src/components/user/customerService/notice/noticeItem/NoticeItem.styled.ts diff --git a/src/pages/user/customerService/notice/noticeItem/NoticeItem.tsx b/src/components/user/customerService/notice/noticeItem/NoticeItem.tsx similarity index 83% rename from src/pages/user/customerService/notice/noticeItem/NoticeItem.tsx rename to src/components/user/customerService/notice/noticeItem/NoticeItem.tsx index 1806031a..7716f399 100644 --- a/src/pages/user/customerService/notice/noticeItem/NoticeItem.tsx +++ b/src/components/user/customerService/notice/noticeItem/NoticeItem.tsx @@ -1,8 +1,8 @@ import * as S from './NoticeItem.styled'; -import ContentBorder from '../../../../../components/common/contentBorder/ContentBorder'; +import ContentBorder from '../../../../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 NoticeList from '../NoticeList'; +import NoResult from '../../../../common/noResult/NoResult'; import type { NoticeList as TNoticeList } from '../../../../../models/customerService'; import { useLocation } from 'react-router-dom'; diff --git a/src/constants/admin/adminGap.ts b/src/constants/admin/adminGap.ts new file mode 100644 index 00000000..27f7cc2a --- /dev/null +++ b/src/constants/admin/adminGap.ts @@ -0,0 +1,5 @@ +export const GAP_HEIGHT = { + outletTop: '7rem', + headerTitleTop: '9rem', + sectionTop: '2rem', +} as const; diff --git a/src/constants/admin/sidebar.ts b/src/constants/admin/sidebar.ts index 55535b2a..146d4e83 100644 --- a/src/constants/admin/sidebar.ts +++ b/src/constants/admin/sidebar.ts @@ -19,6 +19,11 @@ export const SIDEBAR_LIST = { title: '공지사항', router: ADMIN_ROUTE.notice, }, + { + name: 'faq', + title: 'FAQ', + router: ADMIN_ROUTE.faq, + }, { name: 'banner', title: '배너관리', diff --git a/src/constants/routes.ts b/src/constants/routes.ts index f65de3b6..d27463c3 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -35,6 +35,7 @@ export const ADMIN_ROUTE = { admin: '/admin', devPals: '/main', notice: 'notice', + faq: 'faq', banner: 'banner', tags: 'tags', allUser: 'all-user', diff --git a/src/hooks/admin/useAdminFAQ.ts b/src/hooks/admin/useAdminFAQ.ts new file mode 100644 index 00000000..6b93e022 --- /dev/null +++ b/src/hooks/admin/useAdminFAQ.ts @@ -0,0 +1,113 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + deleteFAQ, + getFAQDetail, + postFAQ, + putFAQ, +} from '../../api/admin/customerService/FAQ.api'; +import type { WriteBody } from '../../models/customerService'; +import { AxiosError } from 'axios'; +import { useNavigate } from 'react-router-dom'; +import { ADMIN_MODAL_MESSAGE } from '../../constants/admin/adminModal'; +import { CustomerService } from '../queries/user/keys'; + +type State = 'success' | 'fail'; + +export const useAdminFAQ = ({ + handleModalOpen, + formDefault, + pathname, + id = '', + handleConfirm, +}: { + handleModalOpen: (message: string) => void; + formDefault?: () => void; + pathname: string; + id?: string; + handleConfirm?: () => void; +}) => { + 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 { + handleConfirm?.(); + 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 getFAQDetailData = useQuery({ + queryKey: [CustomerService.faq, id], + queryFn: () => getFAQDetail(id), + enabled: !!id, + }); + + const postFAQMutate = useMutation({ + mutationFn: (formData: WriteBody) => postFAQ(formData), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CustomerService.faq], + }); + handleButtonState('success'); + }, + onError: () => { + handleButtonState('fail'); + }, + }); + + const putFAQMutate = useMutation< + void, + AxiosError, + { id: string; formDataObj: WriteBody } + >({ + mutationFn: ({ id, formDataObj: formData }) => putFAQ({ id, formData }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CustomerService.faq], + }); + handleButtonState('success'); + }, + onError: () => { + handleButtonState('fail'); + }, + }); + + const deleteFAQMutate = useMutation({ + mutationFn: (id: string) => deleteFAQ(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [CustomerService.faq], + }); + handleButtonState('success', true); + }, + onError: () => { + handleButtonState('fail'); + }, + }); + + return { getFAQDetailData, postFAQMutate, putFAQMutate, deleteFAQMutate }; +}; diff --git a/src/models/customerService.ts b/src/models/customerService.ts index 6f5cf87e..c7ded677 100644 --- a/src/models/customerService.ts +++ b/src/models/customerService.ts @@ -10,6 +10,14 @@ export interface ApiFAQ extends ApiCommonType { data: FAQ[]; } +export interface FAQDetail extends WriteBody { + id: number; +} + +export interface ApiFAQDetail extends ApiCommonType { + data: FAQDetail; +} + export interface NoticeList extends OtherNotice { content: string; } diff --git a/src/pages/admin/CommonAdminPage.styled.ts b/src/pages/admin/CommonAdminPage.styled.ts index f98efd8a..30d11b22 100644 --- a/src/pages/admin/CommonAdminPage.styled.ts +++ b/src/pages/admin/CommonAdminPage.styled.ts @@ -1,11 +1,13 @@ import styled from 'styled-components'; import { SpinnerWrapperStyled } from '../../components/user/mypage/Spinner.styled'; +import { GAP_HEIGHT } from '../../constants/admin/adminGap'; export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; export const AdminNoticeContainer = styled.div``; -export const NoticeItemWrapper = styled.section` - display: flex; - justify-content: center; +export const FixedTitle = styled.div``; + +export const OutletWrapper = styled.div` + margin-top: ${GAP_HEIGHT.outletTop}; `; diff --git a/src/pages/admin/CommonAdminPage.tsx b/src/pages/admin/CommonAdminPage.tsx index dd00220f..2d71a100 100644 --- a/src/pages/admin/CommonAdminPage.tsx +++ b/src/pages/admin/CommonAdminPage.tsx @@ -9,8 +9,12 @@ interface CommonAdminPageProps { export default function CommonAdminPage({ title }: CommonAdminPageProps) { return ( - - + + + + + + ); } diff --git a/src/pages/admin/adminAllUser/AdminAllUser.tsx b/src/pages/admin/adminAllUser/AdminAllUser.tsx index 259a433a..927d43ad 100644 --- a/src/pages/admin/adminAllUser/AdminAllUser.tsx +++ b/src/pages/admin/adminAllUser/AdminAllUser.tsx @@ -30,7 +30,7 @@ const AdminAllUser = () => { diff --git a/src/pages/admin/adminFAQ/AdminFAQ.styled.ts b/src/pages/admin/adminFAQ/AdminFAQ.styled.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/admin/adminFAQ/AdminFAQ.tsx b/src/pages/admin/adminFAQ/AdminFAQ.tsx index e69de29b..5a131d56 100644 --- a/src/pages/admin/adminFAQ/AdminFAQ.tsx +++ b/src/pages/admin/adminFAQ/AdminFAQ.tsx @@ -0,0 +1,5 @@ +import CommonAdminPage from '../CommonAdminPage'; + +export default function AdminFAQ() { + return ; +} diff --git a/src/pages/admin/adminFAQ/adminFAQList/AdminFAQListPage.tsx b/src/pages/admin/adminFAQ/adminFAQList/AdminFAQListPage.tsx new file mode 100644 index 00000000..0dc4134b --- /dev/null +++ b/src/pages/admin/adminFAQ/adminFAQList/AdminFAQListPage.tsx @@ -0,0 +1,5 @@ +import AdminFAQList from '../../../../components/admin/adminFAQ/AdminFAQList'; + +export default function AdminFAQListPage() { + return ; +} diff --git a/src/pages/admin/adminFAQ/adminFAQWrite/AdminFAQWritePage.tsx b/src/pages/admin/adminFAQ/adminFAQWrite/AdminFAQWritePage.tsx new file mode 100644 index 00000000..2d19dcc3 --- /dev/null +++ b/src/pages/admin/adminFAQ/adminFAQWrite/AdminFAQWritePage.tsx @@ -0,0 +1,5 @@ +import AdminFAQWrite from '../../../../components/admin/adminFAQ/AdminFAQWrite'; + +export default function AdminFAQWritePage() { + return ; +} diff --git a/src/pages/admin/adminNotice/AdminNotice.tsx b/src/pages/admin/adminNotice/AdminNotice.tsx index 8e456481..aa5519ca 100644 --- a/src/pages/admin/adminNotice/AdminNotice.tsx +++ b/src/pages/admin/adminNotice/AdminNotice.tsx @@ -1,4 +1,4 @@ -import CommonAdminPage from '../commonAdminPage'; +import CommonAdminPage from '../CommonAdminPage'; export default function AdminNotice() { return ; diff --git a/src/pages/user/customerService/faq/FAQ.styled.ts b/src/pages/user/customerService/faq/FAQ.styled.ts index 5cdddc6b..477b7e92 100644 --- a/src/pages/user/customerService/faq/FAQ.styled.ts +++ b/src/pages/user/customerService/faq/FAQ.styled.ts @@ -2,61 +2,3 @@ import styled from 'styled-components'; import { SpinnerWrapperStyled } from '../../../../components/user/mypage/Spinner.styled'; export const SpinnerWrapper = styled(SpinnerWrapperStyled)``; - -export const Container = styled.section` - margin-top: 2rem; - margin-bottom: 5rem; - width: 100%; - display: flex; - justify-content: center; -`; - -export const Wrapper = styled.div` - width: 75%; - display: flex; - flex-direction: column; -`; - -export const ToggleWrapper = styled.div``; - -export const ShowMoreFAQWrapper = styled.div``; - -export const ShowMoreFAQ = styled.button` - width: 100%; - display: flex; - justify-content: center; - align-items: center; - font-weight: bold; - font-size: 1rem; - - svg { - width: 1rem; - } - - &:hover { - background: ${({ theme }) => theme.color.lightgrey}; - } -`; - -export const ShowMoreSpan = styled.span` - width: 100%; - padding: 1.2rem 0; - display: flex; - justify-content: center; - gap: 0.5rem; - @keyframes bounce { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(-3px); - } - 100% { - transform: translateY(0); - } - } - - &:hover { - animation: bounce 0.4s infinite; - } -`; diff --git a/src/pages/user/customerService/faq/FAQ.tsx b/src/pages/user/customerService/faq/FAQ.tsx index f064924d..7f45cec7 100644 --- a/src/pages/user/customerService/faq/FAQ.tsx +++ b/src/pages/user/customerService/faq/FAQ.tsx @@ -1,18 +1,14 @@ import { useState } from 'react'; import * as S from './FAQ.styled'; -import { ChevronDownIcon } from '@heroicons/react/24/outline'; import type { SearchKeyword } from '../../../../models/customerService'; import { useGetFAQ } from '../../../../hooks/user/useGetFAQ'; import { Spinner } from '../../../../components/common/loadingSpinner/LoadingSpinner.styled'; import CustomerServiceHeader from '../../../../components/user/customerService/CustomerServiceHeader'; -import FAQContent from '../../../../components/user/customerService/faq/FAQContent'; -import ContentBorder from '../../../../components/common/contentBorder/ContentBorder'; -import NoResult from '../../../../components/common/noResult/NoResult'; +import FAQItem from '../../../../components/user/customerService/faq/FAQItem'; export default function FAQ() { const [keyword, setKeyword] = useState({ keyword: '' }); const [value, setValue] = useState(''); - const [showFAQ, setShowFAQ] = useState(10); const { faqData, isLoading } = useGetFAQ(keyword); const handleGetKeyword = (keyword: string) => { @@ -37,36 +33,7 @@ export default function FAQ() { keyword={value} onGetKeyword={handleGetKeyword} /> - - - {faqData.length > 0 ? ( - faqData - .filter((_, index) => index < showFAQ) - .map((list) => ( - - - - - )) - ) : ( - - )} - {faqData.length > showFAQ && ( - <> - setShowFAQ((prev) => prev + 10)} - > - - 더보기 - - - - - - )} - - + ); } diff --git a/src/pages/user/customerService/notice/Notice.tsx b/src/pages/user/customerService/notice/Notice.tsx index e78bf6cc..0d0ef219 100644 --- a/src/pages/user/customerService/notice/Notice.tsx +++ b/src/pages/user/customerService/notice/Notice.tsx @@ -6,7 +6,7 @@ import { Spinner } from '../../../../components/common/loadingSpinner/LoadingSpi import CustomerServiceHeader from '../../../../components/user/customerService/CustomerServiceHeader'; import Pagination from '../../../../components/common/pagination/Pagination'; import { useSearchParams } from 'react-router-dom'; -import NoticeItem from './noticeItem/NoticeItem'; +import NoticeItem from '../../../../components/user/customerService/notice/noticeItem/NoticeItem'; export default function Notice() { const [noticeSearch, setNoticeSearch] = useState({ diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index 7ec5e7da..b8db82a0 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -18,6 +18,13 @@ const NoticeWrite = lazy( const NoticeDetail = lazy( () => import('../pages/admin/adminNoticeDetail/AdminNoticeDetail') ); +const FAQ = lazy(() => import('../pages/admin/adminFAQ/AdminFAQ')); +const FAQList = lazy( + () => import('../pages/admin/adminFAQ/adminFAQList/AdminFAQListPage') +); +const FAQWrite = lazy( + () => import('../pages/admin/adminFAQ/adminFAQWrite/AdminFAQWritePage') +); const Banner = lazy(() => import('../pages/admin/adminBanner/AdminBanner')); const Tags = lazy(() => import('../pages/admin/adminTags/AdminTags')); const AllUser = lazy(() => import('../pages/admin/adminAllUser/AdminAllUser')); @@ -60,6 +67,18 @@ export const AdminRoutes = () => { }, ], }, + { + path: ADMIN_ROUTE.faq, + element: , + children: [ + { index: true, element: }, + { path: ADMIN_ROUTE.write, element: }, + { + path: `${ADMIN_ROUTE.modification}/:faqId`, + element: , + }, + ], + }, { path: ADMIN_ROUTE.banner, element: ,