Skip to content

Commit 82b304d

Browse files
github-actions[bot]lurgillqqssttyyseongjinme
authored
feat-fe: 이메일 발송 내역 조회 (#970)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jeongwoo Park <[email protected]> Co-authored-by: Kim Da Eun <[email protected]> Co-authored-by: Jeongwoo Park <[email protected]> Co-authored-by: Seongjin Hong <[email protected]>
1 parent 0e98516 commit 82b304d

File tree

22 files changed

+667
-99
lines changed

22 files changed

+667
-99
lines changed

frontend/src/api/domain/email.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Email } from '@customTypes/email';
12
import { EMAILS } from '../endPoint';
23
import APIClient from '../APIClient';
34

@@ -32,6 +33,9 @@ const emailApis = {
3233
path: '/verify-code',
3334
body: { email, verificationCode },
3435
}),
36+
37+
history: async (params: { clubId: string; applicantId: number }) =>
38+
apiClient.get<{ emailHistoryResponses: Email[] }>({ path: `/${params.clubId}/${params.applicantId}` }),
3539
};
3640

3741
export default emailApis;

frontend/src/components/ApplicantModal/ApplicantBaseInfo/style.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
33
const Container = styled.div`
44
display: flex;
55
flex-direction: column;
6-
padding: 32px 16px;
76
border-radius: 8px;
87
`;
98

