Skip to content

Commit 0958d33

Browse files
authored
Merge pull request #329 from devpalsPlus/feat/#323
관리자 페이지 공지사항 (#issue 323)
2 parents 02635be + a808d89 commit 0958d33

35 files changed

+835
-77
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { ApiCommonBasicType } from '../../../models/apiCommon';
2+
import type { WriteBody } from '../../../models/customerService';
3+
import { httpClient } from '../../http.api';
4+
5+
export const postNotice = async (formData: WriteBody) => {
6+
try {
7+
await httpClient.post<ApiCommonBasicType>(`/notice`, formData);
8+
} catch (e) {
9+
console.error(e);
10+
throw e;
11+
}
12+
};
13+
14+
export const putNotice = async (id: string, formData: WriteBody) => {
15+
try {
16+
await httpClient.put<ApiCommonBasicType>(`/notice/${id}`, formData);
17+
} catch (e) {
18+
console.error(e);
19+
throw e;
20+
}
21+
};
22+
23+
export const deleteNotice = async (id: string) => {
24+
try {
25+
await httpClient.delete<ApiCommonBasicType>(`/notice/${id}`);
26+
} catch (e) {
27+
console.error(e);
28+
throw e;
29+
}
30+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Link } from 'react-router-dom';
2+
import styled from 'styled-components';
3+
4+
export const AdminSearchBarContainer = styled.form`
5+
width: 100%;
6+
display: flex;
7+
justify-content: space-evenly;
8+
margin-bottom: 2rem;
9+
`;
10+
11+
export const AdminSearchBarWrapper = styled.div`
12+
display: flex;
13+
width: 60%;
14+
`;
15+
16+
export const AdminSearchBarInputWrapper = styled.div`
17+
display: flex;
18+
width: 100%;
19+
justify-content: space-between;
20+
padding: 0.5rem 0.5rem 0.5rem 1rem;
21+
border: 1px solid ${({ theme }) => theme.color.deepGrey};
22+
border-radius: ${({ theme }) => theme.borderRadius.large} 0 0
23+
${({ theme }) => theme.borderRadius.large};
24+
`;
25+
26+
export const AdminSearchBarInput = styled.input`
27+
width: 100%;
28+
font-size: 1.3rem;
29+
`;
30+
31+
export const AdminSearchBarBackIcon = styled.button`
32+
svg {
33+
width: 1.5rem;
34+
}
35+
`;
36+
37+
export const AdminSearchBarButton = styled.button`
38+
width: 15%;
39+
min-width: 5rem;
40+
border: 1px solid ${({ theme }) => theme.color.navy};
41+
background: ${({ theme }) => theme.color.navy};
42+
border-radius: 0 ${({ theme }) => theme.borderRadius.large}
43+
${({ theme }) => theme.borderRadius.large} 0;
44+
font-size: 1.3rem;
45+
color: ${({ theme }) => theme.color.white};
46+
padding: 0.5rem 1rem 0.5rem 0.5rem;
47+
`;
48+
49+
export const WriteLink = styled(Link)`
50+
border: 1px solid ${({ theme }) => theme.color.navy};
51+
background: ${({ theme }) => theme.color.navy};
52+
border-radius: ${({ theme }) => theme.borderRadius.large};
53+
font-size: 1rem;
54+
color: ${({ theme }) => theme.color.white};
55+
padding: 0.5rem 1rem;
56+
transition: all 300ms ease-in-out;
57+
58+
&:hover {
59+
background: ${({ theme }) => theme.color.white};
60+
color: ${({ theme }) => theme.color.navy};
61+
}
62+
`;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { XMarkIcon } from '@heroicons/react/24/outline';
2+
import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../constants/user/customerService';
3+
import * as S from './SearchBar.styled';
4+
import { useState } from 'react';
5+
import { useLocation, useSearchParams } from 'react-router-dom';
6+
import { useModal } from '../../../hooks/useModal';
7+
import Modal from '../../common/modal/Modal';
8+
import { ADMIN_ROUTE } from '../../../constants/routes';
9+
10+
interface SearchBarProps {
11+
onGetKeyword: (keyword: string) => void;
12+
value: string;
13+
}
14+
15+
export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
16+
const [inputValue, setInputValue] = useState<string>('');
17+
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
18+
const [searchParams, setSearchParams] = useSearchParams();
19+
const location = useLocation();
20+
const keyword = inputValue ? inputValue : value;
21+
22+
const handleKeyword = (inputValue: string) => {
23+
const newSearchParams = new URLSearchParams(searchParams);
24+
if (inputValue === '') {
25+
newSearchParams.delete('keyword');
26+
} else {
27+
newSearchParams.set('keyword', inputValue);
28+
}
29+
setSearchParams(newSearchParams);
30+
};
31+
32+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
33+
e.preventDefault();
34+
35+
if (inputValue.trim() === '') {
36+
return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword);
37+
} else {
38+
onGetKeyword(inputValue);
39+
handleKeyword(inputValue);
40+
return;
41+
}
42+
};
43+
44+
const handleChangeKeyword = (e: React.ChangeEvent<HTMLInputElement>) => {
45+
const value = e.target.value;
46+
setInputValue(value);
47+
};
48+
49+
const handleClickSearchDefault = () => {
50+
setInputValue('');
51+
onGetKeyword('');
52+
handleKeyword('');
53+
};
54+
55+
return (
56+
<S.AdminSearchBarContainer onSubmit={handleSubmit}>
57+
<S.AdminSearchBarWrapper>
58+
<S.AdminSearchBarInputWrapper>
59+
<S.AdminSearchBarInput
60+
placeholder={MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword}
61+
value={keyword}
62+
onChange={handleChangeKeyword}
63+
/>
64+
{keyword && (
65+
<S.AdminSearchBarBackIcon
66+
type='button'
67+
aria-label='show all result'
68+
onClick={handleClickSearchDefault}
69+
>
70+
<XMarkIcon />
71+
</S.AdminSearchBarBackIcon>
72+
)}
73+
</S.AdminSearchBarInputWrapper>
74+
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
75+
</S.AdminSearchBarWrapper>
76+
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
77+
작성하기
78+
</S.WriteLink>
79+
<Modal isOpen={isOpen} onClose={handleModalClose}>
80+
{message}
81+
</Modal>
82+
</S.AdminSearchBarContainer>
83+
);
84+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import styled from 'styled-components';
22

