Skip to content

Commit 1c3ea1b

Browse files
authored
Merge pull request #352 from devpalsPlus/feat/#346
관리자 문의 조회 기능 구현 ( #issue 346 )
2 parents 13bb52f + bbe20ec commit 1c3ea1b

File tree

15 files changed

+341
-30
lines changed

15 files changed

+341
-30
lines changed

src/api/admin/customerService/inquiry.api.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import type { ApiCommonBasicType } from '../../../models/apiCommon';
22
import type {
3+
AdminInquiryChangeSearchParams,
34
ApiAdminInquiry,
45
ApiAdminInquiryDetail,
56
InquiryAnswerBody,
67
} from '../../../models/inquiry';
78
import { httpClient } from '../../http.api';
89

9-
export const getAllInquiries = async () => {
10+
export const getAllInquiries = async (
11+
childSearchParams: AdminInquiryChangeSearchParams
12+
) => {
1013
try {
11-
const response = await httpClient.get<ApiAdminInquiry>(`/inquiry`);
14+
const response = await httpClient.get<ApiAdminInquiry>(`/inquiry`, {
15+
params: childSearchParams,
16+
});
1217
return response.data.data;
1318
} catch (e) {
1419
console.error('전체 문의 조회 에러', e);

src/api/mypage.api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ApiCommonBasicType } from '../models/apiCommon';
12
import type {
23
ApiUserInfo,
34
ApiUserInfoImg,
@@ -53,6 +54,15 @@ export const patchMyProfileImg = async (file: File) => {
5354
}
5455
};
5556

57+
export const patchGithubLink = async (githubUrl: string) => {
58+
try {
59+
await httpClient.patch<ApiCommonBasicType>('/user/github', { githubUrl });
60+
} catch (error) {
61+
console.error('프로필 깃허브 업데이트: ', error);
62+
throw error;
63+
}
64+
};
65+
5666
export const getMyJoinedProjectList = async () => {
5767
try {
5868
const response = await httpClient.get<ApiJoinedProject>(

src/components/admin/adminInquiry/AdminInquiry.styled.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,33 @@ export const AdminInquiryCategory = styled.span`
1919
font-weight: 500;
2020
`;
2121

22-
export const AdminInquiryUser = styled.div``;
22+
export const AdminInquiryUserWrapper = styled.div`
23+
position: relative;
24+
`;
25+
26+
export const AdminInquiryUser = styled.button`
27+
width: fit-content;
28+
font-size: 1.1rem;
29+
z-index: 1;
30+
transition: color 0.1s ease-in-out;
31+
&:hover {
32+
color: ${({ theme }) => theme.color.lightnavy};
33+
}
34+
`;
35+
36+
export const AdminInquiryUserCheckDropdown = styled.div`
37+
position: absolute;
38+
top: 0.8rem;
39+
right: 0;
40+
background: ${({ theme }) => theme.color.white};
41+
border-radius: ${({ theme }) => theme.borderRadius.primary};
42+
border: 1px solid ${({ theme }) => theme.color.lightgrey};
43+
padding: 0.3rem 0.5rem;
44+
z-index: 100000;
45+
box-shadow: 3px 2px 19px -6px rgba(0, 0, 0, 0.75);
46+
-webkit-box-shadow: 3px 2px 19px -6px rgba(0, 0, 0, 0.75);
47+
-moz-box-shadow: 3px 2px 19px -6px rgba(0, 0, 0, 0.75);
48+
`;
2349

2450
export const AdminInquiryDate = styled.span`
2551
font-size: 0.9rem;

src/components/admin/adminInquiry/AdminInquiry.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useSearchParams } from 'react-router-dom';
12
import { ADMIN_ROUTE } from '../../../constants/routes';
23
import type { AdminInquiry as TAdminInquiry } from '../../../models/inquiry';
34
import ContentBorder from '../../common/contentBorder/ContentBorder';
@@ -8,12 +9,33 @@ interface AdminInquiryProps {
89
}
910

1011
export default function AdminInquiry({ list }: AdminInquiryProps) {
12+
const [searchParams, setSearchParams] = useSearchParams();
13+
14+
const handleClickLookupUser = (e: React.MouseEvent<HTMLButtonElement>) => {
15+
e.stopPropagation();
16+
e.preventDefault();
17+
18+
const id = String(list.user.id);
19+
const userId = id || '';
20+
const nickname = list.user.nickname;
21+
22+
const newParams = new URLSearchParams(searchParams);
23+
newParams.set('userId', userId);
24+
newParams.set('nickname', nickname);
25+
26+
setSearchParams(newParams);
27+
};
28+
1129
return (
1230
<S.AdminInquiryContainer to={`${ADMIN_ROUTE.detail}/${list.id}`}>
1331
<S.AdminInquiryWrapper>
1432
<S.AdminInquiryCategory>[{list.category}]</S.AdminInquiryCategory>
1533
<S.AdminInquiryTitle>{list.title}</S.AdminInquiryTitle>
16-
<S.AdminInquiryUser>{list.user.nickname}</S.AdminInquiryUser>
34+
<S.AdminInquiryUserWrapper>
35+
<S.AdminInquiryUser onClick={handleClickLookupUser}>
36+
{list.user.nickname}
37+
</S.AdminInquiryUser>
38+
</S.AdminInquiryUserWrapper>
1739
<S.AdminInquiryDate>{list.createdAt}</S.AdminInquiryDate>
1840
<S.AdminInquiryState $hasAnswer={list.state}>
1941
{list.state ? '답변완료' : '확인중'}

src/components/admin/adminInquiry/AdminInquiryList.styled.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;
66
export const AdminInquiryListContainer = styled.section`
77
width: 100%;
88
display: flex;
9-
justify-content: center;
9+
flex-direction: column;
10+
align-items: center;
1011
`;
1112

1213
export const AdminInquiryListWrapper = styled.div`

src/components/admin/adminInquiry/AdminInquiryList.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
import { useSearchParams } from 'react-router-dom';
12
import { useGetAllInquiries } from '../../../hooks/admin/useGetAllInquiries';
23
import Spinner from '../../user/mypage/Spinner';
34
import AdminInquiry from './AdminInquiry';
45
import * as S from './AdminInquiryList.styled';
6+
import AdminInquiryListLookup from './AdminInquiryListLookup';
7+
import type { AdminInquiryChangeSearchParams } from '../../../models/inquiry';
58

9+
export type SearchParamsInquiryKeyType = keyof AdminInquiryChangeSearchParams;
610
export default function AdminInquiryList() {
7-
const { allInquiriesData, isLoading } = useGetAllInquiries();
11+
const [searchParams, setSearchParams] = useSearchParams();
12+
const userId = searchParams.get('userId') || '';
13+
const startDate = searchParams.get('startDate') || '';
14+
const endDate = searchParams.get('endDate') || '';
15+
const { allInquiriesData, isLoading } = useGetAllInquiries({
16+
userId,
17+
startDate,
18+
endDate,
19+
});
820

921
if (isLoading) {
1022
return (
@@ -18,6 +30,7 @@ export default function AdminInquiryList() {
1830

1931
return (
2032
<S.AdminInquiryListContainer>
33+
<AdminInquiryListLookup />
2134
<S.AdminInquiryListWrapper>
2235
{allInquiriesData.map((list) => (
2336
<AdminInquiry key={list.id} list={list} />
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import styled from 'styled-components';
2+
3+
export const LookupContainer = styled.nav`
4+
width: 80%;
5+
margin-bottom: 1rem;
6+
7+
input[type='date'] {
8+
position: relative;
9+
padding: 14px;
10+
width: 150px;
11+
height: 30px;
12+
font-size: 14px;
13+
color: ${({ theme }) => theme.color.placeholder};
14+
border: none;
15+
border-bottom: 1px solid ${({ theme }) => theme.color.grey};
16+
}
17+
input[type='date']::-webkit-calendar-picker-indicator {
18+
position: absolute;
19+
top: 0;
20+
left: 0;
21+
right: 0;
22+
bottom: 0;
23+
width: auto;
24+
height: auto;
25+
color: transparent;
26+
background: transparent;
27+
}
28+
`;
29+
30+
export const LookupWrapper = styled.form`
31+
display: flex;
32+
justify-content: space-between;
33+
`;
34+
35+
export const LookupUser = styled.input`
36+
border-bottom: 1px solid ${({ theme }) => theme.color.placeholder};
37+
width: fit-content;
38+
`;
39+
40+
export const LookupDateWrapper = styled.div`
41+
display: flex;
42+
gap: 1rem;
43+
`;
44+
45+
export const LookupStartDate = styled.input``;
46+
47+
export const LookupJoinDate = styled.span``;
48+
49+
export const LookupEndDate = styled.input``;
50+
51+
export const LookupIconWrapper = styled.div`
52+
display: flex;
53+
gap: 2rem;
54+
55+
svg {
56+
width: 1.5rem;
57+
height: 1.5rem;
58+
}
59+
`;
60+
61+
export const IconDefault = styled.button`
62+
color: ${({ theme }) => theme.color.deepGrey};
63+
`;
64+
65+
export const IconSearch = styled.button`
66+
color: ${({ theme }) => theme.color.deepGrey};
67+
`;
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useEffect, useState } from 'react';
2+
import type { SearchParamsInquiryKeyType } from './AdminInquiryList';
3+
import * as S from './AdminInquiryListLookup.styled';
4+
import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';
5+
import { useSearchParams } from 'react-router-dom';
6+
import { AdminInquiryChangeSearchParams } from '../../../models/inquiry';
7+
import Modal from '../../common/modal/Modal';
8+
import { useModal } from '../../../hooks/useModal';
9+
import { MODAL_MESSAGE } from '../../../constants/user/modalMessage';
10+
11+
export default function AdminInquiryListLookup() {
12+
const [searchParams, setSearchParams] = useSearchParams();
13+
const userId = searchParams.get('userId') || '';
14+
const startDate = searchParams.get('startDate') || '';
15+
const endDate = searchParams.get('endDate') || '';
16+
const nickname = searchParams.get('nickname') || '';
17+
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
18+
const [searchFilters, setSearchFilters] = useState<
19+
Omit<AdminInquiryChangeSearchParams, 'userId'>
20+
>({
21+
startDate,
22+
endDate,
23+
});
24+
25+
useEffect(() => {
26+
const startDate = searchParams.get('startDate') || '';
27+
const endDate = searchParams.get('endDate') || '';
28+
setSearchFilters({
29+
startDate,
30+
endDate,
31+
});
32+
}, [searchParams, setSearchFilters]);
33+
34+
const handleSubmitChangeParams = (e: React.FormEvent<HTMLFormElement>) => {
35+
e.preventDefault();
36+
37+
const { startDate, endDate } = searchFilters;
38+
39+
const newParams = new URLSearchParams(searchParams);
40+
41+
if (startDate && !endDate) {
42+
return handleModalOpen(MODAL_MESSAGE.endDateEmpty);
43+
} else if (!startDate && endDate) {
44+
return handleModalOpen(MODAL_MESSAGE.startDateEmpty);
45+
} else if (startDate && endDate) {
46+
newParams.set('startDate', startDate);
47+
newParams.set('endDate', endDate);
48+
} else {
49+
newParams.delete('startDate');
50+
newParams.delete('endDate');
51+
}
52+
53+
setSearchParams(newParams);
54+
};
55+
56+
const handleChangeDate = (
57+
e: React.ChangeEvent<HTMLInputElement>,
58+
key: SearchParamsInquiryKeyType
59+
) => {
60+
const value = e.currentTarget.value;
61+
62+
setSearchFilters((prev) => {
63+
switch (key) {
64+
case 'startDate': {
65+
if (prev.endDate !== '' && prev.endDate < value) {
66+
handleModalOpen(MODAL_MESSAGE.startDateInvalid);
67+
return prev;
68+
}
69+
return { ...prev, startDate: value };
70+
}
71+
case 'endDate': {
72+
if (prev.startDate !== '' && prev.startDate > value) {
73+
handleModalOpen(MODAL_MESSAGE.endDateInvalid);
74+
return prev;
75+
}
76+
return { ...prev, endDate: value };
77+
}
78+
default: {
79+
return prev;
80+
}
81+
}
82+
});
83+
};
84+
85+
const handleClickInit = () => {
86+
setSearchParams({});
87+
setSearchFilters({ startDate: '', endDate: '' });
88+
};
89+
90+
return (
91+
<S.LookupContainer>
92+
<S.LookupWrapper onSubmit={handleSubmitChangeParams}>
93+
<S.LookupUser
94+
type='text'
95+
value={nickname}
96+
placeholder='닉네임을 클릭하세요.'
97+
readOnly
98+
/>
99+
<S.LookupDateWrapper>
100+
<S.LookupStartDate
101+
type='date'
102+
value={searchFilters.startDate}
103+
onChange={(e) => handleChangeDate(e, 'startDate')}
104+
/>
105+
<S.LookupJoinDate> ~ </S.LookupJoinDate>
106+
<S.LookupEndDate
107+
type='date'
108+
value={searchFilters.endDate}
109+
onChange={(e) => handleChangeDate(e, 'endDate')}
110+
/>
111+
</S.LookupDateWrapper>
112+
<S.LookupIconWrapper>
113+
{(userId || startDate || endDate) && (
114+
<S.IconDefault type='button' onClick={handleClickInit}>
115+
<XMarkIcon />
116+
</S.IconDefault>
117+
)}
118+
<S.IconSearch type='submit'>
119+
<MagnifyingGlassIcon />
120+
</S.IconSearch>
121+
</S.LookupIconWrapper>
122+
</S.LookupWrapper>
123+
<Modal isOpen={isOpen} onClose={handleModalClose}>
124+
{message}
125+
</Modal>
126+
</S.LookupContainer>
127+
);
128+
}

src/components/admin/previewComponent/inquiresPreview/InquiresPreview.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import Avatar from '../../../common/avatar/Avatar';
44
import { ADMIN_ROUTE } from '../../../../constants/routes';
55
import arrow_right from '../../../../assets/ArrowRight.svg';
66
import Spinner from '../../../user/mypage/Spinner';
7+
import { AdminInquiryChangeSearchParams } from '../../../../models/inquiry';
78

89
const InquiresPreview = () => {
9-
const { allInquiriesData, isLoading, isFetching } = useGetAllInquiries();
10+
const childSearchParams: AdminInquiryChangeSearchParams = {
11+
userId: '',
12+
startDate: '',
13+
endDate: '',
14+
};
15+
const { allInquiriesData, isLoading, isFetching } =
16+
useGetAllInquiries(childSearchParams);
1017

1118
if (isLoading || isFetching) {
1219
return (

0 commit comments

Comments
 (0)