Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat-fe: 메세지 제출 폼 및 사이드 모달 컴포넌트 구현 #701

Merged
merged 24 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
069cf05
Create draft PR for #700
github-actions[bot] Sep 24, 2024
4ebb011
feat: MessageForm 컴포넌트 구현
lurgi Sep 24, 2024
f02313b
chore: 파일 경로 수정
lurgi Sep 24, 2024
a2e3137
feat: Floating Form 구현
lurgi Sep 25, 2024
d7f611b
chore: Tanstack-Query devtools 설치
lurgi Sep 25, 2024
29e27d9
squash devtools
lurgi Sep 25, 2024
0a6cb91
feat: DropdownItem Separate 추가
lurgi Sep 25, 2024
57251cb
feat: SideFloatingMessageForm ApplicantName 추가
lurgi Sep 25, 2024
f8f932c
feat: PopOverMenu Separate 추가
lurgi Sep 25, 2024
4a2654d
feat: ApplicantCard PopOverMenu 아이템 추가
lurgi Sep 25, 2024
32a7a4e
feat: PopOverMenu Item 이벤트 추가
lurgi Sep 25, 2024
8f24cbf
feat: APIClient에 formData 형식 추가
lurgi Sep 25, 2024
c676654
feat: APIClient formData prop 추가
lurgi Sep 25, 2024
ca3f5fe
feat: 이메일 전송 기능 구현
lurgi Sep 25, 2024
5de5ef8
feat: 이메일 전송 POST 모킹
lurgi Sep 26, 2024
0c6186b
feat: 이메일 전송 이후 컨트롤 설정
lurgi Sep 26, 2024
64cb0f9
refactor-fe: 코드 스플리팅으로 로딩 성능 개선 (#678)
github-actions[bot] Sep 26, 2024
744ae5c
chore: 개행 삭제
lurgi Sep 26, 2024
a7130e8
Merge branch 'fe/develop' into fe-700-COM_MESSAGE
lurgi Sep 26, 2024
821e3aa
chore: 의존성 재설치
lurgi Sep 26, 2024
db47998
chore-fe: SEO 개선 작업 (#674)
github-actions[bot] Sep 26, 2024
34990fa
chore: package-lock 수정
lurgi Sep 26, 2024
5aeaefe
chore-fe: SEO 개선 작업 (#674)
github-actions[bot] Sep 26, 2024
b976205
Merge branch 'fe/develop' into fe-700-COM_MESSAGE
lurgi Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11,772 changes: 4,606 additions & 7,166 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> {
Copy link
Contributor

Choose a reason for hiding this comment

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

앞으로 확장성 있는 APIClient를 만들 땐 formData 타입도 유의해야겠네요..💭

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 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,17 +37,39 @@

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('오픈해야함');

Check warning on line 56 in frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx

View workflow job for this annotation

GitHub Actions / run-test-pr-opened

Unexpected alert

Check warning on line 56 in frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx

View workflow job for this annotation

GitHub Actions / run-test-pr-opened

Unexpected alert
},
});

items.push({
id: 'rejectButton',
name: '불합격 처리',
isHighlight: true,
hasSeparate: true,
onClick: () => {
// TODO: 불합격 로직 추가
alert('오픈해야함');

Check warning on line 67 in frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx

View workflow job for this annotation

GitHub Actions / run-test-pr-opened

Unexpected alert

Check warning on line 67 in frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx

View workflow job for this annotation

GitHub Actions / run-test-pr-opened

Unexpected alert
},
});
const rejectAppHandler = () => {
const confirmAction = (message: string, action: () => void) => {
const isConfirmed = window.confirm(message);

Check warning on line 72 in frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx

View workflow job for this annotation

GitHub Actions / run-test-pr-opened

Unexpected confirm

Check warning on line 72 in frontend/src/components/ApplicantModal/ApplicantBaseInfo/index.tsx

View workflow job for this annotation

GitHub Actions / run-test-pr-opened

Unexpected confirm
if (isConfirmed) {
action();
close();
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
Loading