@@ -23,12 +22,12 @@ const ActionRow = styled.div`
2322
const DetailContainer = styled.div`
2423
display: flex;
2524
flex-direction: column;
26-
gap: 8px;
25+
gap: 12px;
2726
2827
border-top: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
2928
border-bottom: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
3029
31-
padding: 8px 0px;
30+
padding: 10px 4px;
3231
`;
3332

3433
const DetailRow = styled.div`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { FloatingEmailFormProvider } from '@contexts/FloatingEmailFormContext';
3+
import SideFloatingMessageForm from '@components/dashboard/SideFloatingMessageForm';
4+
import { SpecificApplicantIdProvider } from '@contexts/SpecificApplicnatIdContext';
5+
import { MultiApplicantContextProvider } from '@contexts/MultiApplicantContext';
6+
import EmailHistorySection from '.';
7+
8+
const meta: Meta<typeof EmailHistorySection> = {
9+
title: 'Organisms/ApplicantModal/ApplicantDetailInfo/EmailHistorySection',
10+
component: EmailHistorySection,
11+
parameters: {
12+
layout: 'centered',
13+
docs: {
14+
description: {
15+
component: '지원자에게 전송된 이메일 목록입니다.',
16+
},
17+
},
18+
},
19+
args: {
20+
applicantId: 1,
21+
},
22+
tags: ['autodocs'],
23+
decorators: [
24+
(Story) => (
25+
<MultiApplicantContextProvider>
26+
<SpecificApplicantIdProvider>
27+
<FloatingEmailFormProvider>
28+
<SideFloatingMessageForm />
29+
<div style={{ width: '600px', height: '500px' }}>
30+
<Story />
31+
</div>
32+
</FloatingEmailFormProvider>
33+
</SpecificApplicantIdProvider>
34+
</MultiApplicantContextProvider>
35+
),
36+
],
37+
};
38+
39+
export default meta;
40+
type Story = StoryObj<typeof meta>;
41+
42+
export const Default: Story = {
43+
args: {
44+
applicantId: 1,
45+
},
46+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Button from '@components/_common/atoms/Button';
2+
import EmailHistoryItem from '@components/_common/atoms/EmailHistoryItem';
3+
import MessageForm, { SubmitProps } from '@components/dashboard/SideFloatingMessageForm/MessageForm';
4+
5+
import { useState } from 'react';
6+
import useEmail from '@hooks/useEmail';
7+
import useClubId from '@hooks/service/useClubId';
8+
import useGetEmailHistory from '@hooks/useGetEmailHistory';
9+
10+
import S from './style';
11+
12+
interface EmailHistorySectionProps {
13+
applicantId: number;
14+
}
15+
16+
export default function EmailHistorySection({ applicantId }: EmailHistorySectionProps) {
17+
const [toggle, setToggle] = useState(false);
18+
const { mutate: sendMutate, isPending } = useEmail(() => setToggle(false));
19+
const { emailHistory } = useGetEmailHistory({ applicantId });
20+
const clubId = useClubId().getClubId();
21+
22+
const toggleHidden = () => setToggle((prev) => !prev);
23+
24+
const handleSubmit = (props: SubmitProps) => {
25+
if (!isPending) sendMutate({ clubId, applicantIds: [applicantId], ...props });
26+
};
27+
28+
return (
29+
<S.Container>
30+
<S.Header>
31+
<S.Title>{`보낸 메일 (${emailHistory.length})`}</S.Title>
32+
<S.EmailButtonContainer>
33+
<Button
34+
size="fillContainer"
35+
color="white"
36+
onClick={toggleHidden}
37+
>
38+
{!toggle ? '메일 쓰기' : '취소'}
39+
</Button>
40+
</S.EmailButtonContainer>
41+
</S.Header>
42+
{toggle && (
43+
<S.EmailFormContainer>
44+
<MessageForm>
45+
<MessageForm.Form
46+
onSubmit={handleSubmit}
47+
isPending={isPending}
48+
/>
49+
</MessageForm>
50+
</S.EmailFormContainer>
51+
)}
52+
<S.ContentContainer>
53+
{emailHistory.map((email) => (
54+
<EmailHistoryItem
55+
key={`${email.createdDate}`}
56+
email={email}
57+
/>
58+
))}
59+
</S.ContentContainer>
60+
</S.Container>
61+
);
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import styled from '@emotion/styled';
2+
3+
const Container = styled.div`
4+
width: 100%;
5+
padding: 1.6rem;
6+
7+
display: flex;
8+
flex-direction: column;
9+
gap: 1.6rem;
10+
`;
11+
12+
const EmailFormContainer = styled.div`
13+
width: 100%;
14+
padding: 1.6rem 0.8rem;
15+
border-top: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
16+
border-bottom: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
17+
`;
18+
19+
const EmailButtonContainer = styled.div`
20+
width: 6.4rem;
21+
height: 3.2rem;
22+
`;
23+
24+
const Header = styled.div`
25+
padding: 0 0.8rem;
26+
27+
display: flex;
28+
justify-content: space-between;
29+
align-items: center;
30+
`;
31+
32+
const Title = styled.div`
33+
${({ theme }) => theme.typography.heading[500]};
34+
color: ${({ theme }) => theme.baseColors.grayscale[700]};
35+
`;
36+
37+
const ContentContainer = styled.div`
38+
width: 100%;
39+
40+
display: flex;
41+
flex-direction: column;
42+
43+
border: 1px solid ${({ theme }) => theme.baseColors.grayscale[300]};
44+
`;
45+
46+
const S = {
47+
Container,
48+
EmailFormContainer,
49+
EmailButtonContainer,
50+
Header,
51+
Title,
52+
ContentContainer,
53+
};
54+
55+
export default S;

frontend/src/components/ApplicantModal/ApplicantDetailInfo/QuestionSection/style.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
22

33
const Container = styled.div`
44
padding: 1.6rem;
5-
background-color: ${({ theme }) => theme.baseColors.grayscale[400]};
5+
background-color: ${({ theme }) => theme.baseColors.grayscale[200]};
66
77
width: 100%;
88
height: 100%;

frontend/src/components/ApplicantModal/ModalHeader/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface ModalHeaderProps {
77
title: string;
88
}
99

10-
export default function ApplicatnModalHeader({ title }: ModalHeaderProps) {
10+
export default function ApplicantModalHeader({ title }: ModalHeaderProps) {
1111
const { close } = useModal();
1212
return (
1313
<S.Container>

frontend/src/components/ApplicantModal/index.tsx

+38-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
import { useSpecificApplicantId } from '@contexts/SpecificApplicnatIdContext';
22
import { useSpecificProcessId } from '@contexts/SpecificProcessIdContext';
33

4+
import useTab from '@components/_common/molecules/Tab/useTab';
5+
import { FiFileText, FiMail } from 'react-icons/fi';
46
import BaseModal from './BaseModal';
57

68
import ApplicantBaseInfo from './ApplicantBaseInfo';
7-
import ApplicatnModalHeader from './ModalHeader';
89
import QuestionSection from './ApplicantDetailInfo/QuestionSection';
910
import ApplicantEvalInfo from './ApplicantEvalInfo';
1011
import EvaluationHeader from './ApplicantEvalInfo/EvaluationHeader';
1112
import InquireEvalHeader from './InquireEvalHeader';
13+
import ApplicantModalHeader from './ModalHeader';
1214

1315
import S from './style';
1416
import usePaginatedEvaluation from './usePaginatedEvaluation';
17+
import EmailHistorySection from './ApplicantDetailInfo/EmailHistorySection';
18+
19+
export type ApplicantModalTabItems = '지원서' | '이메일';
20+
21+
const TabMenus = {
22+
지원서: {
23+
title: '지원서',
24+
icon: FiFileText,
25+
description: '지원 시 접수된 지원서 내용입니다.',
26+
},
27+
이메일: {
28+
title: '이메일',
29+
icon: FiMail,
30+
description: '지원자에게 전송한 이메일 내역입니다.',
31+
},
32+
} as const;
1533

1634
export default function ApplicantModal() {
1735
const { applicantId } = useSpecificApplicantId();
1836
const { processId } = useSpecificProcessId();
1937

38+
const { currentMenu, moveTabByParam } = useTab<ApplicantModalTabItems>({ defaultValue: '지원서' });
2039
const { currentProcess, isCurrentProcess, moveProcess, isLastProcess, isFirstProcess } =
2140
usePaginatedEvaluation(processId);
2241

@@ -26,22 +45,36 @@ export default function ApplicantModal() {
2645
<BaseModal>
2746
<S.Container>
2847
<S.ModalHeader>
29-
<ApplicatnModalHeader title="지원서" />
48+
<ApplicantModalHeader title="지원자 상세" />
3049
</S.ModalHeader>
3150

3251
<S.ModalSidebar>
3352
<ApplicantBaseInfo applicantId={applicantId} />
53+
<S.ModalMenus>
54+
<S.ModalMenusTitle>메뉴</S.ModalMenusTitle>
55+
{Object.values(TabMenus).map((menu) => (
56+
<S.ModalMenusItem
57+
key={menu.title}
58+
isSelected={currentMenu === menu.title}
59+
onClick={() => moveTabByParam(menu.title)}
60+
>
61+
<menu.icon size={20} />
62+
{menu.title}
63+
</S.ModalMenusItem>
64+
))}
65+
</S.ModalMenus>
3466
</S.ModalSidebar>
3567

3668
<S.ModalNav>
3769
<S.ModalNavHeaderContainer>
38-
<S.ModalNavHeader>지원서</S.ModalNavHeader>
39-
<S.ModalNavContent>지원 시 접수된 지원서 내용입니다.</S.ModalNavContent>
70+
<S.ModalNavHeader>{TabMenus[currentMenu].title}</S.ModalNavHeader>
71+
<S.ModalNavContent>{TabMenus[currentMenu].description}</S.ModalNavContent>
4072
</S.ModalNavHeaderContainer>
4173
</S.ModalNav>
4274

4375
<S.ModalMain>
44-
<QuestionSection applicantId={applicantId} />
76+
{currentMenu === '지원서' && <QuestionSection applicantId={applicantId} />}
77+
{currentMenu === '이메일' && <EmailHistorySection applicantId={applicantId} />}
4578
</S.ModalMain>
4679

4780
<S.ModalEvalHeader>

frontend/src/components/ApplicantModal/style.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import styled from '@emotion/styled';
2+
import { hideScrollBar } from '@styles/utils';
23

34
const Container = styled.div`
45
width: 80vw;
@@ -26,6 +27,7 @@ const ModalHeader = styled.div`
2627
const ModalSidebar = styled.div`
2728
grid-area: sidebar;
2829
border-right: 0.1rem solid ${({ theme }) => theme.baseColors.grayscale[600]};
30+
padding: 3.2rem 1.6rem;
2931
`;
3032

3133
const ModalNav = styled.div`
@@ -61,6 +63,7 @@ const ModalMain = styled.div`
6163
grid-area: main;
6264
border-right: 0.1rem solid ${({ theme }) => theme.baseColors.grayscale[600]};
6365
overflow: auto;
66+
${hideScrollBar}
6467
`;
6568

6669
const ModalAsideHeader = styled.div`
@@ -71,7 +74,7 @@ const ModalAsideHeader = styled.div`
7174

7275
const ModalAside = styled.div`
7376
grid-area: aside;
74-
padding: 1.6rem; //TODO: Refactor
77+
padding: 1.6rem;
7578
overflow: auto;
7679
`;
7780

@@ -81,10 +84,42 @@ const ModalEvalHeader = styled.div`
8184
border-bottom: 0.1rem solid ${({ theme }) => theme.baseColors.grayscale[600]};
8285
`;
8386

87+
const ModalMenus = styled.div`
88+
padding: 1rem;
89+
display: flex;
90+
flex-direction: column;
91+
gap: 2rem;
92+
`;
93+
94+
const ModalMenusTitle = styled.div`
95+
height: 2rem;
96+
color: ${({ theme }) => theme.baseColors.grayscale[500]};
97+
${({ theme }) => theme.typography.heading[400]}
98+
99+
margin-bottom: -0.6rem;
100+
`;
101+
102+
const ModalMenusItem = styled.div<{ isSelected: boolean }>`
103+
${({ theme }) => theme.typography.heading[400]}
104+
105+
display: flex;
106+
align-items: center;
107+
gap: 1rem;
108+
109+
color: ${({ theme, isSelected }) => (isSelected ? theme.baseColors.grayscale[900] : theme.baseColors.grayscale[600])};
110+
111+
&:hover {
112+
cursor: pointer;
113+
}
114+
`;
115+
84116
const S = {
85117
Container,
86118
ModalHeader,
87119
ModalSidebar,
120+
ModalMenus,
121+
ModalMenusTitle,
122+
ModalMenusItem,
88123
ModalNav,
89124
ModalNavHeaderContainer,
90125
ModalNavHeader,

0 commit comments

Comments
 (0)