33
export const LayoutContainer = styled.div`
4+
max-width: 1440px;
45
height: 100vh;
56
display: flex;
67
`;

src/components/common/admin/title/AdminTitle.styled.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import styled from 'styled-components';
22

3-
export const TitleContainer = styled.header``;
3+
export const TitleContainer = styled.header`
4+
margin-bottom: 1rem;
5+
`;
46

57
export const TitleWrapper = styled.div`
68
margin-bottom: 2rem;

src/components/common/dropDown/DropDown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ interface DropDownProps {
77
children: React.ReactNode;
88
toggleButton: React.ReactNode;
99
isOpen?: boolean;
10-
comment: boolean;
10+
comment?: boolean;
1111
}
1212

1313
const DropDown = ({
1414
children,
1515
toggleButton,
1616
isOpen = false,
17-
comment,
17+
comment = false,
1818
...props
1919
}: DropDownProps) => {
2020
const [open, setOpen] = useState(isOpen);

src/components/user/customerService/CustomerServiceHeader.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ interface CustomerServiceHeaderProps {
1515

1616
export default function CustomerServiceHeader({
1717
title,
18-
keyword,
18+
keyword: value,
1919
onGetKeyword,
2020
}: CustomerServiceHeaderProps) {
2121
const [inputValue, setInputValue] = useState<string>('');
2222
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
2323
const [searchParams, setSearchParams] = useSearchParams();
24+
const keyword = value ? value : inputValue;
2425

2526
const handleKeyword = (inputValue: string) => {
2627
const newSearchParams = new URLSearchParams(searchParams);
@@ -64,7 +65,7 @@ export default function CustomerServiceHeader({
6465
<S.SearchBarInput
6566
type='text'
6667
placeholder='궁금한 내용을 검색해보세요.'
67-
value={keyword ? keyword : inputValue}
68+
value={keyword}
6869
onChange={handleChangeValue}
6970
/>
7071
<S.ButtonWrapper>

src/components/user/customerService/noticeDetail/NoticeDetailBundle.styled.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { SpinnerWrapperStyled } from '../../mypage/Spinner.styled';
33

44
export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;
55

6-
export const Container = styled.section`
7-
width: 75%;
6+
export const Container = styled.section<{ $width: string }>`
7+
width: ${({ $width }) => $width};
88
margin: 0 auto;
99
margin-bottom: 2rem;
1010
`;

