Skip to content

Commit

Permalink
feat-fe: 메세지 제출 폼 및 사이드 모달 컴포넌트 구현 (#701)
Browse files Browse the repository at this point in the history
Co-authored-by: Jeongwoo Park <[email protected]>
  • Loading branch information
2 people authored and llqqssttyy committed Sep 27, 2024
1 parent e260eb1 commit 9b13ed6
Show file tree
Hide file tree
Showing 27 changed files with 10,174 additions and 12,278 deletions.
21,896 changes: 9,667 additions & 12,229 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@sentry/react": "^8.20.0",
"@sentry/webpack-plugin": "^2.21.1",
"@tanstack/react-query": "^5.51.1",
"@tanstack/react-query-devtools": "^5.58.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/

const PACKAGE_VERSION = '2.3.2'
const PACKAGE_VERSION = '2.4.9'
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/api/APIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class APIClient implements APIClientType {
return this.request<T>({ method: 'GET', ...params });
}

async post<T>(params: APIClientParamsWithBody): Promise<T> {
async post<T>(params: APIClientParamsWithBody & { isFormData?: boolean }): Promise<T> {
return this.request<T>({ method: 'POST', ...params });
}

Expand All @@ -54,10 +54,12 @@ export default class APIClient implements APIClientType {
method,
body,
hasCookies = true,
isFormData = false,
}: {
method: Method;
body?: BodyHashMap;
hasCookies?: boolean;
isFormData?: boolean;
}) {
const headers: HeadersInit = {
Accept: 'application/json',
Expand All @@ -69,14 +71,21 @@ export default class APIClient implements APIClientType {
headers,
};

if (['POST', 'PUT', 'PATCH'].includes(method)) {
if (['POST', 'PUT', 'PATCH'].includes(method) && !isFormData) {
headers['Content-Type'] = 'application/json';
}

if (body) {
if (body && !isFormData) {
requestInit.body = JSON.stringify(body);
}

if (body && isFormData) {
const formData = new FormData();
const bodyKeys = Object.keys(body);
bodyKeys.forEach((key) => formData.append(key, body[key]));
requestInit.body = formData;
}

return requestInit;
}

Expand All @@ -85,14 +94,16 @@ export default class APIClient implements APIClientType {
method,
body,
hasCookies,
isFormData,
}: {
path: string;
method: Method;
body?: BodyHashMap;
hasCookies?: boolean;
isFormData?: boolean;
}): Promise<T> {
const url = this.baseURL + path;
const response = await fetch(url, this.getRequestInit({ method, body, hasCookies }));
const response = await fetch(url, this.getRequestInit({ method, body, hasCookies, isFormData }));

if (!response.ok) {
const json = await response.json();
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/api/domain/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EMAILS } from '../endPoint';
import APIClient from '../APIClient';

const apiClient = new APIClient(EMAILS);

const emailApis = {
send: async ({
clubId,
applicantId,
subject,
content,
}: {
clubId: string;
applicantId: number;
subject: string;
content: string;
}) =>
apiClient.post({
path: '/send',
body: { clubId, applicantIds: applicantId, subject, content },
isFormData: true,
}),
};

export default emailApis;
2 changes: 2 additions & 0 deletions frontend/src/api/endPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export const AUTH = `${BASE_URL}/auth`;
export const MEMBERS = `${BASE_URL}/members`;

export const QUESTIONS = `${BASE_URL}/questions`;

export const EMAILS = `${BASE_URL}/emails`;
38 changes: 31 additions & 7 deletions frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import useApplicant from '@hooks/useApplicant';
import specificApplicant from '@hooks/useSpecificApplicant';
import formatDate from '@utils/formatDate';
import { useModal } from '@contexts/ModalContext';

import { DropdownListItem } from '@customTypes/common';
import S from './style';

interface ApplicantBaseInfoProps {
Expand Down Expand Up @@ -35,14 +37,36 @@ export default function ApplicantBaseInfo({ applicantId }: ApplicantBaseInfoProp

const items = processList
.filter(({ processName }) => processName !== process.name)
.map(({ processId, processName }) => ({
id: processId,
name: processName,
onClick: ({ targetProcessId }: { targetProcessId: number }) => {
moveApplicantProcess({ processId: targetProcessId, applicants: [applicantId] });
},
}));
.map(
({ processId, processName }) =>
({
id: processId,
name: processName,
onClick: ({ targetProcessId }: { targetProcessId: number | string }) => {
moveApplicantProcess({ processId: Number(targetProcessId), applicants: [applicantId] });
},
}) as DropdownListItem,
);
items.push({
id: 'emailButton',
name: '이메일 보내기',
hasSeparate: true,
onClick: () => {
// TODO: 이메일 보내는 로직 추가
alert('오픈해야함');
},
});

items.push({
id: 'rejectButton',
name: '불합격 처리',
isHighlight: true,
hasSeparate: true,
onClick: () => {
// TODO: 불합격 로직 추가
alert('오픈해야함');
},
});
const rejectAppHandler = () => {
const confirmAction = (message: string, action: () => void) => {
const isConfirmed = window.confirm(message);
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/_common/atoms/DropdownItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ export interface DropdownItemProps {
size: 'sm' | 'md';
onClick: () => void;
isHighlight?: boolean;
hasSeparate?: boolean;
}

export default function DropdownItem({ item, size, onClick, isHighlight = false }: DropdownItemProps) {
export default function DropdownItem({
item,
size,
onClick,
isHighlight = false,
hasSeparate = false,
}: DropdownItemProps) {
const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onClick();
Expand All @@ -18,6 +25,7 @@ export default function DropdownItem({ item, size, onClick, isHighlight = false
size={size}
onClick={clickHandler}
isHighlight={isHighlight}
hasSeparate={hasSeparate}
>
{item}
</S.Item>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/_common/atoms/DropdownItem/style.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import styled from '@emotion/styled';

const Item = styled.button<{ isHighlight: boolean; size: 'sm' | 'md' }>`
const Item = styled.button<{ isHighlight: boolean; size: 'sm' | 'md'; hasSeparate: boolean }>`
display: block;
text-align: left;
width: 100%;
padding: ${({ size }) => (size === 'md' ? '6px 8px' : '6px 4px')};
${({ theme, size }) => (size === 'md' ? theme.typography.common.default : theme.typography.common.small)};
color: ${({ isHighlight, theme }) => (isHighlight ? theme.baseColors.redscale[500] : 'none')};
border-radius: 4px;
border-top: ${({ theme, hasSeparate }) =>
hasSeparate ? `0.15rem solid ${theme.baseColors.grayscale[400]}` : 'none'};
cursor: pointer;
transition: all 0.2s ease-in-out;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ export const InitValueTest: StoryObj<DropdownProps> = {
initValue: '아무 글자',
},
};

export const SeparateTest: StoryObj<DropdownProps> = {
...Template,
args: {
size: 'sm',
items: [...testItemList, { ...testItem, name: 'SeparateTest', id: testItemList.length, hasSeparate: true }],
initValue: '아무 글자',
},
};
3 changes: 2 additions & 1 deletion frontend/src/components/_common/molecules/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default function Dropdown({
size={size}
isShadow={isShadow}
>
{items.map(({ name, isHighlight, id, onClick }) => (
{items.map(({ name, isHighlight, id, hasSeparate, onClick }) => (
<DropdownItem
onClick={() => {
onClick({ targetProcessId: id });
Expand All @@ -86,6 +86,7 @@ export default function Dropdown({
item={name}
isHighlight={isHighlight}
size={size}
hasSeparate={hasSeparate}
/>
))}
</S.List>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Template: StoryObj<PopOverMenuProps> = {
const testItem: PopOverMenuItem = {
id: 123,
name: 'Menu Label',
onClick: () => console.log('clicked'),
onClick: () => alert('clicked'),
};
const testItemList: PopOverMenuItem[] = Array.from({ length: 3 }, (_, index) => ({
...testItem,
Expand All @@ -39,3 +39,11 @@ export const SmallSize: StoryObj<PopOverMenuProps> = {
items: testItemList,
},
};

export const SeparateTest: StoryObj<PopOverMenuProps> = {
...Template,
args: {
size: 'sm',
items: [...testItemList, { ...testItem, name: 'SeparateTest', id: testItemList.length, hasSeparate: true }],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function PopOverMenu({ isOpen, setClose, size = 'sm', popOverPosi
{isOpen && (
<S.ListWrapper>
<S.List size={size}>
{items.map(({ name, isHighlight, id, onClick }) => (
{items.map(({ name, isHighlight, id, hasSeparate, onClick }) => (
<DropdownItem
size={size}
onClick={() => {
Expand All @@ -30,6 +30,7 @@ export default function PopOverMenu({ isOpen, setClose, size = 'sm', popOverPosi
key={id}
item={name}
isHighlight={isHighlight}
hasSeparate={hasSeparate}
/>
))}
</S.List>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/dashboard/ProcessBoard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Process } from '@customTypes/process';
import ApplicantModal from '@components/ApplicantModal';
import ProcessColumn from '../ProcessColumn';
import SideFloatingMessageForm from '../SideFloatingMessageForm';
import S from './style';

interface KanbanBoardProps {
Expand All @@ -20,6 +21,8 @@ export default function ProcessBoard({ processes, showRejectedApplicant = false
))}

<ApplicantModal />

<SideFloatingMessageForm />
</S.Wrapper>
);
}
49 changes: 40 additions & 9 deletions frontend/src/components/dashboard/ProcessColumn/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { Process } from '@customTypes/process';
import useProcess from '@hooks/useProcess';
import useApplicant from '@hooks/useApplicant';
import { useModal } from '@contexts/ModalContext';
import { PopOverMenuItem } from '@customTypes/common';

import S from './style';
import specificApplicant from '@hooks/useSpecificApplicant';
import { useFloatingEmailForm } from '@contexts/FloatingEmailFormContext';
import ApplicantCard from '../ApplicantCard';
import ProcessDescription from './ProcessDescription/index';
import ProcessDescription from './ProcessDescription';
import S from './style';

interface ProcessColumnProps {
process: Process;
Expand All @@ -20,19 +23,47 @@ export default function ProcessColumn({ process, showRejectedApplicant }: Proces
const { dashboardId, applyFormId } = useParams() as { dashboardId: string; applyFormId: string };
const { processList } = useProcess({ dashboardId, applyFormId });
const { mutate: moveApplicantProcess } = useApplicant({});
const { mutate: rejectMutate } = specificApplicant.useRejectApplicant({ dashboardId, applyFormId });

const { setApplicantId } = useSpecificApplicantId();
const { setProcessId } = useSpecificProcessId();
const { open } = useModal();
const { open: sideEmailFormOpen } = useFloatingEmailForm();

const menuItemsList = ({ applicantId }: { applicantId: number }) =>
processList.map(({ processName, processId }) => ({
id: processId,
name: processName,
onClick: ({ targetProcessId }: { targetProcessId: number }) => {
moveApplicantProcess({ processId: targetProcessId, applicants: [applicantId] });
const menuItemsList = ({ applicantId }: { applicantId: number }) => {
const menuItems = processList.map(
({ processName, processId }) =>
({
id: processId,
name: processName,
onClick: ({ targetProcessId }: { targetProcessId: number }) => {
moveApplicantProcess({ processId: targetProcessId, applicants: [applicantId] });
},
}) as PopOverMenuItem,
);

menuItems.push({
id: 'emailButton',
name: '이메일 보내기',
hasSeparate: true,
onClick: () => {
setApplicantId(applicantId);
sideEmailFormOpen();
},
}));
});

menuItems.push({
id: 'rejectButton',
name: '불합격 처리',
isHighlight: true,
hasSeparate: true,
onClick: () => {
rejectMutate({ applicantId });
},
});

return menuItems;
};

const cardClickHandler = (id: number) => {
setApplicantId(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FloatingEmailFormProvider } from '@contexts/FloatingEmailFormContext';
import MessageForm from '.';

const meta: Meta<typeof MessageForm> = {
title: 'Organisms/Dashboard/SideFloatingMessageForm/MessageForm',
component: MessageForm,
tags: ['autodocs'],
args: {
recipient: '러기',
onSubmit: (data) => {
alert(`Subject: ${data.subject}, Content: ${data.content}`);
},
onClose: () => alert('close button clied'),
},
decorators: [
(Story) => (
<FloatingEmailFormProvider>
<Story />
</FloatingEmailFormProvider>
),
],
};

export default meta;
type Story = StoryObj<typeof MessageForm>;

export const Default: Story = {};
Loading

0 comments on commit 9b13ed6

Please sign in to comment.