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: 여러 지원자 선택 토글 UI 및 기능 구현 #820

Open
wants to merge 13 commits into
base: fe/develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MultiApplicantContextProvider } from '@contexts/MultiApplicantContext';
import { DropdownProvider } from '@contexts/DropdownContext';
import MultiSelectToggle from '.';

const meta = {
title: 'Organisms/Dashboard/MultiSelectToggle',
component: MultiSelectToggle,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'MultiSelectToggle 컴포넌트는 여러 지원자의 선택 여부를 토글하고, 선택시 실행할 공통 작업을 제공하는 컴포넌트입니다.',
},
},
},
argTypes: {
isToggled: { control: 'boolean', description: '여러 지원자 선택 토글 여부를 나타냅니다.' },
selectedApplicantIds: {
description: '선택된 지원자들의 ID값 목록을 나타냅니다.',
},
},
tags: ['autodocs'],
decorators: [
(Child) => (
<div
style={{
width: '60rem',
height: '3.6rem',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<MultiApplicantContextProvider>
<DropdownProvider>
<Child />
</DropdownProvider>
</MultiApplicantContextProvider>
</div>
),
],
} satisfies Meta<typeof MultiSelectToggle>;

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

const processes = [
{
processName: '지원서',
processId: 1,
},
{
processName: '프로세스 1',
processId: 4,
},
{
processName: '프로세스 2',
processId: 2,
},
{
processName: '프로세스 3',
processId: 5,
},
{
processName: '최종 합격',
processId: 6,
},
];

const selectedApplicantIds = [1, 2, 3];

export const MultiSelectToggleDefault: Story = {
args: {
isToggled: false,
processes,
selectedApplicantIds,
},
};
107 changes: 107 additions & 0 deletions frontend/src/components/dashboard/MultiSelectToggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useEffect, useState } from 'react';

import { useMultiApplicant } from '@contexts/MultiApplicantContext';
import { DropdownProvider } from '@contexts/DropdownContext';
import type { SimpleProcess } from '@hooks/useProcess';

import useApplicant from '@hooks/useApplicant';
import ToggleSwitch from '@components/_common/atoms/ToggleSwitch';
import Dropdown from '@components/_common/molecules/Dropdown';
import DropdownItemRenderer, { DropdownItemType } from '@components/_common/molecules/DropdownItemRenderer';

import S from './style';

interface MultiSelectToggleProps {
isToggled: boolean;
processes: SimpleProcess[];
selectedApplicantIds: number[];
}

export default function MultiSelectToggle({ isToggled, processes, selectedApplicantIds }: MultiSelectToggleProps) {
const { toggleIsMultiType, resetApplicants } = useMultiApplicant();
const { mutate: moveApplicantProcess } = useApplicant({});

const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
const [isToggleVisible, setIsToggleVisible] = useState<boolean>(true);

useEffect(() => {
if (isToggled) {
setIsToggleVisible(false);
const timer = setTimeout(() => setIsDropdownVisible(true), 200);
return () => clearTimeout(timer);
}

setIsDropdownVisible(false);
const timer = setTimeout(() => setIsToggleVisible(true), 300);
return () => clearTimeout(timer);
Comment on lines +29 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

애니메이션이 유려하네요🫢👍
Toggle
토글이 오픈되었을 때 두 요소의 정렬을 가운데로 맞춰주실 수 있으실까요?

}, [isToggled]);

const handleToggleMultiType = () => {
if (isToggled) resetApplicants();
toggleIsMultiType();
};

const menuItems: DropdownItemType[] = [
{
type: 'subTrigger',
id: 'moveProcess',
name: '단계 이동',
items: processes.map(({ processName, processId }) => ({
type: 'clickable',
id: processId,
name: processName,
onClick: ({ targetProcessId }) => {
moveApplicantProcess({ processId: targetProcessId, applicants: selectedApplicantIds });
},
})),
},
{
type: 'clickable',
id: 'emailButton',
name: '이메일 보내기',
hasSeparate: true,
onClick: () => {
// TODO: 복수 지원자에 대한 이메일 전송 구현이 필요합니다. (24/10/16 아르)
console.log(`${selectedApplicantIds.join(', ')} 지원자들에게 이메일을 보냅니다.`);
},
},
{
type: 'clickable',
id: 'rejectButton',
name: '불합격 처리',
isHighlight: true,
hasSeparate: true,
onClick: () => {
// TODO: 일괄 불합격/불합격 해제에 해당하는 API 연결 Mutation 구현이 필요합니다. (24/10/16 아르)
console.log(`${selectedApplicantIds.join(', ')} 지원자들을 모두 불합격 처리합니다.`);
},
},
];

return (
<S.Wrapper>
<S.ToggleWrapper isVisible={isToggleVisible}>
<S.ToggleLabel>여러 지원자 선택</S.ToggleLabel>
<ToggleSwitch
width="4rem"
onChange={handleToggleMultiType}
isChecked={isToggled}
isDisabled={false}
/>
</S.ToggleWrapper>

<S.DropdownWrapper isVisible={isDropdownVisible}>
<DropdownProvider>
<Dropdown
initValue="실행할 작업"
width={120}
isShadow={false}
disabled={selectedApplicantIds.length === 0}
>
<DropdownItemRenderer items={menuItems} />
</Dropdown>
</DropdownProvider>
</S.DropdownWrapper>
</S.Wrapper>
);
}
41 changes: 41 additions & 0 deletions frontend/src/components/dashboard/MultiSelectToggle/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import styled from '@emotion/styled';

const Wrapper = styled.div`
display: flex;
justify-content: flex-end;
`;

const ToggleWrapper = styled.div<{ isVisible: boolean }>`
display: flex;
gap: 1.2rem;
align-items: center;
transition: transform 0.3s ease-in-out;
transform: ${({ isVisible }) => (isVisible ? 'translateX(0)' : 'translateX(-13.8rem)')};
`;

const ToggleLabel = styled.span`
${({ theme }) => theme.typography.heading[200]};
color: ${({ theme }) => theme.baseColors.grayscale[800]};
`;

const DropdownWrapper = styled.div<{ isVisible: boolean }>`
position: absolute;
transform: translateX(0);
opacity: ${({ isVisible }) => (isVisible ? 1 : 0.01)};
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
transition:
opacity 0.3s ease-in-out,
visibility 0.3s ease-in-out;
color: ${({ theme }) => theme.baseColors.grayscale[800]};
`;

const S = {
Wrapper,
ToggleWrapper,
ToggleLabel,
DropdownWrapper,
};

export default S;
8 changes: 4 additions & 4 deletions frontend/src/components/dashboard/ProcessBoard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ApplicantModal from '@components/ApplicantModal';
import { Process } from '@customTypes/process';
import type { Process } from '@customTypes/process';

import ApplicantModal from '@components/ApplicantModal';
import ProcessColumn from '../ProcessColumn';
import SideFloatingMessageForm from '../SideFloatingMessageForm';
import S from './style';

interface KanbanBoardProps {
interface ProcessBoardProps {
processes: Process[];
// eslint-disable-next-line react/no-unused-prop-types
isSubTab?: boolean;
Expand All @@ -17,7 +17,7 @@ export default function ProcessBoard({
processes,
showRejectedApplicant = false,
searchedName = '',
}: KanbanBoardProps) {
}: ProcessBoardProps) {
return (
<S.Container>
{/* TODO: isSubTab을 가져와서 SubTab을 렌더링 합니다. */}
Expand Down
Loading