Skip to content

Commit

Permalink
feat-fe: 대시보드 내 지원서 관리 컴포넌트 및 기능 구현 (#365)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Seongjin Hong <[email protected]>
  • Loading branch information
github-actions[bot] and seongjinme committed Aug 23, 2024
1 parent 8ae9a94 commit 946d15b
Show file tree
Hide file tree
Showing 18 changed files with 627 additions and 29 deletions.
19 changes: 19 additions & 0 deletions frontend/src/api/domain/question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { QUESTIONS } from '@api/endPoint';
import { convertParamsToQueryString } from '@api/utils';
import { ModifyQuestionData } from '@customTypes/question';

import APIClient from '@api/APIClient';

const apiClient = new APIClient(QUESTIONS);

const questionApis = {
patch: async ({ applyformId, questions }: { applyformId: string; questions: ModifyQuestionData[] }) =>
apiClient.patch({
path: `?${convertParamsToQueryString({
applyformId,
})}`,
body: { questions },
}),
};

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

export const MEMBERS = `${BASE_URL}/members`;

export const QUESTIONS = `${BASE_URL}/questions`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { Meta, StoryObj } from '@storybook/react';
import { reactRouterParameters } from 'storybook-addon-remix-react-router';
import ApplyManagement from './index';

const meta: Meta<typeof ApplyManagement> = {
title: 'Components/Dashboard/ApplyManagement',
component: ApplyManagement,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'ApplyManagement 컴포넌트는 해당 공고의 지원서 사전 질문들을 수정하는 컴포넌트입니다.',
},
},
reactRouter: reactRouterParameters({
location: {
pathParams: { postId: '1' },
},
routing: { path: '/postId/:postId' },
}),
},
tags: ['autodocs'],
decorators: [
(Child) => (
<div
style={{
minWidth: '700px',
}}
>
<Child />
</div>
),
],
};

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

export const Default: Story = {
render: (args) => (
<div style={{ width: '700px' }}>
<ApplyManagement
{...args}
isVisible
/>
</div>
),
};
120 changes: 120 additions & 0 deletions frontend/src/components/applyManagement/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';

import useApplyManagement from '@hooks/useApplyManagement';
import Button from '@components/common/Button';
import QuestionBuilder from '@components/dashboard/DashboardCreate/Apply/QuestionBuilder';
import { APPLY_QUESTION_HEADER, DEFAULT_QUESTIONS, MAX_QUESTION_LENGTH } from '@constants/constants';

import { HiOutlinePlusCircle } from 'react-icons/hi';
import S from './style';

export default function ApplyManagement({ isVisible }: { isVisible: boolean }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const { postId } = useParams<{ postId: string }>() as {
postId: string;
};

const {
isLoading,
applyState,
modifyApplyQuestionsMutator,
addQuestion,
setQuestionTitle,
setQuestionType,
setQuestionOptions,
setQuestionRequiredToggle,
setQuestionPrev,
setQuestionNext,
deleteQuestion,
} = useApplyManagement({ postId });

useEffect(() => {
if (isVisible && wrapperRef.current && !isLoading) {
wrapperRef.current.scrollTop = 0;
}
}, [isVisible, isLoading]);

if (isLoading) {
<div>로딩 중입니다...</div>;
}

const isNextBtnValid =
applyState.length === DEFAULT_QUESTIONS.length ||
applyState
.slice(DEFAULT_QUESTIONS.length)
.every((question) => question.question.trim() && question.choices.length !== 1);

const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
modifyApplyQuestionsMutator.mutate();
};

return (
<S.Wrapper ref={wrapperRef}>
<S.Section>
<S.SectionTitleContainer>
<h2>{APPLY_QUESTION_HEADER.defaultQuestions.title}</h2>
<span>{APPLY_QUESTION_HEADER.defaultQuestions.description}</span>
</S.SectionTitleContainer>
<S.DefaultInputItemsContainer>
<S.DefaultInputItem>이름</S.DefaultInputItem>
<S.DefaultInputItem>이메일</S.DefaultInputItem>
<S.DefaultInputItem>전화번호</S.DefaultInputItem>
</S.DefaultInputItemsContainer>
</S.Section>

<S.Section>
<S.SectionTitleContainer>
<h2>{APPLY_QUESTION_HEADER.addQuestion.title}</h2>
<span>{APPLY_QUESTION_HEADER.addQuestion.description}</span>
</S.SectionTitleContainer>

{applyState.map((question, index) => {
if (index >= DEFAULT_QUESTIONS.length) {
return (
<S.QuestionsContainer key={question.id}>
<QuestionBuilder
index={index}
question={question}
setQuestionTitle={setQuestionTitle}
setQuestionType={setQuestionType}
setQuestionOptions={setQuestionOptions}
setQuestionPrev={setQuestionPrev}
setQuestionNext={setQuestionNext}
deleteQuestion={deleteQuestion}
setQuestionRequiredToggle={setQuestionRequiredToggle}
/>
</S.QuestionsContainer>
);
}
return null;
})}

{applyState.length < MAX_QUESTION_LENGTH && (
<S.AddQuestionButton
type="button"
onClick={addQuestion}
>
<HiOutlinePlusCircle />
항목 추가
</S.AddQuestionButton>
)}
</S.Section>

<S.Section>
<S.ModifyButtonContainer>
<Button
type="button"
color="primary"
size="fillContainer"
disabled={!isNextBtnValid}
onClick={handleSubmit}
>
수정하기
</Button>
</S.ModifyButtonContainer>
</S.Section>
</S.Wrapper>
);
}
148 changes: 148 additions & 0 deletions frontend/src/components/applyManagement/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import styled from '@emotion/styled';

