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: 대시보드 내 지원서 관리 컴포넌트 및 기능 구현 #365

Merged
merged 38 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a735e0
Create draft PR for #364
github-actions[bot] Aug 9, 2024
8b22cac
feat: 대시보드 상단에 '지원서 관리' 탭 메뉴 추가
seongjinme Aug 9, 2024
c4b25cf
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 9, 2024
834f413
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 9, 2024
6d16232
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 9, 2024
4494454
feat: 랜덤값 ID key값 생성 함수 구현
seongjinme Aug 9, 2024
009a419
feat: 대시보드 화면에서 기존 추가질문 항목 보기 기능 구현
seongjinme Aug 9, 2024
8663c0d
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 14, 2024
d7a8c39
feat: 기본 질문 항목 및 텍스트 상수화
seongjinme Aug 14, 2024
e77ba93
refactor: 수정폼 컴포넌트 구조 변경
seongjinme Aug 14, 2024
fdccf71
refactor: 상수화된 요소들 적용
seongjinme Aug 14, 2024
dedfcd5
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 19, 2024
78139a3
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 19, 2024
2dec5f6
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 21, 2024
4f8aec8
fix: 불필요한 컴포넌트 노출 조건식 제거
seongjinme Aug 21, 2024
c818065
fix: "항목 추가" 버튼에 부여된 잘못된 조건식 수정
seongjinme Aug 21, 2024
2143092
fix: 지원서 관리 탭에서 질문 양이 많을 경우 scroll이 안 되는 문제 수정
seongjinme Aug 21, 2024
480dcb5
fix: 다른 PR에서 수정되었던 key prop 이슈가 미반영된 문제 수정
seongjinme Aug 21, 2024
06fdeca
fix: 질문이 여러 개일 때 스크롤이 하단으로 고정되는 이슈 수정
seongjinme Aug 21, 2024
ba36873
fix: 사전 질문의 고유 ID를 맵핑 데이터에 추가
seongjinme Aug 21, 2024
f913f85
test: applyForm 관련 항목별 questionId를 기본 질문 ID와 겹치지 않게 수정
seongjinme Aug 21, 2024
e7c3f0d
style: "수정하기" 버튼 스타일을 Figma 시안과 일치화
seongjinme Aug 21, 2024
9c1e594
feat: 사전 질문 쿼리 상태에 따른 임시 분기처리 추가
seongjinme Aug 21, 2024
e6c06f7
feat: API 호출을 위한 endpoint 추가
seongjinme Aug 21, 2024
0f3ba8a
test: 로컬 테스트를 위한 핸들러 추가
seongjinme Aug 21, 2024
e9510c2
feat: API 호출 위한 타입 추가
seongjinme Aug 21, 2024
f9d280c
feat: API 호출 코드 구현
seongjinme Aug 21, 2024
6e55ab2
feat: 데이터 mutation 코드 추가
seongjinme Aug 21, 2024
492a9eb
fix: 새 질문 추가시 id값 중복으로 인한 key 충돌 이슈 수정
seongjinme Aug 21, 2024
1b5a2ab
feat: 각 질문별 필수 입력 내용을 체크하여 수정 버튼 활성화하는 기능 추가
seongjinme Aug 21, 2024
e826784
refactor: Apply 컴포넌트에 삽입된 불필요 스타일 코드 제거
seongjinme Aug 21, 2024
7b89d2f
test: 스토리북 추가
seongjinme Aug 21, 2024
218f6b2
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 21, 2024
da323cf
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 22, 2024
de42c19
Merge remote-tracking branch 'upstream/fe/develop' into fe-364-COM_DA…
seongjinme Aug 22, 2024
6a5c462
fix: API측 문서 스펙과 실제 코드의 불일치 해결을 위한 데이터 Key값 변경 적용
seongjinme Aug 22, 2024
771374e
fix: 선택질문에 대한 토글값이 지원서 관리 탭에서 항상 true인 문제 수정
seongjinme Aug 22, 2024
41b3123
fix: 추가 질문 없이 공고 생성한 경우에 대한 상태 로직 수정
seongjinme Aug 22, 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
4 changes: 3 additions & 1 deletion frontend/src/api/domain/apply/applyDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Question {
description: string;
orderIndex: number;
choices: Choice[];
required: boolean;
}

export interface ApplyDto {
Expand All @@ -37,13 +38,14 @@ export function dtoToRecruitmentPost({ title, startDate, endDate, postingContent

export function dtoToApplyForm(dto: ApplyDto): ApplyForm {
return {
questions: dto.questions.map(({ id, type, label, description, orderIndex, choices }) => ({
questions: dto.questions.map(({ id, type, label, description, orderIndex, choices, required }) => ({
questionId: id,
type,
label,
description,
orderIndex,
choices,
required,
})) as CustomQuestion[],
};
}
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
Loading
Loading