Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
29 changes: 28 additions & 1 deletion src/api/customerService.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { ApiFAQ, SearchKeyword } from '../models/customerService';
import type {
ApiFAQ,
ApiNotice,
ApiNoticeDetail,
SearchKeyword,
} from '../models/customerService';
import { httpClient } from './http.api';

export const getFAQ = async (params: SearchKeyword) => {
Expand All @@ -11,3 +16,25 @@ export const getFAQ = async (params: SearchKeyword) => {
throw e;
}
};

export const getNotice = async (params: SearchKeyword) => {
try {
const response = await httpClient.get<ApiNotice>(`/notice`, { params });

return response.data.data;
} catch (e) {
console.error(e);
throw e;
}
};

export const getNoticeDetail = async (id: string) => {
try {
const response = await httpClient.get<ApiNoticeDetail>(`/notice/${id}`);

return response.data.data;
} catch (e) {
console.error(e);
throw e;
}
};
4 changes: 2 additions & 2 deletions src/components/customerService/inquiry/Inquiry.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export const CategorySelect = styled.button<{ $isCategoryOpen: boolean }>`
height: 1.3rem;
transition: transform 300ms ease-in-out;
transform: rotate(0deg);
${({ $isOpen }) =>
$isOpen &&
${({ $isCategoryOpen }) =>
$isCategoryOpen &&
css`
transform: rotate(180deg);
`}
Expand Down
26 changes: 26 additions & 0 deletions src/components/customerService/notice/NoticeList.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from 'styled-components';

export const Container = styled.nav`
width: 100%;
`;

export const Wrapper = styled.button`
width: 100%;
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;

&:hover {
background: ${({ theme }) => theme.color.lightgrey};
}
`;

export const Title = styled.span`
font-size: 1.2rem;
font-weight: 700;
`;

export const Date = styled.span`
font-size: 1.1rem;
`;
18 changes: 18 additions & 0 deletions src/components/customerService/notice/NoticeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Notice } from '../../../models/customerService';
import { formatDate } from '../../../util/format';
import * as S from './NoticeList.styled';

interface NoticeProps {
list: Notice;
}

export default function NoticeList({ list }: NoticeProps) {
return (
<S.Container>
<S.Wrapper>
<S.Title>{list.title}</S.Title>
<S.Date>{formatDate(list.createdAt)}</S.Date>
</S.Wrapper>
</S.Container>
);
}
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. props 이름을 list에서 notice로 변경
  2. createdAt 값이 없는 경우를 대비한 오류 처리 추가
-export default function NoticeList({ list }: NoticeProps) {
+export default function NoticeList({ notice }: NoticeProps) {
  return (
    <S.Container>
      <S.Wrapper>
-        <S.Title>{list.title}</S.Title>
-        <S.Date>{formatDate(list.createdAt)}</S.Date>
+        <S.Title>{notice.title}</S.Title>
+        <S.Date>{notice.createdAt ? formatDate(notice.createdAt) : '-'}</S.Date>
      </S.Wrapper>
    </S.Container>
  );
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from 'styled-components';

export const SpinnerWrapper = styled.div`
height: 60vh;
`;

export const Container = styled.div`
width: 75%;
margin: 0 auto;
`;
40 changes: 40 additions & 0 deletions src/components/customerService/noticeDetail/NoticeDetailBundle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useLocation, useParams } from 'react-router-dom';
import { useGetNoticeDetail } from '../../../hooks/useGetNoticeDetail';
import * as S from './NoticeDetailBundle.styled';
import NoticeDetailBottom from './bottom/NoticeDetailBottom';
import NoticeDetailContent from './content/NoticeDetailContent';
import NoticeDetailHeader from './header/NoticeDetailHeader';
import Spinner from '../../mypage/Spinner';

export default function NoticeDetailBundle() {
const location = useLocation();
const { noticeId } = useParams();

const id = noticeId || String(location.state && location.state.id);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

옵셔널 체이닝을 사용하여 코드를 개선하세요.

현재 코드에서는 location.state && location.state.id와 같은 방식으로 nullable 값을 처리하고 있습니다. 옵셔널 체이닝을 사용하면 더 간결하고 가독성이 높은 코드를 작성할 수 있습니다.

다음과 같이 수정해보세요:

-  const id = noticeId || String(location.state && location.state.id);
+  const id = noticeId || String(location.state?.id);
🧰 Tools
🪛 Biome (1.9.4)

[error] 13-13: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID 매개변수의 유효성 검사가 필요합니다.

현재 코드에서는 ID가 없는 경우에 대한 처리가 부족합니다. noticeIdlocation.state.id 모두 없는 경우, API 호출이 빈 문자열이나 "undefined" 문자열로 실행될 수 있으며 이는 오류를 발생시킬 수 있습니다.

다음과 같이 ID의 유효성을 검사하는 코드를 추가하세요:

  const { noticeId } = useParams();
-  const id = noticeId || String(location.state && location.state.id);
+  const stateId = location.state?.id;
+  const id = noticeId || (stateId ? String(stateId) : '');
+
+  // ID가 없는 경우 처리
+  if (!id) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항 ID가 유효하지 않습니다.</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const id = noticeId || String(location.state && location.state.id);
const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
const location = useLocation();
const { noticeId } = useParams();
- const id = noticeId || String(location.state && location.state.id);
+ const stateId = location.state?.id;
+ const id = noticeId || (stateId ? String(stateId) : '');
+
+ // ID가 없는 경우 처리
+ if (!id) {
+ return (
+ <S.ErrorWrapper>
+ <p>공지사항 ID가 유효하지 않습니다.</p>
+ <S.BackLink to="/customer-service/notice">
+ 목록으로 돌아가기
+ </S.BackLink>
+ </S.ErrorWrapper>
+ );
+ }
const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
🧰 Tools
🪛 Biome (1.9.4)

[error] 13-13: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


if (!noticeDetailData) return;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

ID의 유효성 검사와 오류 처리가 필요합니다.

현재 코드에서는 id가 없는 경우나 API 오류 발생 시 처리 로직이 누락되어 있습니다. 사용자가 URL을 통해 직접 접근할 경우 stateundefined일 수 있으며, API 호출 중 오류가 발생할 수 있습니다.

다음과 같이 ID 유효성 검사 및 오류 처리 로직을 추가하세요:

  const { noticeId } = useParams();
-  const id = noticeId || String(location.state && location.state.id);
+  const stateId = location.state?.id;
+  const id = noticeId || (stateId ? String(stateId) : '');
-  const { noticeDetailData, isLoading } = useGetNoticeDetail(id);
+  const { noticeDetailData, isLoading, isError, error } = useGetNoticeDetail(id);
+
+  // ID가 없는 경우 처리
+  if (!id) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항 ID가 유효하지 않습니다.</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }
+
+  // API 오류 처리
+  if (isError) {
+    return (
+      <S.ErrorWrapper>
+        <p>공지사항을 불러오는 중 오류가 발생했습니다.</p>
+        <p>{error instanceof Error ? error.message : '다시 시도해주세요.'}</p>
+        <S.BackLink to="/customer-service/notice">목록으로 돌아가기</S.BackLink>
+      </S.ErrorWrapper>
+    );
+  }

관련 스타일 컴포넌트를 NoticeDetailBundle.styled.ts에 추가해야 합니다:

export const ErrorWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 300px;
  gap: 1rem;
`;

