Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/api/admin/customerService/Notice.api.ts
Original file line number Diff line number Diff line change
@@ -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<ApiCommonBasicType>(`/notice`, formData);
} catch (e) {
console.error(e);
throw e;
}
};

export const putNotice = async (id: string, formData: WriteBody) => {
try {
await httpClient.put<ApiCommonBasicType>(`/notice/${id}`, formData);
} catch (e) {
console.error(e);
throw e;
}
};

export const deleteNotice = async (id: string) => {
try {
await httpClient.delete<ApiCommonBasicType>(`/notice/${id}`);
} catch (e) {
console.error(e);
throw e;
}
};
2 changes: 1 addition & 1 deletion src/api/customerService.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ export const getNoticeDetail = async (id: string) => {
return response.data.data;
} catch (e) {
console.error(e);
throw e;
// throw e;
}
};
62 changes: 62 additions & 0 deletions src/components/admin/searchBar/SearchBar.styled.ts
Original file line number Diff line number Diff line change
@@ -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};
}
`;
82 changes: 82 additions & 0 deletions src/components/admin/searchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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: (value: string) => void;
value: string;
}

export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
const [keyword, setKeyword] = useState<string>('');
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
const [searchParams, setSearchParams] = useSearchParams();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

상태 관리 구조를 개선해야 합니다.

컴포넌트가 로컬 keyword 상태와 외부 value prop을 모두 관리하고 있어 동기화 문제가 발생할 수 있습니다. 단일 진실 원칙(Single Source of Truth)을 위반합니다.

다음 중 하나의 방식으로 수정하는 것을 권장합니다:

방법 1: 완전한 제어 컴포넌트

-  const [keyword, setKeyword] = useState<string>('');
+  // keyword 상태 제거하고 value prop만 사용

방법 2: 로컬 상태만 사용

interface SearchBarProps {
-  onGetKeyword: (value: string) => void;
-  value: string;
+  onGetKeyword: (value: string) => void;
+  initialValue?: string;
}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/admin/searchBar/SearchBar.tsx around lines 14 to 17, the
component manages both a local keyword state and an external value prop, causing
potential synchronization issues and violating the single source of truth
principle. To fix this, either convert the component into a fully controlled
component by removing the local keyword state and relying solely on the value
prop and onGetKeyword callback, or remove the value prop and onGetKeyword
callback and manage the keyword state locally within the component only.

const location = useLocation();

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<HTMLFormElement>) => {
e.preventDefault();

if (keyword.trim() === '') {
return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword);
} else {
onGetKeyword(keyword);
handleKeyword(keyword);
return;
}
};

const handleChangeKeyword = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setKeyword(value);
};

const handleClickSearchDefault = () => {
setKeyword('');
onGetKeyword('');
handleKeyword('');
};

return (
<S.AdminSearchBarContainer onSubmit={handleSubmit}>
<S.AdminSearchBarWrapper>
<S.AdminSearchBarInputWrapper>
<S.AdminSearchBarInput
placeholder={MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword}
value={keyword ? keyword : value}
onChange={handleChangeKeyword}
/>
{value && (
<S.AdminSearchBarBackIcon
type='button'
onClick={handleClickSearchDefault}
>
<XMarkIcon />
</S.AdminSearchBarBackIcon>
)}
</S.AdminSearchBarInputWrapper>
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
</S.AdminSearchBarWrapper>
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
작성하기
</S.WriteLink>
<Modal isOpen={isOpen} onClose={handleModalClose}>
{message}
</Modal>
</S.AdminSearchBarContainer>
);
}
1 change: 1 addition & 0 deletions src/components/common/admin/sidebar/AdminSidebar.styled.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from 'styled-components';

export const LayoutContainer = styled.div`
max-width: 1440px;
height: 100vh;
display: flex;
`;
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/admin/title/AdminTitle.styled.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/dropDown/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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

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

return (
<S.Container>
<NoticeDetailHeader />
<S.Container $width={$width}>
{!includesAdmin && <NoticeDetailHeader />}
<NoticeDetailContent
id={detailId}
title={title}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ROUTES } from '../../../../../../constants/routes';
import { useLocation } from 'react-router-dom';
import { ADMIN_ROUTE, ROUTES } from '../../../../../../constants/routes';
import ContentBorder from '../../../../../common/contentBorder/ContentBorder';
import * as S from './ListButton.styled';

Expand All @@ -7,14 +8,20 @@ interface ListButtonProps {
}

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

return (
<>
<ContentBorder />
<S.ListWrapper>
<S.ListLink
to={`${ROUTES.customerService}/${ROUTES.notice}${isKeyword}`}
to={
includesAdmin
? `${ADMIN_ROUTE.admin}/${ADMIN_ROUTE.notice}${isKeyword}`
: `${ROUTES.customerService}/${ROUTES.notice}${isKeyword}`
}
>
<S.ListTitle>목록</S.ListTitle>
</S.ListLink>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom';
import styled from 'styled-components';

export const Container = styled.div`
Expand Down Expand Up @@ -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;
`;
Expand Down
Loading