const Wrapper = styled.div`
width: 100%;
height: 100%;
padding: 2rem 6rem;
display: flex;
flex-direction: column;
gap: 4rem;
overflow-y: auto;
`;

const Section = styled.div`
display: flex;
flex-direction: column;
gap: 1.6rem;
`;

const SectionTitleContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.8rem;
${({ theme }) => theme.typography.common.default};
h2 {
${({ theme }) => theme.typography.heading[500]};
}
span {
color: ${({ theme }) => theme.baseColors.grayscale[800]};
}
`;

const DefaultInputItemsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.8rem;
`;

const DefaultInputItem = styled.div`
width: 100%;
display: flex;
background: ${({ theme }) => theme.baseColors.grayscale[50]};
border: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
border-radius: 0.8rem;
padding: 0.8rem 2.4rem;
${({ theme }) => theme.typography.common.default};
font-weight: 600;
text-align: left;
vertical-align: middle;
`;

const QuestionsContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 3.6rem;
`;

const AddQuestionButton = styled.button`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.3rem;
padding: 0.8rem;
width: calc(100% - 3.6rem);
background: ${({ theme }) => theme.baseColors.grayscale[50]};
border: 1px dashed ${({ theme }) => theme.baseColors.grayscale[600]};
border-radius: 0.8rem;
${({ theme }) => theme.typography.heading[500]};
color: ${({ theme }) => theme.baseColors.grayscale[950]};
transition: all 0.2s ease-in-out;
:hover {
color: ${({ theme }) => theme.baseColors.purplescale[500]};
border-color: ${({ theme }) => theme.baseColors.purplescale[500]};
}
`;

const StepButtonsContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 2.4rem;
`;

const StepButton = styled.button`
display: flex;
flex-direction: row;
align-items: center;
gap: 0.3rem;
background: ${({ theme }) => theme.baseColors.grayscale[50]};
border: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
border-radius: 0.8rem;
padding: 0.5rem 0.8rem;
${({ theme }) => theme.typography.common.default};
font-weight: 700;
transition: all 0.2s ease-in-out;
:hover {
border-color: ${({ theme }) => theme.baseColors.grayscale[700]};
box-shadow: ${({ theme }) => `0px 2px 2px ${theme.baseColors.grayscale[400]}`};
}
`;

const ButtonContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding: 0 0.4rem;
gap: 0.4rem;
`;

const ModifyButtonContainer = styled.div`
width: 100%;
max-width: 30rem;
height: 5.2rem;
margin: 0 auto;
`;

const S = {
Wrapper,
Section,
SectionTitleContainer,

DefaultInputItemsContainer,
DefaultInputItem,

QuestionsContainer,
AddQuestionButton,

StepButtonsContainer,
StepButton,
ButtonContent,

ModifyButtonContainer,
};

export default S;
2 changes: 1 addition & 1 deletion frontend/src/components/common/Tab/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const TabButton = styled.button<{ isActive: boolean }>`
const TabPanel = styled.div<{ isVisible: boolean }>`
width: 100%;
height: 85%;
padding: 2rem 0rem;
padding: 2rem 0;
${hideScrollBar};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function QuestionBuilder({
}: QuestionBuilderProps) {
const [title, setTitle] = useState<string>(question?.question || '');
const [currentQuestionType, setCurrentQuestionType] = useState<Question['type']>(question?.type || 'SHORT_ANSWER');
const [isRequired, setIsRequired] = useState<boolean>(question?.required || true);
const [isRequired, setIsRequired] = useState<boolean>(question ? question.required : true);

const handleChangeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
setTitle(event.target.value);
Expand Down
Loading

0 comments on commit 946d15b

Please sign in to comment.