Skip to content

Commit 0e708cb

Browse files
authored
Merge pull request #334 from devpalsPlus/feat/#333
관리자 페이지 FAQ 구현 (#issue 333)
2 parents ab68b7d + a5a7055 commit 0e708cb

File tree

36 files changed

+688
-164
lines changed

36 files changed

+688
-164
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ApiCommonBasicType } from '../../../models/apiCommon';
2+
import type { ApiFAQDetail, WriteBody } from '../../../models/customerService';
3+
import { httpClient } from '../../http.api';
4+
5+
export const getFAQDetail = async (id: string) => {
6+
try {
7+
const response = await httpClient.get<ApiFAQDetail>(`/faq/${id}`);
8+
9+
return response.data.data;
10+
} catch (e) {
11+
console.error(e);
12+
throw e;
13+
}
14+
};
15+
16+
export const postFAQ = async (formData: WriteBody) => {
17+
try {
18+
await httpClient.post<ApiCommonBasicType>(`faq`, formData);
19+
} catch (e) {
20+
console.error(e);
21+
throw e;
22+
}
23+
};
24+
25+
export const putFAQ = async ({
26+
id,
27+
formData,
28+
}: {
29+
id: string;
30+
formData: WriteBody;
31+
}) => {
32+
try {
33+
await httpClient.put<ApiCommonBasicType>(`faq/${id}`, formData);
34+
} catch (e) {
35+
console.error(e);
36+
throw e;
37+
}
38+
};
39+
40+
export const deleteFAQ = async (id: string) => {
41+
try {
42+
await httpClient.delete<ApiCommonBasicType>(`faq/${id}`);
43+
} catch (e) {
44+
console.error(e);
45+
throw e;
46+
}
47+
};