src/components/user/customerService/noticeDetail/NoticeDetailBundle.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import NoticeDetailHeader from './header/NoticeDetailHeader';
77
import Spinner from '../../mypage/Spinner';
88
import ListButton from './bottom/button/ListButton';
99

10-
export default function NoticeDetailBundle() {
10+
interface NoticeDetailBundleProps {
11+
$width: string;
12+
}
13+
14+
export default function NoticeDetailBundle({
15+
$width,
16+
}: NoticeDetailBundleProps) {
1117
const location = useLocation();
1218
const { noticeId } = useParams();
1319
const id = noticeId || String(location.state.id);
1420
const keyword = location.state?.keyword ?? '';
21+
const includesAdmin = location.pathname.includes('admin') ?? false;
1522

1623
const { noticeDetail: noticeDetailData, isLoading } = useGetNoticeDetail(id);
1724

@@ -25,7 +32,7 @@ export default function NoticeDetailBundle() {
2532

2633
if (!noticeDetailData) {
2734
return (
28-
<S.Container>
35+
<S.Container $width={$width}>
2936
<NoticeDetailContent
3037
id={0}
3138
title='해당 공지사항을 찾을 수 없습니다.'
@@ -49,8 +56,8 @@ export default function NoticeDetailBundle() {
4956
} = noticeDetailData;
5057

5158
return (
52-
<S.Container>
53-
<NoticeDetailHeader />
59+
<S.Container $width={$width}>
60+
{!includesAdmin && <NoticeDetailHeader />}
5461
<NoticeDetailContent
5562
id={detailId}
5663
title={title}

src/components/user/customerService/noticeDetail/bottom/button/ListButton.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ROUTES } from '../../../../../../constants/routes';
1+
import { useLocation } from 'react-router-dom';
2+
import { ADMIN_ROUTE, ROUTES } from '../../../../../../constants/routes';
23
import ContentBorder from '../../../../../common/contentBorder/ContentBorder';
34
import * as S from './ListButton.styled';
45

@@ -7,14 +8,20 @@ interface ListButtonProps {
78
}
89

910
export default function ListButton({ keyword }: ListButtonProps) {
11+
const location = useLocation();
12+
const includesAdmin = location.pathname.includes('admin') ?? false;
1013
const isKeyword = keyword ? `?keyword=${keyword}` : ``;
1114

1215
return (
1316
<>
1417
<ContentBorder />
1518
<S.ListWrapper>
1619
<S.ListLink
17-
to={`${ROUTES.customerService}/${ROUTES.notice}${isKeyword}`}
20+
to={
21+
includesAdmin
22+
? `${ADMIN_ROUTE.admin}/${ADMIN_ROUTE.notice}${isKeyword}`
23+
: `${ROUTES.customerService}/${ROUTES.notice}${isKeyword}`
24+
}
1825
>
1926
<S.ListTitle>목록</S.ListTitle>
2027
</S.ListLink>

0 commit comments

Comments
 (0)