Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
37 changes: 37 additions & 0 deletions src/api/admin/customerService/Notice.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ApiCommonBasicType } from '../../../models/apiCommon';
import { httpClient } from '../../http.api';

export const postNotice = async (formData: FormData) => {
try {
await httpClient.post<ApiCommonBasicType>(`/notice`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
} catch (e) {
console.error(e);
throw e;
}
};

export const putNotice = async (id: number, formData: FormData) => {
try {
await httpClient.put<ApiCommonBasicType>(`/notice/${id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
} catch (e) {
console.error(e);
throw e;
}
};

export const deleteNotice = async (id: number) => {
try {
await httpClient.delete<ApiCommonBasicType>(`/notice/${id}`);
} catch (e) {
console.error(e);
throw e;
}
};
22 changes: 14 additions & 8 deletions src/api/alarm.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ApiAlarmList } from '../models/alarm';
import useAuthStore from '../store/authStore';
import { httpClient } from './http.api';

export const getAlarmList = async () => {
Expand Down Expand Up @@ -36,14 +37,19 @@ export const patchAlarm = async (id: number) => {
};

export const testLiveAlarm = async () => {
try {
const response = await httpClient.get<ApiAlarmList>(
`/user/send-alarm?alarmFilter=0`
);
const { accessToken } = useAuthStore.getState();
if (accessToken) {
try {
const response = await httpClient.get<ApiAlarmList>(
'/user/send-alarm?alarmFilter=0'
);

return response;
} catch (e) {
console.error(e);
throw e;
return response;
} catch (e) {
console.error(e);
throw e;
}
} else {
return;
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

함수 반환 타입 명시와 일관된 에러 처리 개선 필요

인증 토큰 체크 로직 추가는 보안상 좋은 개선이지만, 몇 가지 개선사항이 있습니다:

  1. 함수 반환 타입이 명시되지 않아 타입 안정성이 떨어집니다
  2. 토큰이 없을 때 undefined 반환보다는 명시적인 에러나 null 반환을 고려해보세요
  3. 다른 API 함수들과 에러 처리 방식의 일관성을 유지해야 합니다

다음과 같이 개선하는 것을 권장합니다:

-export const testLiveAlarm = async () => {
+export const testLiveAlarm = async (): Promise<AxiosResponse<ApiAlarmList> | null> => {
  const { accessToken } = useAuthStore.getState();
  if (accessToken) {
    try {
      const response = await httpClient.get<ApiAlarmList>(
        '/user/send-alarm?alarmFilter=0'
      );

      return response;
    } catch (e) {
      console.error(e);
      throw e;
    }
  } else {
-    return;
+    return null;
  }
};
🤖 Prompt for AI Agents
In src/api/alarm.api.ts around lines 39 to 55, the testLiveAlarm function lacks
an explicit return type, returns undefined when no accessToken is present, and
has inconsistent error handling compared to other API functions. To fix this,
explicitly declare the function's return type, replace the undefined return with
a null or a specific error to clearly indicate the absence of a token, and align
the error handling pattern with other API functions by possibly removing the
try-catch block here or handling errors uniformly.

61 changes: 61 additions & 0 deletions src/components/admin/searchBar/SearchBar.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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%;
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
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
7 changes: 7 additions & 0 deletions src/constants/admin/adminModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const ADMIN_MODAL_MESSAGE = {
writeSuccess: '성공적으로 작성되었습니다.',
writeFail: '작성이 실패하였습니다.',
writeDeleteSuccess: '삭제되었습니다.',
writeDeleteFail: '삭제가 실패하였습니다.',
writeError: '알수없는 에러가 발생했습니다.',
};
2 changes: 2 additions & 0 deletions src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ export const ADMIN_ROUTE = {
reports: 'reports',
inquiries: 'inquiries',
manage: 'manage',
detail: 'detail',
write: 'write',
};
Loading