diff --git a/src/api/customerService.api.ts b/src/api/customerService.api.ts new file mode 100644 index 00000000..43136237 --- /dev/null +++ b/src/api/customerService.api.ts @@ -0,0 +1,13 @@ +import type { ApiFAQ, SearchKeyword } from '../models/customerService'; +import { httpClient } from './http.api'; + +export const getFAQ = async (params: SearchKeyword) => { + try { + const response = await httpClient.get(`/faq`, { params }); + + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/components/common/emptyLoadingPage/EmptyLoadingPage.styled.ts b/src/components/common/emptyLoading/EmptyLoading.styled.ts similarity index 85% rename from src/components/common/emptyLoadingPage/EmptyLoadingPage.styled.ts rename to src/components/common/emptyLoading/EmptyLoading.styled.ts index 7b0b3637..6e091c58 100644 --- a/src/components/common/emptyLoadingPage/EmptyLoadingPage.styled.ts +++ b/src/components/common/emptyLoading/EmptyLoading.styled.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { EmptyLoadingPageProps } from './EmptyLoadingPage'; +import { EmptyLoadingPageProps } from './EmptyLoading'; export const Container = styled.div` width: 100%; diff --git a/src/components/common/emptyLoadingPage/EmptyLoadingPage.tsx b/src/components/common/emptyLoading/EmptyLoading.tsx similarity index 76% rename from src/components/common/emptyLoadingPage/EmptyLoadingPage.tsx rename to src/components/common/emptyLoading/EmptyLoading.tsx index 54954218..d00d5d75 100644 --- a/src/components/common/emptyLoadingPage/EmptyLoadingPage.tsx +++ b/src/components/common/emptyLoading/EmptyLoading.tsx @@ -1,4 +1,4 @@ -import * as S from './EmptyLoadingPage.styled'; +import * as S from './EmptyLoading.styled'; export interface EmptyLoadingPageProps { height: string; @@ -6,7 +6,7 @@ export interface EmptyLoadingPageProps { $mHeight: string; } -export default function EmptyLoadingPage({ +export default function EmptyLoading({ height, $tHeight, $mHeight, diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx index 14d86356..b48e1611 100644 --- a/src/components/common/header/Header.tsx +++ b/src/components/common/header/Header.tsx @@ -46,10 +46,10 @@ function Header() { - + FAQ - + 공지사항 diff --git a/src/components/common/noResultPage/NoResultPage.styled.ts b/src/components/common/noResult/NoResult.styled.ts similarity index 91% rename from src/components/common/noResultPage/NoResultPage.styled.ts rename to src/components/common/noResult/NoResult.styled.ts index 6614a1f1..bc97ef8c 100644 --- a/src/components/common/noResultPage/NoResultPage.styled.ts +++ b/src/components/common/noResult/NoResult.styled.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { NoResultPageProps } from './NoResultPage'; +import { NoResultPageProps } from './NoResult'; export const Container = styled.div>` width: 100%; diff --git a/src/components/common/noResultPage/NoResultPage.tsx b/src/components/common/noResult/NoResult.tsx similarity index 73% rename from src/components/common/noResultPage/NoResultPage.tsx rename to src/components/common/noResult/NoResult.tsx index e5f034bc..bafc441b 100644 --- a/src/components/common/noResultPage/NoResultPage.tsx +++ b/src/components/common/noResult/NoResult.tsx @@ -1,11 +1,11 @@ import { FaceFrownIcon } from '@heroicons/react/24/outline'; -import * as S from './NoResultPage.styled'; +import * as S from './NoResult.styled'; export interface NoResultPageProps { height: string; } -export default function NoResultPage({ height }: NoResultPageProps) { +export default function NoResult({ height }: NoResultPageProps) { return ( diff --git a/src/pages/customerService/CustomerServiceHeader.styled.ts b/src/components/customerService/CustomerServiceHeader.styled.ts similarity index 77% rename from src/pages/customerService/CustomerServiceHeader.styled.ts rename to src/components/customerService/CustomerServiceHeader.styled.ts index df3f2700..76e659d4 100644 --- a/src/pages/customerService/CustomerServiceHeader.styled.ts +++ b/src/components/customerService/CustomerServiceHeader.styled.ts @@ -35,6 +35,21 @@ export const SearchBarInput = styled.input` font-size: 1rem; `; +export const ButtonWrapper = styled.div` + display: flex; + gap: 0.5rem; +`; + +export const UturnButton = styled.button` + &:hover { + color: ${({ theme }) => theme.color.lightnavy}; + } + svg { + width: 20px; + height: 20px; + } +`; + export const SearchButton = styled.button` svg { width: 20px; diff --git a/src/components/customerService/CustomerServiceHeader.tsx b/src/components/customerService/CustomerServiceHeader.tsx new file mode 100644 index 00000000..47a14493 --- /dev/null +++ b/src/components/customerService/CustomerServiceHeader.tsx @@ -0,0 +1,71 @@ +import { + ArrowUturnLeftIcon, + MagnifyingGlassIcon, +} 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'; + +interface CustomerServiceHeaderProps { + title: string; + keyword: string; + onGetKeyword: (value: string) => void; +} + +export default function CustomerServiceHeader({ + title, + keyword, + onGetKeyword, +}: CustomerServiceHeaderProps) { + const [inputValue, setInputValue] = useState(''); + + const handleSubmitKeyword = (e: React.FormEvent) => { + e.preventDefault(); + onGetKeyword(inputValue); + }; + + const handleChangeValue = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + }; + + const handleReset = () => { + onGetKeyword(''); + setInputValue(''); + }; + + return ( + + + DevPals {title} + + + + + + {keyword !== '' && ( + + + + )} + + + + + + + + + + ); +} diff --git a/src/pages/customerService/MoveInquiredLink.styled.ts b/src/components/customerService/MoveInquiredLink.styled.ts similarity index 100% rename from src/pages/customerService/MoveInquiredLink.styled.ts rename to src/components/customerService/MoveInquiredLink.styled.ts diff --git a/src/pages/customerService/MoveInquiredLink.tsx b/src/components/customerService/MoveInquiredLink.tsx similarity index 100% rename from src/pages/customerService/MoveInquiredLink.tsx rename to src/components/customerService/MoveInquiredLink.tsx diff --git a/src/components/customerService/faq/FAQContent.styled.ts b/src/components/customerService/faq/FAQContent.styled.ts new file mode 100644 index 00000000..c7c62df1 --- /dev/null +++ b/src/components/customerService/faq/FAQContent.styled.ts @@ -0,0 +1,53 @@ +import styled, { css } from 'styled-components'; + +export const ListContainer = styled.div` + width: 100%; +`; + +export const ListWrapper = styled.button` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 0; +`; + +export const ListTitle = styled.div` + font-size: 1.3rem; + padding-left: 1.5rem; + font-weight: bold; +`; + +export const ListPlusIcon = styled.div<{ $isOpen: boolean }>` + margin-right: 1.5rem; + transition: transform 500ms ease-in-out; + ${({ $isOpen }) => + $isOpen && + css` + transform: rotate(45deg); + `} + svg { + width: 1.5rem; + height: 1.5rem; + } +`; + +export const ListContentWrapper = styled.div` + cursor: auto; + background: ${({ theme }) => theme.color.lightgrey}; + padding: 1.5rem 1rem; + display: flex; + gap: 0.5rem; +`; + +export const ListButtonWrapper = styled.div` + svg { + width: 1.3rem; + height: 1.3rem; + } +`; + +export const ListContent = styled.div` + font-size: 1.1rem; + padding-right: 1.5rem; +`; diff --git a/src/components/customerService/faq/FAQContent.tsx b/src/components/customerService/faq/FAQContent.tsx new file mode 100644 index 00000000..785e7278 --- /dev/null +++ b/src/components/customerService/faq/FAQContent.tsx @@ -0,0 +1,34 @@ +import { ChevronRightIcon, PlusIcon } from '@heroicons/react/24/outline'; +import { 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} + + + + + {isFAQContentOpen && ( + + + + + {list.content} + + )} + + ); +} diff --git a/src/pages/customerService/inquiry/Inquiry.styled.ts b/src/components/customerService/inquiry/Inquiry.styled.ts similarity index 100% rename from src/pages/customerService/inquiry/Inquiry.styled.ts rename to src/components/customerService/inquiry/Inquiry.styled.ts diff --git a/src/pages/customerService/inquiry/Inquiry.tsx b/src/components/customerService/inquiry/Inquiry.tsx similarity index 100% rename from src/pages/customerService/inquiry/Inquiry.tsx rename to src/components/customerService/inquiry/Inquiry.tsx diff --git a/src/components/home/projectCardLists/ProjectCardLists.tsx b/src/components/home/projectCardLists/ProjectCardLists.tsx index b00234d1..696e4300 100644 --- a/src/components/home/projectCardLists/ProjectCardLists.tsx +++ b/src/components/home/projectCardLists/ProjectCardLists.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; import { useProjectCardListData } from '../../../hooks/useProjectCardListData'; -import EmptyLoadingPage from '../../common/emptyLoadingPage/EmptyLoadingPage'; -import NoResultPage from '../../common/noResultPage/NoResultPage'; import CardList from './cardList/CardList'; import Pagination from './pagination/Pagination'; import * as S from './ProjectCardLists.styled'; import { Link } from 'react-router-dom'; import { ROUTES } from '../../../constants/routes'; +import EmptyLoading from '../../common/emptyLoading/EmptyLoading'; +import NoResult from '../../common/noResult/NoResult'; export default function ProjectCardLists() { const { projectListsData, isLoading } = useProjectCardListData(); @@ -22,7 +22,7 @@ export default function ProjectCardLists() { if (isLoading) return ( - + ); return ( @@ -41,7 +41,7 @@ export default function ProjectCardLists() { )) ) : ( - + )} diff --git a/src/components/mypage/ScrollWrapper.styled.ts b/src/components/mypage/ScrollWrapper.styled.ts index dc69ea1b..ed937b57 100644 --- a/src/components/mypage/ScrollWrapper.styled.ts +++ b/src/components/mypage/ScrollWrapper.styled.ts @@ -4,6 +4,7 @@ export const ScrollWrapper = styled.div<{ $height: string }>` width: 100%; height: calc(100% - ${({ $height }) => $height}); overflow-y: auto; + overflow-x: hidden; background: ${({ theme }) => theme.color.lightgrey}; border-radius: ${({ theme }) => theme.borderRadius.large}; diff --git a/src/components/mypage/activityLog/inquiries/Inquiries.styled.ts b/src/components/mypage/activityLog/inquiries/Inquiries.styled.ts index c1a3d2c5..b278bef1 100644 --- a/src/components/mypage/activityLog/inquiries/Inquiries.styled.ts +++ b/src/components/mypage/activityLog/inquiries/Inquiries.styled.ts @@ -2,20 +2,34 @@ import styled from 'styled-components'; export const container = styled.div` height: 100%; + padding: 0 0.5rem 0 0.8rem; `; export const InquiriesContainer = styled.div` - padding: 1rem; width: 100%; `; +export const InquiriesTableHeadContainer = styled.div` + width: 100%; + position: sticky; + padding-top: 1rem; + top: 0; + background: ${({ theme }) => theme.color.lightgrey}; +`; + export const InquiriesTableHeadWrapper = styled.div` width: 100%; display: grid; - grid-template-columns: 8% 18% 62% 12%; + grid-template-columns: 8% 15% 65% 17%; font-size: 1.3rem; font-weight: 600; - margin-bottom: 1rem; + margin-bottom: 0.5rem; +`; + +export const ContentBorder = styled.div` + width: 100%; + height: 0.5px; + background: ${({ theme }) => theme.color.placeholder}; `; export const InquiriesTableHeaderNo = styled.div` @@ -36,6 +50,7 @@ export const InquiriesTableHeaderState = styled.div` `; export const InquiriesWrapper = styled.div` + margin-top: 1rem; display: flex; flex-direction: column; gap: 1.5rem; diff --git a/src/components/mypage/activityLog/inquiries/Inquiries.tsx b/src/components/mypage/activityLog/inquiries/Inquiries.tsx index 99b4d414..57a3addf 100644 --- a/src/components/mypage/activityLog/inquiries/Inquiries.tsx +++ b/src/components/mypage/activityLog/inquiries/Inquiries.tsx @@ -21,12 +21,17 @@ export default function Inquiries() { return ( - - No - 구별 - 제목 - 상태 - + + + No + + 구별 + + 제목 + 상태 + + + {myInquiriesData.map((list, index) => ( - setIsOpen((prev) => !prev)}> + setIsOpen((prev) => !prev)} + > {no} {`[${list.category}]`} {list.title} diff --git a/src/components/mypage/myProfile/MyProfile.styled.ts b/src/components/mypage/myProfile/MyProfile.styled.ts index 017ee3b6..b02942ff 100644 --- a/src/components/mypage/myProfile/MyProfile.styled.ts +++ b/src/components/mypage/myProfile/MyProfile.styled.ts @@ -31,8 +31,5 @@ export const Container = styled.div` `; export const SectionContainer = styled.section` - background-color: ${({ theme }) => theme.color.lightgrey}; - border-radius: ${({ theme }) => theme.borderRadius.large} 0 0 - ${({ theme }) => theme.borderRadius.large}; padding: 2rem; `; diff --git a/src/components/mypage/myProfile/profile/Profile.tsx b/src/components/mypage/myProfile/profile/Profile.tsx index a35cda43..9c00dcd2 100644 --- a/src/components/mypage/myProfile/profile/Profile.tsx +++ b/src/components/mypage/myProfile/profile/Profile.tsx @@ -8,6 +8,7 @@ import { Radar } from 'react-chartjs-2'; import { UserInfo } from '../../../../models/userInfo'; import { useEffect } from 'react'; import MyProfileWrapper from '../MyProfileWrapper'; +import { PROFILE_DEFAULT_MESSAGE } from '../../../../constants/myPageProfile'; export default function Profile() { const { @@ -52,7 +53,7 @@ export default function Profile() { )) ) : ( -
  • 스킬을 선택해주세요.
  • +
  • {PROFILE_DEFAULT_MESSAGE.skills}
  • )} @@ -68,7 +69,7 @@ export default function Profile() { {position.name} )) ) : ( - 포지션을 선택해주세요. + {PROFILE_DEFAULT_MESSAGE.positions} )} @@ -76,7 +77,7 @@ export default function Profile() { - {myData.github || '깃허브 링크를 올려보세요.'} + {myData.github || PROFILE_DEFAULT_MESSAGE.github} @@ -92,7 +93,7 @@ export default function Profile() { )) ) : ( -
  • 경력을 기록하세요.
  • +
  • {PROFILE_DEFAULT_MESSAGE.career}
  • )} @@ -100,7 +101,7 @@ export default function Profile() { - {myData.bio || '내 소개를 적어주세요.'} + {myData.bio || PROFILE_DEFAULT_MESSAGE.bio} diff --git a/src/components/mypage/notifications/all/All.tsx b/src/components/mypage/notifications/all/All.tsx index af7c190c..aa494266 100644 --- a/src/components/mypage/notifications/all/All.tsx +++ b/src/components/mypage/notifications/all/All.tsx @@ -32,7 +32,16 @@ export default function All() { return ; } - if (!alarmListData || alarmListData.length === 0) { + const filterLength = alarmListData?.filter((list) => { + if (filterId === 0) { + return true; + } else if (list.alarmFilterId === filterId) { + return true; + } + return false; + }).length; + + if (!alarmListData || alarmListData.length === 0 || filterLength === 0) { return ( diff --git a/src/constants/myPageProfile.ts b/src/constants/myPageProfile.ts new file mode 100644 index 00000000..d9ea3c55 --- /dev/null +++ b/src/constants/myPageProfile.ts @@ -0,0 +1,7 @@ +export const PROFILE_DEFAULT_MESSAGE = { + github: '깃허브를 공유하세요.', + positions: '포지션을 선택해주세요.', + skills: '스킬을 선택해주세요.', + career: '경력을 기록하세요.', + bio: '소개를 적어주세요.', +}; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index ee74a8bc..e0d21454 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -23,7 +23,8 @@ export const ROUTES = { userJoinedProject: 'joined-projects', modifyProject: '/project-modify', notFound: '/not-found', - FAQ: '/faq', - notice: '/notice', + customerService: '/customer-service', + FAQ: 'faq', + notice: 'notice', inquiry: '/inquiry', } as const; diff --git a/src/hooks/queries/keys.ts b/src/hooks/queries/keys.ts index d538e460..2ab0e945 100644 --- a/src/hooks/queries/keys.ts +++ b/src/hooks/queries/keys.ts @@ -43,3 +43,8 @@ export const ActivityLog = { myComments: ['MyComments'], myInquiries: ['MyInquiries'], }; + +export const CustomerService = { + faq: 'faq', + notice: 'notice', +}; diff --git a/src/hooks/useGetFAQ.ts b/src/hooks/useGetFAQ.ts new file mode 100644 index 00000000..cde44d6a --- /dev/null +++ b/src/hooks/useGetFAQ.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; +import { getFAQ } from '../api/customerService.api'; +import { CustomerService } from './queries/keys'; +import { SearchKeyword } from '../models/customerService'; + +export const useGetFAQ = (keyword: SearchKeyword) => { + const { data: faqData, isLoading } = useQuery({ + // keyword 조회시 keyword 키 추가 + queryKey: [CustomerService.faq, keyword], + queryFn: () => getFAQ(keyword), + staleTime: Infinity, + gcTime: Infinity, + }); + + return { faqData, isLoading }; +}; diff --git a/src/hooks/useMyInfo.ts b/src/hooks/useMyInfo.ts index 435c46f3..cbe715fe 100644 --- a/src/hooks/useMyInfo.ts +++ b/src/hooks/useMyInfo.ts @@ -25,6 +25,7 @@ export const useMyProfileInfo = () => { queryKey: myInfoKey.myProfile, queryFn: () => getMyInfo(), staleTime: Infinity, + gcTime: Infinity, enabled: isLoggedIn, }); diff --git a/src/models/customerService.ts b/src/models/customerService.ts new file mode 100644 index 00000000..5845da97 --- /dev/null +++ b/src/models/customerService.ts @@ -0,0 +1,15 @@ +import { ApiCommonType } from './apiCommon'; + +export interface FAQ { + id: number; + title: string; + content: string; +} + +export interface ApiFAQ extends ApiCommonType { + data: FAQ[]; +} + +export interface SearchKeyword { + keyword: string; +} diff --git a/src/pages/customerService/CustomerServiceHeader.tsx b/src/pages/customerService/CustomerServiceHeader.tsx deleted file mode 100644 index e5aa9dff..00000000 --- a/src/pages/customerService/CustomerServiceHeader.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import * as S from './CustomerServiceHeader.styled'; -import MovedInquiredLink from './MoveInquiredLink'; - -interface CustomerServiceHeaderProps { - title: string; -} - -export default function CustomerServiceHeader({ - title, -}: CustomerServiceHeaderProps) { - return ( - - - DevPals {title} - - - - - - - - - - - - ); -} diff --git a/src/pages/customerService/faq/FAQ.styled.ts b/src/pages/customerService/faq/FAQ.styled.ts index 926ed2ed..1bc2056a 100644 --- a/src/pages/customerService/faq/FAQ.styled.ts +++ b/src/pages/customerService/faq/FAQ.styled.ts @@ -1,3 +1,27 @@ import styled from 'styled-components'; -export const Container = styled.section``; +export const SpinnerWrapper = styled.div` + height: 60vh; +`; + +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 ContentBorder = styled.div` + width: 100%; + height: 0.5px; + background: ${({ theme }) => theme.color.placeholder}; +`; diff --git a/src/pages/customerService/faq/FAQ.tsx b/src/pages/customerService/faq/FAQ.tsx index 6f0c93f2..78f125fa 100644 --- a/src/pages/customerService/faq/FAQ.tsx +++ b/src/pages/customerService/faq/FAQ.tsx @@ -1,11 +1,53 @@ -import CustomerServiceHeader from '../CustomerServiceHeader'; +import { useState } from 'react'; +import { useGetFAQ } from '../../../hooks/useGetFAQ'; import * as S from './FAQ.styled'; +import { SearchKeyword } from '../../../models/customerService'; +import Spinner from '../../../components/mypage/Spinner'; +import CustomerServiceHeader from '../../../components/customerService/CustomerServiceHeader'; +import FAQContent from '../../../components/customerService/faq/FAQContent'; +import NoResult from '../../../components/common/noResult/NoResult'; export default function FAQ() { + const [keyword, setKeyword] = useState({ keyword: '' }); + const [value, setValue] = useState(''); + const { faqData, isLoading } = useGetFAQ(keyword); + + const handleGetKeyword = (keyword: string) => { + setKeyword({ keyword }); + setValue(keyword); + }; + + if (isLoading) { + return ( + + + + ); + } + + if (!faqData) return; + return ( <> - - + + + + {faqData.length > 0 ? ( + faqData.map((list) => ( + + + + + )) + ) : ( + + )} + + ); } diff --git a/src/pages/customerService/notice/Notice.tsx b/src/pages/customerService/notice/Notice.tsx index 36dc211b..f6cd848c 100644 --- a/src/pages/customerService/notice/Notice.tsx +++ b/src/pages/customerService/notice/Notice.tsx @@ -1,10 +1,8 @@ -import CustomerServiceHeader from '../CustomerServiceHeader'; import * as S from './Notice.styled'; export default function Notice() { return ( <> - ); diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index e2624297..e95f4d8d 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -23,7 +23,9 @@ const Layout = lazy(() => import('../components/common/layout/Layout')); const Home = lazy(() => import('../pages/home/Home')); const FAQ = lazy(() => import('../pages/customerService/faq/FAQ')); const Notice = lazy(() => import('../pages/customerService/notice/Notice')); -const Inquiry = lazy(() => import('../pages/customerService/inquiry/Inquiry')); +const Inquiry = lazy( + () => import('../components/customerService/inquiry/Inquiry') +); const MyPage = lazy(() => import('../pages/mypage/MyPage')); const UserPage = lazy(() => import('../pages/userpage/UserPage')); const Apply = lazy(() => import('../pages/apply/ApplyStep')); @@ -120,24 +122,28 @@ const AppRoutes = () => { ), }, { - path: ROUTES.FAQ, - element: ( - - - - - - ), - }, - { - path: ROUTES.notice, + path: ROUTES.customerService, element: ( - + ), + children: [ + { + index: true, + element: , + }, + { + path: ROUTES.FAQ, + element: , + }, + { + path: ROUTES.notice, + element: , + }, + ], }, { path: ROUTES.inquiry,