Skip to content

Commit 9b9ba14

Browse files
authored
Merge pull request #341 from devpalsPlus/feat/#335
관리자 문의 ( #issue 335 )
2 parents 0e708cb + 59f6af4 commit 9b9ba14

File tree

66 files changed

+868
-66
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+868
-66
lines changed

src/api/activityLog.api.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,3 @@ export const getMyInquiries = async () => {
2323
throw e;
2424
}
2525
};
26-
27-
export const getAllInquiries = async () => {
28-
try {
29-
const response = await httpClient.get<ApiAllInquiries>(`/inquiry`);
30-
return response.data.data;
31-
} catch (e) {
32-
console.error('전체 문의 조회 에러', e);
33-
throw e;
34-
}
35-
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { ApiCommonBasicType } from '../../../models/apiCommon';
2+
import type {
3+
ApiAdminInquiry,
4+
ApiAdminInquiryDetail,
5+
InquiryAnswerBody,
6+
} from '../../../models/inquiry';
7+
import { httpClient } from '../../http.api';
8+
9+
export const getAllInquiries = async () => {
10+
try {
11+
const response = await httpClient.get<ApiAdminInquiry>(`/inquiry`);
12+
return response.data.data;
13+
} catch (e) {
14+
console.error('전체 문의 조회 에러', e);
15+
throw e;
16+
}
17+
};
18+
19+
export const getInquiryDetail = async (id: string) => {
20+
try {
21+
const response = await httpClient.get<ApiAdminInquiryDetail>(
22+
`/inquiry/${id}`
23+
);
24+
25+
return response.data.data;
26+
} catch (e) {
27+
console.error(e);
28+
throw e;
29+
}
30+
};
31+
32+
export const postInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => {
33+
try {
34+
await httpClient.post<ApiCommonBasicType>(`/inquiry/${id}/answer`, {
35+
answer,
36+
});
37+
} catch (e) {
38+
console.error(e);
39+
throw e;
40+
}
41+
};
42+
43+
export const patchInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => {
44+
try {
45+
await httpClient.patch<ApiCommonBasicType>(`/inquiry/${id}/answer`, {
46+
answer,
47+
});
48+
} catch (e) {
49+
console.error(e);
50+
throw e;
51+
}
52+
};
53+
54+
export const deleteInquiry = async (id: string) => {
55+
try {
56+
await httpClient.delete<ApiCommonBasicType>(`/inquiry/${id}`);
57+
} catch (e) {
58+
console.error(e);
59+
throw e;
60+
}
61+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Link } from 'react-router-dom';
2+
import styled, { css } from 'styled-components';
3+
4+
export const AdminInquiryContainer = styled(Link)``;
5+
6+
export const AdminInquiryWrapper = styled.div`
7+
padding: 0.8rem;
8+
display: grid;
9+
grid-template-columns: 13% 51% 15% 11% 10%;
10+
justify-content: center;
11+
align-items: center;
12+
font-size: 1.1rem;
13+
`;
14+
15+
export const AdminInquiryTitle = styled.div``;
16+
17+
export const AdminInquiryCategory = styled.span`
18+
font-size: 1.1rem;
19+
font-weight: 500;
20+
`;
21+
22+
export const AdminInquiryUser = styled.div``;
23+
24+
export const AdminInquiryDate = styled.span`
25+
font-size: 0.9rem;
26+
color: ${({ theme }) => theme.color.deepGrey};
27+
`;
28+
29+
export const AdminInquiryState = styled.div<{ $hasAnswer: boolean }>`
30+
text-align: center;
31+
color: ${({ $hasAnswer, theme }) =>
32+
$hasAnswer ? theme.color.white : theme.color.green};
33+
padding: 0.3rem;
34+
35+
${({ $hasAnswer }) =>
36+
$hasAnswer &&
37+
css`
38+
border-radius: ${({ theme }) => theme.borderRadius.large};
39+
background: ${({ theme }) => theme.color.navy};
40+
`}
41+
`;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ADMIN_ROUTE } from '../../../constants/routes';
2+
import type { AdminInquiry as TAdminInquiry } from '../../../models/inquiry';
3+
import ContentBorder from '../../common/contentBorder/ContentBorder';
4+
import * as S from './AdminInquiry.styled';
5+
6+
interface AdminInquiryProps {
7+
list: TAdminInquiry;
8+
}
9+
10+
export default function AdminInquiry({ list }: AdminInquiryProps) {
11+
return (
12+
<S.AdminInquiryContainer to={`${ADMIN_ROUTE.detail}/${list.id}`}>
13+
<S.AdminInquiryWrapper>
14+
<S.AdminInquiryCategory>[{list.category}]</S.AdminInquiryCategory>
15+
<S.AdminInquiryTitle>{list.title}</S.AdminInquiryTitle>
16+
<S.AdminInquiryUser>{list.user.nickname}</S.AdminInquiryUser>
17+
<S.AdminInquiryDate>{list.createdAt}</S.AdminInquiryDate>
18+
<S.AdminInquiryState $hasAnswer={list.state}>
19+
{list.state ? '답변완료' : '확인중'}
20+
</S.AdminInquiryState>
21+
</S.AdminInquiryWrapper>
22+
<ContentBorder />
23+
</S.AdminInquiryContainer>
24+
);
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import styled from 'styled-components';
2+
import {
3+
AdminInquiryContentContainer,
4+
AdminInquiryContentWrapper,
5+
InquiryContent,
6+
InquiryHeaderWrapper,
7+
InquiryInfo,
8+
} from './AdminInquiryDetailContent.styled';
9+
import { SendButton } from '../../user/customerService/inquiry/Inquiry.styled';
10+
11+
export const InquiryAnswerContentContainer = styled(
12+
AdminInquiryContentContainer
13+
)``;
14+
15+
export const InquiryAnswerContentWrapper = styled(AdminInquiryContentWrapper)``;
16+
17+
export const AnswerHeaderWrapper = styled(InquiryHeaderWrapper)`
18+
display: flex;
19+
gap: 3rem;
20+
`;
21+
22+
export const InquiryAnswerInfo = styled(InquiryInfo)`
23+
display: flex;
24+
align-items: center;
25+
`;
26+
27+
export const InquiryAnswerButton = styled(SendButton)`
28+
font-size: 0.9rem;
29+
padding: 0.3rem 0.5rem;
30+
`;
31+
32+
export const AnswerButtonSpan = styled.span``;
33+
34+
export const InquiryAnswerContent = styled(InquiryContent)``;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useEffect, useState } from 'react';
2+
import * as S from './AdminInquiryAnswer.styled';
3+
import { Link, useOutletContext } from 'react-router-dom';
4+
import { ADMIN_ROUTE } from '../../../constants/routes';
5+
6+
interface AdminInquiryDetailContentOutletContext {
7+
createdAt: string;
8+
answerData: string;
9+
}
10+
11+
type LinkType = '작성하기' | '수정하기';
12+
13+
export default function AdminInquiryAnswer() {
14+
const { createdAt, answerData }: AdminInquiryDetailContentOutletContext =
15+
useOutletContext();
16+
17+
const [answer, setAnswer] = useState<string>('');
18+
const selectButton: LinkType = answer === null ? '작성하기' : '수정하기';
19+
20+
useEffect(() => {
21+
if (answerData === null) {
22+
return setAnswer('');
23+
} else {
24+
return setAnswer(answerData);
25+
}
26+
}, [answerData]);
27+
28+
return (
29+
<S.InquiryAnswerContentContainer>
30+
<S.InquiryAnswerContentWrapper>
31+
<S.AnswerHeaderWrapper>
32+
<S.InquiryAnswerInfo>
33+
{createdAt ? createdAt : ''}
34+
</S.InquiryAnswerInfo>
35+
<S.InquiryAnswerButton
36+
as={Link}
37+
to={
38+
selectButton === '작성하기'
39+
? ADMIN_ROUTE.write
40+
: ADMIN_ROUTE.modification
41+
}
42+
>
43+
<S.AnswerButtonSpan>{selectButton}</S.AnswerButtonSpan>
44+
</S.InquiryAnswerButton>
45+
</S.AnswerHeaderWrapper>
46+
<S.InquiryAnswerContent>{answer}</S.InquiryAnswerContent>
47+
</S.InquiryAnswerContentWrapper>
48+
</S.InquiryAnswerContentContainer>
49+
);
50+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import styled from 'styled-components';
2+
import {
3+
AdminInquiryContentContainer,
4+
AdminInquiryContentWrapper,
5+
InquiryContent,
6+
} from './AdminInquiryDetailContent.styled';
7+
import {
8+
AnswerHeaderWrapper,
9+
InquiryAnswerButton,
10+
InquiryAnswerInfo,
11+
} from './AdminInquiryAnswer.styled';
12+
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';
13+
14+
export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;
15+
16+
export const InquiryAnswerContentContainer = styled(
17+
AdminInquiryContentContainer
18+
)``;
19+
20+
export const InquiryAnswerWriteWrapper = styled(AdminInquiryContentWrapper)``;
21+
22+
export const InquiryAnswerWriteHeaderWrapper = styled(AnswerHeaderWrapper)`
23+
display: flex;
24+
gap: 3rem;
25+
`;
26+
27+
export const InquiryAnswerWriteInfo = styled(InquiryAnswerInfo)`
28+
display: flex;
29+
align-items: center;
30+
`;
31+
32+
export const InquiryAnswerWriteButton = styled(InquiryAnswerButton)`
33+
align-items: start;
34+
height: fit-content;
35+
font-size: 0.9rem;
36+
padding: 0.3rem 0.5rem;
37+
`;
38+
39+
export const InquiryAnswerWriteButtonSpan = styled.span``;
40+
41+
export const InquiryAnswerWrite = styled(InquiryContent)`
42+
resize: none;
43+
border: 1px solid ${({ theme }) => theme.color.placeholder};
44+
border-radius: ${({ theme }) => theme.borderRadius.primary};
45+
padding: 1rem;
46+
`;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useOutletContext } from 'react-router-dom';
2+
import * as S from './AdminInquiryAnswerWrite.styled';
3+
import React, { useEffect, useRef, useState } from 'react';
4+
import { InquiryAnswerBody } from '../../../models/inquiry';
5+
import { UseMutationResult } from '@tanstack/react-query';
6+
import { AxiosError } from 'axios';
7+
import Modal from '../../common/modal/Modal';
8+
import { useModal } from '../../../hooks/useModal';
9+
import { INQUIRY_MESSAGE } from '../../../constants/user/customerService';
10+
import Spinner from '../../user/mypage/Spinner';
11+
12+
interface AdminInquiryAnswerWriteProps {
13+
answerData: string;
14+
isWrite: boolean;
15+
id: string;
16+
postInquiryAnswerMutate: UseMutationResult<
17+
void,
18+
AxiosError,
19+
InquiryAnswerBody
20+
>;
21+
patchInquiryAnswerMutate: UseMutationResult<
22+
void,
23+
AxiosError,
24+
InquiryAnswerBody
25+
>;
26+
}
27+
28+
export default function AdminInquiryAnswerWrite() {
29+
const {
30+
answerData,
31+
isWrite,
32+
id,
33+
postInquiryAnswerMutate,
34+
patchInquiryAnswerMutate,
35+
} = useOutletContext<AdminInquiryAnswerWriteProps>();
36+
const [answer, setAnswer] = useState<string>('');
37+
const inputRef = useRef<HTMLTextAreaElement>(null);
38+
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
39+
const isLoading =
40+
postInquiryAnswerMutate.isPending || patchInquiryAnswerMutate.isPending;
41+
42+
useEffect(() => {
43+
if (answerData) {
44+
setAnswer(answerData);
45+
}
46+
}, [answerData]);
47+
48+
if (isLoading) {
49+
return (
50+
<S.SpinnerWrapper $isAdmin={true}>
51+
<Spinner />
52+
</S.SpinnerWrapper>
53+
);
54+
}
55+
56+
const handleSubmitAnswer = (e: React.FormEvent<HTMLFormElement>) => {
57+
e.preventDefault();
58+
59+
if (answer.trim() === '') {
60+
return handleModalOpen(INQUIRY_MESSAGE.writeContent);
61+
}
62+
63+
if (isWrite) {
64+
postInquiryAnswerMutate.mutate({ id, answer: answer });
65+
} else {
66+
patchInquiryAnswerMutate.mutate({ id, answer: answer });
67+
}
68+
};
69+
70+
const handleChangeAnswer = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
71+
if (inputRef && inputRef.current) {
72+
inputRef.current.style.height = 'auto';
73+
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`;
74+
}
75+
76+
const answerValue = e.target.value;
77+
78+
setAnswer(answerValue);
79+
};
80+
81+
return (
82+
<S.InquiryAnswerContentContainer>
83+
<S.InquiryAnswerWriteWrapper as='form' onSubmit={handleSubmitAnswer}>
84+
<S.InquiryAnswerWriteHeaderWrapper>
85+
<S.InquiryAnswerWriteButton type='submit'>
86+
<S.InquiryAnswerWriteButtonSpan>
87+
답변하기
88+
</S.InquiryAnswerWriteButtonSpan>
89+
</S.InquiryAnswerWriteButton>
90+
<S.InquiryAnswerWriteInfo></S.InquiryAnswerWriteInfo>
91+
</S.InquiryAnswerWriteHeaderWrapper>
92+
<S.InquiryAnswerWrite
93+
as='textarea'
94+
value={answer}
95+
ref={inputRef}
96+
onChange={handleChangeAnswer}
97+
></S.InquiryAnswerWrite>
98+
</S.InquiryAnswerWriteWrapper>
99+
<Modal isOpen={isOpen} onClose={handleModalClose}>
100+
{message}
101+
</Modal>
102+
</S.InquiryAnswerContentContainer>
103+
);
104+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import styled from 'styled-components';
2+
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';
3+
4+
export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;
5+
6+
export const AdminInquiryDetailContainer = styled.main`
7+
width: 100%;
8+
display: flex;
9+
flex-direction: column;
10+
justify-content: center;
11+
gap: 1.5rem;
12+
`;

0 commit comments

Comments
 (0)