Skip to content
Merged
47 changes: 47 additions & 0 deletions src/api/admin/customerService/FAQ.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ApiCommonBasicType } from '../../../models/apiCommon';
import type { ApiFAQDetail, WriteBody } from '../../../models/customerService';
import { httpClient } from '../../http.api';

export const getFAQDetail = async (id: string) => {
try {
const response = await httpClient.get<ApiFAQDetail>(`/faq/${id}`);

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

export const postFAQ = async (formData: WriteBody) => {
try {
await httpClient.post<ApiCommonBasicType>(`faq`, formData);
} catch (e) {
console.error(e);
throw e;
}
};

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

export const deleteFAQ = async (id: string) => {
try {
await httpClient.delete<ApiCommonBasicType>(`faq/${id}`);
} catch (e) {
console.error(e);
throw e;
}
};
10 changes: 5 additions & 5 deletions src/api/auth.api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {
import type {
ApiGetAllUsers,
ApiGetAllUsersPreview,
type ApiOauth,
type ApiVerifyNickname,
type VerifyEmail,
ApiOauth,
ApiVerifyNickname,
VerifyEmail,
} from '../models/auth';
import { httpClient } from './http.api';
import { loginFormValues } from '../pages/login/Login';
import { registerFormValues } from '../pages/user/register/Register';
import { changePasswordFormValues } from '../pages/user/changePassword/ChangePassword';
import { type SearchType } from '../models/search';
import type { SearchType } from '../models/search';

export const postVerificationEmail = async (email: string) => {
try {
Expand Down
12 changes: 12 additions & 0 deletions src/components/admin/adminFAQ/AdminFAQList.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import styled from 'styled-components';
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';
import { SearchBarFixedWrapperStyled } from '../../common/admin/searchBar/SearchBar.styled';
import { GAP_HEIGHT } from '../../../constants/admin/adminGap';

export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;

export const SearchBarFixedWrapper = styled(SearchBarFixedWrapperStyled)``;

export const FAQItemWrapper = styled.div`
margin-top: calc(${GAP_HEIGHT.headerTitleTop} + ${GAP_HEIGHT.sectionTop});
`;
33 changes: 33 additions & 0 deletions src/components/admin/adminFAQ/AdminFAQList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as S from './AdminFAQList.styled';
import SearchBar from '../../../components/common/admin/searchBar/SearchBar';
import FAQItem from '../../user/customerService/faq/FAQItem';
import { useGetFAQ } from '../../../hooks/user/useGetFAQ';
import Spinner from '../../user/mypage/Spinner';
import useSearchBar from '../../../hooks/admin/useSearchBar';

export default function AdminFAQList() {
const { searchUnit, value, handleGetKeyword } = useSearchBar();
const keyword = searchUnit.keyword;
const { faqData, isLoading } = useGetFAQ({ keyword });

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

if (!faqData) return;

return (
<>
<S.SearchBarFixedWrapper>
<SearchBar onGetKeyword={handleGetKeyword} value={value} />
</S.SearchBarFixedWrapper>
<S.FAQItemWrapper>
<FAQItem faqData={faqData} $isAdmin={true} />
</S.FAQItemWrapper>
</>
);
}
124 changes: 124 additions & 0 deletions src/components/admin/adminFAQ/AdminFAQWrite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { INQUIRY_MESSAGE } from '../../../constants/user/customerService';
import * as S from './../../admin/adminNotice/AdminNoticeWrite.styled';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useModal } from '../../../hooks/useModal';
import Modal from '../../../components/common/modal/Modal';
import type { WriteBody } from '../../../models/customerService';
import Spinner from '../../../components/user/mypage/Spinner';
import { useAdminFAQ } from '../../../hooks/admin/useAdminFAQ';

export default function AdminFAQWrite() {
const location = useLocation();
const {
isOpen: isModalOpen,
message,
handleModalOpen,
handleModalClose,
} = useModal();
const pathname = location.state?.from || '';
const id = location.state?.id || '';

const formDefault = () => {
setForm({
title: '',
content: '',
});
};

const { getFAQDetailData, postFAQMutate, putFAQMutate } = useAdminFAQ({
handleModalOpen,
formDefault,
pathname,
id,
});
const [form, setForm] = useState<WriteBody>({
title: '',
content: '',
});
const { data: FAQDetailData, isLoading } = getFAQDetailData;

useEffect(() => {
if (!FAQDetailData) return;
setForm({ title: FAQDetailData.title, content: FAQDetailData.content });
}, [FAQDetailData]);

const handleSubmitInquiry = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const isValid = {
title: form.title.trim() !== '',
content: form.content.trim() !== '',
};

if (!isValid.title) {
return handleModalOpen(INQUIRY_MESSAGE.writeTitle);
}
if (!isValid.content) {
return handleModalOpen(INQUIRY_MESSAGE.writeContent);
}

const formData = new FormData(e.currentTarget as HTMLFormElement);

const formDataObj: WriteBody = {
title: formData.get('title') as string,
content: formData.get('content') as string,
};

if (!id) {
return postFAQMutate.mutate(formDataObj);
} else {
return putFAQMutate.mutate({ id, formDataObj });
}
};

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

return (
<S.AdminNoticeContainer>
<S.AdminNoticeForm
onSubmit={handleSubmitInquiry}
method='post'
encType='multipart/form-data'
>
<S.AdminNoticeWrapper>
<S.AdminNoticeNav>
<S.AdminNoticeInputTitle
name='title'
type='text'
placeholder='제목을 입력하세요.'
value={form.title}
onChange={(e) =>
setForm((prev) => ({ ...prev, title: e.target.value }))
}
/>
</S.AdminNoticeNav>
<S.AdminNoticeContentWrapper>
<S.AdminNoticeContent
as='textarea'
name='content'
value={form.content}
onChange={(e) =>
setForm((prev) => ({ ...prev, content: e.target.value }))
}
></S.AdminNoticeContent>
</S.AdminNoticeContentWrapper>
<S.AdminNoticeSendButtonWrapper>
<S.AdminNoticeSendButton type='submit'>
제출
</S.AdminNoticeSendButton>
</S.AdminNoticeSendButtonWrapper>
</S.AdminNoticeWrapper>
</S.AdminNoticeForm>
<Modal isOpen={isModalOpen} onClose={handleModalClose}>
{message}
</Modal>
</S.AdminNoticeContainer>
);
}
12 changes: 11 additions & 1 deletion src/components/admin/adminNotice/AdminNoticeList.styled.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { GAP_HEIGHT } from './../../../constants/admin/adminGap';
import styled from 'styled-components';
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';
import { SearchBarFixedWrapperStyled } from '../../common/admin/searchBar/SearchBar.styled';

export const SpinnerWrapper = styled(SpinnerWrapperStyled)`
width: 100%;
`;

export const NoticeItemWrapper = styled.section`
export const SearchBarFixedWrapper = styled(SearchBarFixedWrapperStyled)``;

export const NoticeItemContainer = styled.section`
margin-top: calc(
${GAP_HEIGHT.headerTitleTop} + ${GAP_HEIGHT.sectionTop} + 1rem
);
`;

export const NoticeItemWrapper = styled.div`
display: flex;
justify-content: center;
`;
34 changes: 17 additions & 17 deletions src/components/admin/adminNotice/AdminNoticeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import * as S from './AdminNoticeList.styled';
import { useGetNotice } from '../../../hooks/user/useGetNotice';
import Pagination from '../../../components/common/pagination/Pagination';
import Spinner from '../../../components/user/mypage/Spinner';
import NoticeItem from '../../../pages/user/customerService/notice/noticeItem/NoticeItem';
import useSearchBar from '../../../hooks/admin/useSearchBar';
import NoticeItem from '../../user/customerService/notice/noticeItem/NoticeItem';

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

return (
<>
<SearchBar
onGetKeyword={handleGetKeyword}
value={value}
isNotice={true}
/>
<S.NoticeItemWrapper>
<NoticeItem
noticeData={noticeData.notices}
value={value}
$width='90%'
<S.SearchBarFixedWrapper>
<SearchBar onGetKeyword={handleGetKeyword} value={value} />
</S.SearchBarFixedWrapper>
<S.NoticeItemContainer>
<S.NoticeItemWrapper>
<NoticeItem
noticeData={noticeData.notices}
value={value}
$width='90%'
/>
</S.NoticeItemWrapper>
<Pagination
page={searchUnit.page}
getLastPage={lastPage}
onChangePagination={handleChangePagination}
/>
</S.NoticeItemWrapper>
<Pagination
page={searchUnit.page}
getLastPage={lastPage}
onChangePagination={handleChangePagination}
/>
</S.NoticeItemContainer>
</>
);
}
13 changes: 12 additions & 1 deletion src/components/common/admin/searchBar/SearchBar.styled.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { GAP_HEIGHT } from '../../../../constants/admin/adminGap';

export const AdminSearchBarContainer = styled.form`
width: 100%;
display: flex;
justify-content: space-evenly;
margin-bottom: 2rem;
margin-bottom: 1rem;
`;

export const AdminSearchBarWrapper = styled.div`
Expand Down Expand Up @@ -60,3 +61,13 @@ export const WriteLink = styled(Link)`
color: ${({ theme }) => theme.color.navy};
}
`;

export const SearchBarFixedWrapperStyled = styled.div`
max-width: calc(1440px - 19rem);
width: calc(100vw - 19rem);
position: fixed;
top: 0;
padding-top: ${GAP_HEIGHT.headerTitleTop};
background: ${({ theme }) => theme.color.white};
z-index: 10;
`;
6 changes: 3 additions & 3 deletions src/components/common/admin/searchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { ADMIN_ROUTE } from '../../../../constants/routes';
interface SearchBarProps {
onGetKeyword: (value: string) => void;
value: string;
isNotice?: boolean;
canWrite?: boolean;
}

export default function SearchBar({
onGetKeyword,
value,
isNotice,
canWrite = true,
}: SearchBarProps) {
const [keyword, setKeyword] = useState<string>(value);
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function SearchBar({
</S.AdminSearchBarInputWrapper>
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
</S.AdminSearchBarWrapper>
{isNotice && (
{canWrite && (
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
작성하기
</S.WriteLink>
Expand Down
3 changes: 3 additions & 0 deletions src/components/common/admin/sidebar/AdminSidebar.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ export const LayoutContainer = styled.div`

export const ContainerArea = styled.section`
flex: 1;
width: 100%;
padding: 2rem;
margin-left: 15rem;
`;

export const SidebarContainer = styled.section`
position: fixed;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

사이드바와 헤더 너비 불일치 문제가 있습니다.

사이드바 너비는 15rem인데 AdminTitle.styled.ts에서는 20rem을 가정하고 있습니다 (calc(100vw - 20rem)). 이로 인해 레이아웃 불일치가 발생할 수 있습니다.

다음 중 하나로 수정해주세요:

# 옵션 1: AdminTitle.styled.ts 수정
-  width: calc(100vw - 20rem);
+  width: calc(100vw - 15rem);

또는

# 옵션 2: 사이드바 너비를 20rem으로 변경
-  width: 15rem;
+  width: 20rem;
-  margin-left: 15rem;
+  margin-left: 20rem;

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

🤖 Prompt for AI Agents
In src/components/common/admin/sidebar/AdminSidebar.styled.ts at line 17, the
sidebar width is set to 15rem, but AdminTitle.styled.ts assumes 20rem width,
causing layout mismatch. To fix this, either update the sidebar width to 20rem
to match AdminTitle.styled.ts or adjust AdminTitle.styled.ts to use 15rem
consistently. Ensure both files use the same width value to maintain layout
alignment.

padding: 1rem;
width: 15rem;
border-right: 1px solid ${({ theme }) => theme.color.grey};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EnvelopeIcon,
ExclamationTriangleIcon,
HomeIcon,
LightBulbIcon,
MegaphoneIcon,
PhotoIcon,
TagIcon,
Expand All @@ -15,6 +16,7 @@ const iconMap = {
mainPage: <HomeIcon />,
movedSite: <ArrowRightStartOnRectangleIcon />,
notice: <MegaphoneIcon />,
faq: <LightBulbIcon />,
banner: <PhotoIcon />,
tags: <TagIcon />,
allUser: <UserGroupIcon />,
Expand Down
Loading