src/api/auth.api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import {
1+
import type {
22
ApiGetAllUsers,
33
ApiGetAllUsersPreview,
4-
type ApiOauth,
5-
type ApiVerifyNickname,
6-
type VerifyEmail,
4+
ApiOauth,
5+
ApiVerifyNickname,
6+
VerifyEmail,
77
} from '../models/auth';
88
import { httpClient } from './http.api';
99
import { loginFormValues } from '../pages/login/Login';
1010
import { registerFormValues } from '../pages/user/register/Register';
1111
import { changePasswordFormValues } from '../pages/user/changePassword/ChangePassword';
12-
import { type SearchType } from '../models/search';
12+
import type { SearchType } from '../models/search';
1313

1414
export const postVerificationEmail = async (email: string) => {
1515
try {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import styled from 'styled-components';
2+
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';
3+
import { SearchBarFixedWrapperStyled } from '../../common/admin/searchBar/SearchBar.styled';
4+
import { GAP_HEIGHT } from '../../../constants/admin/adminGap';
5+
6+
export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;
7+
8+
export const SearchBarFixedWrapper = styled(SearchBarFixedWrapperStyled)``;
9+
10+
export const FAQItemWrapper = styled.div`
11+
margin-top: calc(${GAP_HEIGHT.headerTitleTop} + ${GAP_HEIGHT.sectionTop});
12+
`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as S from './AdminFAQList.styled';
2+
import SearchBar from '../../../components/common/admin/searchBar/SearchBar';
3+
import FAQItem from '../../user/customerService/faq/FAQItem';
4+
import { useGetFAQ } from '../../../hooks/user/useGetFAQ';
5+
import Spinner from '../../user/mypage/Spinner';
6+
import useSearchBar from '../../../hooks/admin/useSearchBar';
7+
8+
export default function AdminFAQList() {
9+
const { searchUnit, value, handleGetKeyword } = useSearchBar();
10+
const keyword = searchUnit.keyword;
11+
const { faqData, isLoading } = useGetFAQ({ keyword });
12+
13+
if (isLoading) {
14+
return (
15+
<S.SpinnerWrapper>
16+
<Spinner />
17+
</S.SpinnerWrapper>
18+
);
19+
}
20+
21+
if (!faqData) return;
22+
23+
return (
24+
<>
25+
<S.SearchBarFixedWrapper>
26+
<SearchBar onGetKeyword={handleGetKeyword} value={value} />
27+
</S.SearchBarFixedWrapper>
28+
<S.FAQItemWrapper>
29+
<FAQItem faqData={faqData} $isAdmin={true} />
30+
</S.FAQItemWrapper>
31+
</>
32+
);
33+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { INQUIRY_MESSAGE } from '../../../constants/user/customerService';
2+
import * as S from './../../admin/adminNotice/AdminNoticeWrite.styled';
3+
import React, { useEffect, useState } from 'react';
4+
import { useLocation } from 'react-router-dom';
5+
import { useModal } from '../../../hooks/useModal';
6+
import Modal from '../../../components/common/modal/Modal';
7+
import type { WriteBody } from '../../../models/customerService';
8+
import Spinner from '../../../components/user/mypage/Spinner';
9+
import { useAdminFAQ } from '../../../hooks/admin/useAdminFAQ';
10+
11+
export default function AdminFAQWrite() {
12+
const location = useLocation();
13+
const {
14+
isOpen: isModalOpen,
15+
message,
16+
handleModalOpen,
17+
handleModalClose,
18+
} = useModal();
19+
const pathname = location.state?.from || '';
20+
const id = location.state?.id || '';
21+
22+
const formDefault = () => {
23+
setForm({
24+
title: '',
25+
content: '',
26+
});
27+
};
28+
29+
const { getFAQDetailData, postFAQMutate, putFAQMutate } = useAdminFAQ({
30+
handleModalOpen,
31+
formDefault,
32+
pathname,
33+
id,
34+
});
35+
const [form, setForm] = useState<WriteBody>({
36+
title: '',
37+
content: '',
38+
});
39+
const { data: FAQDetailData, isLoading } = getFAQDetailData;
40+
41+
useEffect(() => {
42+
if (!FAQDetailData) return;
43+
setForm({ title: FAQDetailData.title, content: FAQDetailData.content });
44+
}, [FAQDetailData]);
45+
46+
const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => {
47+
e.preventDefault();
48+
49+
const isValid = {
50+
title: form.title.trim() !== '',
51+
content: form.content.trim() !== '',
52+
};
53+
54+
if (!isValid.title) {
55+
return handleModalOpen(INQUIRY_MESSAGE.writeTitle);
56+
}
57+
if (!isValid.content) {
58+
return handleModalOpen(INQUIRY_MESSAGE.writeContent);
59+
}
60+
61+
const formData = new FormData(e.currentTarget as HTMLFormElement);
62+
63+
const formDataObj: WriteBody = {
64+
title: formData.get('title') as string,
65+
content: formData.get('content') as string,
66+
};
67+
68+
if (!id) {
69+
return postFAQMutate.mutate(formDataObj);
70+
} else {
71+
return putFAQMutate.mutate({ id, formDataObj });
72+
}
73+
};
74+
75+
if (isLoading) {
76+
return (
77+
<S.SpinnerWrapper>
78+
<Spinner />
79+
</S.SpinnerWrapper>
80+
);
81+
}
82+
83+
return (
84+
<S.AdminNoticeContainer>
85+
<S.AdminNoticeForm
86+
onSubmit={handleSubmitInquiry}
87+
method='post'
88+
encType='multipart/form-data'
89+
>
90+
<S.AdminNoticeWrapper>
91+
<S.AdminNoticeNav>
92+
<S.AdminNoticeInputTitle
93+
name='title'
94+
type='text'
95+
placeholder='제목을 입력하세요.'
96+
value={form.title}
97+
onChange={(e) =>
98+
setForm((prev) => ({ ...prev, title: e.target.value }))
99+
}
100+
/>
101+
</S.AdminNoticeNav>
102+
<S.AdminNoticeContentWrapper>
103+
<S.AdminNoticeContent
104+
as='textarea'
105+
name='content'
106+
value={form.content}
107+
onChange={(e) =>
108+
setForm((prev) => ({ ...prev, content: e.target.value }))
109+
}
110+
></S.AdminNoticeContent>
111+
</S.AdminNoticeContentWrapper>
112+
<S.AdminNoticeSendButtonWrapper>
113+
<S.AdminNoticeSendButton type='submit'>
114+
제출
115+
</S.AdminNoticeSendButton>
116+
</S.AdminNoticeSendButtonWrapper>
117+
</S.AdminNoticeWrapper>
118+
</S.AdminNoticeForm>
119+
<Modal isOpen={isModalOpen} onClose={handleModalClose}>
120+
{message}
121+
</Modal>
122+
</S.AdminNoticeContainer>
123+
);
124+
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
import { GAP_HEIGHT } from './../../../constants/admin/adminGap';
12
import styled from 'styled-components';
23
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';
4+
import { SearchBarFixedWrapperStyled } from '../../common/admin/searchBar/SearchBar.styled';
35

46
export const SpinnerWrapper = styled(SpinnerWrapperStyled)`
57
width: 100%;
68
`;
79

8-
export const NoticeItemWrapper = styled.section`
10+
export const SearchBarFixedWrapper = styled(SearchBarFixedWrapperStyled)``;
11+
12+
export const NoticeItemContainer = styled.section`
13+
margin-top: calc(
14+
${GAP_HEIGHT.headerTitleTop} + ${GAP_HEIGHT.sectionTop} + 1rem
15+
);
16+
`;
17+
18+
export const NoticeItemWrapper = styled.div`
919
display: flex;
1020
justify-content: center;
1121
`;

src/components/admin/adminNotice/AdminNoticeList.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import * as S from './AdminNoticeList.styled';
33
import { useGetNotice } from '../../../hooks/user/useGetNotice';
44
import Pagination from '../../../components/common/pagination/Pagination';
55
import Spinner from '../../../components/user/mypage/Spinner';
6-
import NoticeItem from '../../../pages/user/customerService/notice/noticeItem/NoticeItem';
76
import useSearchBar from '../../../hooks/admin/useSearchBar';
7+
import NoticeItem from '../../user/customerService/notice/noticeItem/NoticeItem';
88

99
export default function AdminNoticeList() {
1010
const { searchUnit, value, handleGetKeyword, handleChangePagination } =
@@ -25,23 +25,23 @@ export default function AdminNoticeList() {
2525

2626
return (
2727
<>
28-
<SearchBar
29-
onGetKeyword={handleGetKeyword}
30-
value={value}
31-
isNotice={true}
32-
/>
33-
<S.NoticeItemWrapper>
34-
<NoticeItem
35-
noticeData={noticeData.notices}
36-
value={value}
37-
$width='90%'
28+
<S.SearchBarFixedWrapper>
29+
<SearchBar onGetKeyword={handleGetKeyword} value={value} />
30+
</S.SearchBarFixedWrapper>
31+
<S.NoticeItemContainer>
32+
<S.NoticeItemWrapper>
33+
<NoticeItem
34+
noticeData={noticeData.notices}
35+
value={value}
36+
$width='90%'
37+
/>
38+
</S.NoticeItemWrapper>
39+
<Pagination
40+
page={searchUnit.page}
41+
getLastPage={lastPage}
42+
onChangePagination={handleChangePagination}
3843
/>
39-
</S.NoticeItemWrapper>
40-
<Pagination
41-
page={searchUnit.page}
42-
getLastPage={lastPage}
43-
onChangePagination={handleChangePagination}
44-
/>
44+
</S.NoticeItemContainer>
4545
</>
4646
);
4747
}

src/components/common/admin/searchBar/SearchBar.styled.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Link } from 'react-router-dom';
22
import styled from 'styled-components';
3+
import { GAP_HEIGHT } from '../../../../constants/admin/adminGap';
34

45
export const AdminSearchBarContainer = styled.form`
56
width: 100%;
67
display: flex;
78
justify-content: space-evenly;
8-
margin-bottom: 2rem;
9+
margin-bottom: 1rem;
910
`;
1011

1112
export const AdminSearchBarWrapper = styled.div`
@@ -60,3 +61,13 @@ export const WriteLink = styled(Link)`
6061
color: ${({ theme }) => theme.color.navy};
6162
}
6263
`;
64+
65+
export const SearchBarFixedWrapperStyled = styled.div`
66+
max-width: calc(1440px - 19rem);
67+
width: calc(100vw - 19rem);
68+
position: fixed;
69+
top: 0;
70+
padding-top: ${GAP_HEIGHT.headerTitleTop};
71+
background: ${({ theme }) => theme.color.white};
72+
z-index: 10;
73+
`;

src/components/common/admin/searchBar/SearchBar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import { ADMIN_ROUTE } from '../../../../constants/routes';
1010
interface SearchBarProps {
1111
onGetKeyword: (value: string) => void;
1212
value: string;
13-
isNotice?: boolean;
13+
canWrite?: boolean;
1414
}
1515

1616
export default function SearchBar({
1717
onGetKeyword,
1818
value,
19-
isNotice,
19+
canWrite = true,
2020
}: SearchBarProps) {
2121
const [keyword, setKeyword] = useState<string>(value);
2222
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
@@ -75,7 +75,7 @@ export default function SearchBar({
7575
</S.AdminSearchBarInputWrapper>
7676
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
7777
</S.AdminSearchBarWrapper>
78-
{isNotice && (
78+
{canWrite && (
7979
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
8080
작성하기
8181
</S.WriteLink>

src/components/common/admin/sidebar/AdminSidebar.styled.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ export const LayoutContainer = styled.div`
88

99
export const ContainerArea = styled.section`
1010
flex: 1;
11+
width: 100%;
1112
padding: 2rem;
13+
margin-left: 15rem;
1214
`;
1315

1416
export const SidebarContainer = styled.section`
17+
position: fixed;
1518
padding: 1rem;
1619
width: 15rem;
1720
border-right: 1px solid ${({ theme }) => theme.color.grey};

0 commit comments

Comments
 (0)