export const BackLink = styled(Link)`
  padding: 0.5rem 1rem;
  background-color: #f0f0f0;
  border-radius: 4px;
  text-decoration: none;
  color: #333;
  
  &:hover {
    background-color: #e0e0e0;
  }
`;
🧰 Tools
🪛 Biome (1.9.4)

[error] 12-12: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


if (isLoading) {
return (
<S.SpinnerWrapper>
<Spinner />
</S.SpinnerWrapper>
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

오류 상태 처리가 누락되었습니다.

데이터가 없거나 로딩 중인 상태는 처리되어 있으나, API 요청 중 오류가 발생한 경우에 대한 처리가 없습니다.

다음과 같이 오류 상태를 처리하는 코드를 추가하세요:

-const { noticeDetailData, isLoading } = useGetNoticeDetail(String(id));
+const { noticeDetailData, isLoading, isError, error } = useGetNoticeDetail(String(id));

 if (!noticeDetailData) return;

+if (isError) {
+  return (
+    <S.ErrorWrapper>
+      <p>공지사항을 불러오는 중 오류가 발생했습니다.</p>
+      <p>{error instanceof Error ? error.message : '다시 시도해주세요.'}</p>
+    </S.ErrorWrapper>
+  );
+}
+
 if (isLoading) {
   return (
     <S.SpinnerWrapper>

그리고 관련 스타일 컴포넌트를 NoticeDetailBundle.styled.ts에 추가해야 합니다.


const { title, content, createdAt, prev, next } = noticeDetailData;

return (
<S.Container>
<NoticeDetailHeader />
<NoticeDetailContent
title={title}
content={content}
createdAt={createdAt}
/>
<NoticeDetailBottom prev={prev} next={next} />
</S.Container>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Link } from 'react-router-dom';
import styled from 'styled-components';

export const Container = styled.nav``;

export const NotOtherNotice = styled.div`
padding: 0.5rem 1rem;
font-size: 0.9rem;
`;

export const ListWrapper = styled.div`
width: 100%;
display: flex;
justify-content: center;
margin-top: 1.5rem;
`;

export const ListLink = styled(Link)`
display: flex;
justify-content: center;
align-items: center;
font-size: 0.9rem;
background: ${({ theme }) => theme.color.navy};
border-radius: ${({ theme }) => theme.borderRadius.large};
color: ${({ theme }) => theme.color.white};
border: 1px solid ${({ theme }) => theme.color.navy};
padding: 0.5rem 1rem;

&:hover {
background: ${({ theme }) => theme.color.lightgrey};
color: ${({ theme }) => theme.color.navy};
border: 1px solid ${({ theme }) => theme.color.navy};
transition: all 0.3s ease-in-out;
}
`;

export const ListTitle = styled.span``;

export const ContentBorder = styled.div`
width: 100%;
height: 0.5px;
background: ${({ theme }) => theme.color.placeholder};
position: relative;
z-index: 1;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ROUTES } from '../../../../constants/routes';
import type { OtherNotice } from '../../../../models/customerService';
import OtherNoticeButton from './button/OtherNoticeButton';
import * as S from './NoticeDetailBottom.styled';

interface NoticeDetailBottomProps {
prev: OtherNotice | null;
next: OtherNotice | null;
}

export default function NoticeDetailBottom({
prev,
next,
}: NoticeDetailBottomProps) {
return (
<S.Container>
<S.ContentBorder></S.ContentBorder>
{prev !== null ? (
<OtherNoticeButton
navigation='이전'
id={prev.id}
title={prev.title}
createdAt={prev.createdAt}
/>
) : (
<S.NotOtherNotice>이전 공지사항이 없습니다.</S.NotOtherNotice>
)}
<S.ContentBorder></S.ContentBorder>
{next !== null ? (
<OtherNoticeButton
navigation='다음'
id={next.id}
title={next.title}
createdAt={next.createdAt}
/>
) : (
<S.NotOtherNotice>다음 공지사항이 없습니다.</S.NotOtherNotice>
)}
<S.ContentBorder></S.ContentBorder>
<S.ListWrapper>
<S.ListLink to={`${ROUTES.customerService}/${ROUTES.notice}`}>
<S.ListTitle>목록</S.ListTitle>
</S.ListLink>
</S.ListWrapper>
</S.Container>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import styled from 'styled-components';

export const OtherNoticeLink = styled(Link)`
width: 100%;
display: flex;
padding: 0.5rem 1rem;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;

&:hover {
background: ${({ theme }) => theme.color.lightgrey};
}
`;

export const OtherNoticeWrapper = styled.div`
display: flex;
gap: 1rem;
`;

export const OtherNotice = styled.span`
font-weight: 600;
`;

export const OtherNoticeTitle = styled.span``;

export const OtherNoticeDate = styled.span`
font-size: 0.8rem;
color: ${({ theme }) => theme.color.placeholder};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ROUTES } from '../../../../../constants/routes';
import { OtherNotice } from '../../../../../models/customerService';
import { formatDate } from '../../../../../util/format';
import * as S from './OtherNoticeButton.styled';

interface OtherNoticeButtonProps extends OtherNotice {
navigation: string;
}

export default function OtherNoticeButton({
navigation,
id,
title,
createdAt,
}: OtherNoticeButtonProps) {
return (
<S.OtherNoticeLink
to={`${ROUTES.customerService}/${ROUTES.noticeDetail}/${id}`}
state={{ id }}
>
<S.OtherNoticeWrapper>
<S.OtherNotice>{navigation}</S.OtherNotice>
<S.OtherNoticeTitle>{title}</S.OtherNoticeTitle>
</S.OtherNoticeWrapper>
<S.OtherNoticeDate>{formatDate(createdAt)}</S.OtherNoticeDate>
</S.OtherNoticeLink>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import styled from 'styled-components';

export const Container = styled.section`
width: 100%;
margin: 2rem 0;
`;

export const TitleWrapper = styled.div`
padding: 1rem;
`;

export const Title = styled.h2``;

export const AdminWrapper = styled.div`
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.2rem;
`;

export const AdminImg = styled.img`
width: 2rem;
height: 2rem;
`;

export const Admin = styled.span`
font-size: 1.1rem;
`;

export const InfoWrapper = styled.div`
display: flex;
align-items: center;
gap: 1rem;
`;

export const NoticeContentDate = styled.span`
font-size: 0.8rem;
`;

export const ViewWrapper = styled.div``;

export const ViewIcon = styled.div`
display: flex;
svg {
width: 1rem;
height: 1rem;
}
`;

export const ViewCount = styled.span`
font-size: 0.8rem;
`;

export const ContentWrapper = styled.div`
padding: 1.5rem 1rem;
`;

export const Content = styled.p``;

export const ContentBorder = styled.div`
width: 100%;
height: 0.5px;
background: ${({ theme }) => theme.color.placeholder};
position: relative;
z-index: 1;
`;
Loading