Skip to content

Commit

Permalink
글 작성/수정 폼 리팩토링해서 폼 훅 분리하기 (#838)
Browse files Browse the repository at this point in the history
* refactor: (#837) 변수명 수정 (deadLine -> deadline)

* refactor: (#837) 마감시간 관련 훅 분리

* fix: (#837) 마감시간 설정 오류 및 리다이렉트 오류 해결

- 수정 페이지 url로 접근 시 마감시간이 지난 글을 수정할 수 있는 오류 수정
- 타임피커 모달을 확인을 누르면 시간 검증없이 시간이 설정되는 오류 수정

* refactor: (#837) 게시물 정보를 폼데이터형식으로 변환함수 분리

* refactor: (#837) 게시물 정보 유효성 검사 인수 형식 객체로 수정 및 기존 타입 활용

- 폼데이터를 만드는 정보객체 재활용
- 마감시간 정보가 stringData 타입으로 수정되며 0일 0시간 0분을 검열하기 어려워짐 -> 코드 삭제

* fix: (#837) 마감시간 초기화 시 버튼 타입 미지정으로submit 되는 오류 수정

- 모달에 넣는 buttonInfo가 ButtonHTMLAttributes 상속하게 만들어 type을 지정할 수 있도록 함
- 모달과 폼에서 모두 사용되어 타입을 전역 타입폴더로 분리

* refactor: (#837) formdata  -> formData로 수정

* refactor: (#837) type buttonInfo -> ModalButton 수정

* refactor: (#837) useDeadline 함수 파일명 수정

* refactor: (#837) 모달 테스트 스토리명 변경
  • Loading branch information
chsua authored Nov 14, 2023
1 parent 8a46eb4 commit fdca208
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 136 deletions.
159 changes: 67 additions & 92 deletions frontend/src/components/PostForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { UseMutateFunction } from '@tanstack/react-query';

import React, { HTMLAttributes, useContext, useState } from 'react';
import React, { HTMLAttributes, useContext } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';

import { ResponsiveFlex } from 'votogether-design-system';

import { ModalButton } from '@type/modalButton';
import { PostInfo } from '@type/post';

import { useMultiSelect, useContentImage, useText, useToggle, useWritingOption } from '@hooks';

import { ToastContext } from '@hooks/context/toast';
import { useDeadline } from '@hooks/useDeadline';

import HeaderTextButton from '@components/common/HeaderTextButton';
import Modal from '@components/common/Modal';
Expand All @@ -26,20 +28,18 @@ import {
POST_TITLE_POLICY,
} from '@constants/policyMessage';

import { deleteOverlappingNewLine } from '@utils/deleteOverlappingNewLine';
import { addTimeToDate } from '@utils/post/addTimeToDate';
import { calculateDeadlineDHMTime } from '@utils/post/calculateDeadlineDHMTime';
import { checkWriter } from '@utils/post/checkWriter';
import { convertToFormData } from '@utils/post/convertToFormData';
import { getDeadlineMessage } from '@utils/post/getDeadlineMessage';
import { getSelectedDHMTimeOption } from '@utils/post/getSelectedTimeOption';
import { checkIrreplaceableTime } from '@utils/time/checkIrreplaceableTime';

import { theme } from '@styles/theme';

import CategoryWrapper from './CategoryWrapper';
import { DEADLINE_OPTION, DeadlineOptionInfo, DeadlineOptionName } from './constants';
import { DEADLINE_OPTION, DeadlineOptionInfo } from './constants';
import ContentImagePart from './ContentImageSection';
import * as S from './style';
import { WritingPostInfo } from './type';
import { checkValidationPost } from './validation';
interface PostFormProps extends HTMLAttributes<HTMLFormElement> {
data?: PostInfo;
Expand Down Expand Up @@ -79,116 +79,92 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
}))
);

//마감시간 관련 코드
const deadlineDHMTime = calculateDeadlineDHMTime(createTime, deadline);
const baseTime = createTime ? new Date(createTime) : new Date();

const [selectTimeOption, setSelectTimeOption] = useState<
DeadlineOptionName | '사용자지정' | null
>(getSelectedDHMTimeOption(deadlineDHMTime));
const [userSelectTime, setUserSelectTime] = useState(deadlineDHMTime);

if (postId && writer && !checkWriter(writer.id)) return <Navigate to={PATH.HOME} />;
//마감시간 관련 훅
const {
userSelectedDHMTime,
selectedTimeOption,
changeDeadlineOption,
changeDeadlinePicker,
resetDeadline,
getFinalDeadline,
getLimitDeadline,
} = useDeadline(createTime, deadline);

if (deadline && Number(new Date(deadline)) < Date.now()) {
addMessage('마감완료된 게시물은 수정할 수 없습니다.');
return <Navigate to={PATH.HOME} />;
}

if (postId && writer && !checkWriter(writer.id)) {
addMessage('사용자가 작성한 글만 수정할 수 있습니다.');
return <Navigate to={PATH.HOME} />;
}

if (serverVoteInfo && serverVoteInfo.allPeopleCount !== 0) {
addMessage('투표한 사용자가 있어 글 수정이 불가합니다.');
return <Navigate to={PATH.HOME} />;
}

// 마감시간 관련 핸들러
const handleDeadlineButtonClick = (option: DeadlineOptionInfo) => {
const targetTime = option.time;

if (data && checkIrreplaceableTime(targetTime, data.createTime))
if (data && checkIrreplaceableTime(option.time, data.createTime))
return addMessage('마감시간 지정 조건을 다시 확인해주세요.');
setSelectTimeOption(option.name);
setUserSelectTime(targetTime);

changeDeadlineOption(option);
};

const handleResetButton = () => {
if (window.confirm('정말 초기화하시겠습니까?')) {
const updatedTime = {
day: 0,
hour: 0,
minute: 0,
};
setUserSelectTime(updatedTime);
resetDeadline();
}
};

const handleModalClose = () => {
if (data && checkIrreplaceableTime(userSelectTime, data.createTime)) {
if (data && checkIrreplaceableTime(userSelectedDHMTime, data.createTime)) {
addMessage('마감시간 지정 조건을 다시 확인해주세요.');
const updatedTime = {
day: 0,
hour: 0,
minute: 0,
};
setUserSelectTime(updatedTime);
setSelectTimeOption(null);
resetDeadline();
}

setSelectTimeOption(
Object.values(userSelectTime).every(time => time === 0) ? null : '사용자지정'
);
closeModal();
};

const handlePostFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();

//예외처리
const { selectedOptionList } = categorySelectHook;
const errorMessage = checkValidationPost(
selectedOptionList,
writingTitle,
writingContent,
writingOptionHook.optionList,
userSelectTime
);
if (errorMessage) return addMessage(errorMessage);

const writingOptionList = writingOptionHook.optionList.map(
({ id, isServerId, text, imageUrl }, index) => {
return { id, isServerId, content: text, imageUrl };
}
);

if (e.target instanceof HTMLFormElement) {
const imageFileInputs = e.target.querySelectorAll<HTMLInputElement>('input[type="file"]');
const fileInputList = [...imageFileInputs];

selectedOptionList.forEach(categoryId =>
formData.append('categoryIds', categoryId.id.toString())
);
formData.append('title', writingTitle);
formData.append('content', deleteOverlappingNewLine(writingContent));
formData.append('imageUrl', contentImageHook.contentImage);
writingOptionList.forEach((option, index) => {
option.isServerId && formData.append(`postOptions[${index}].id`, option.id.toString());
formData.append(`postOptions[${index}].content`, deleteOverlappingNewLine(option.content));
formData.append(`postOptions[${index}].imageUrl`, option.imageUrl);
});
formData.append('deadline', addTimeToDate(userSelectTime, baseTime));

fileInputList.forEach((item: HTMLInputElement, index: number) => {
if (!item.files) return;

if (index === 0) {
item.files[0] && formData.append('imageFile', item.files[0]);
} else {
item.files[0] && formData.append(`postOptions[${index - 1}].imageFile`, item.files[0]);
}
});
const writingPostInfo: WritingPostInfo = {
categoryOptionList: categorySelectHook.selectedOptionList,
title: writingTitle,
content: writingContent,
imageUrl: contentImageHook.contentImage,
optionList: writingOptionHook.optionList,
deadline: getFinalDeadline(),
fileInputList: fileInputList,
};

//예외처리
const errorMessage = checkValidationPost(writingPostInfo);
if (errorMessage) return addMessage(errorMessage);

const formData = convertToFormData(writingPostInfo);

mutate(formData);
}
};

const primaryButton = {
const primaryButton: ModalButton = {
text: '저장',
handleClick: closeModal,
handleClick: handleModalClose,
type: 'button',
};

const secondaryButton = {
const secondaryButton: ModalButton = {
text: '초기화',
handleClick: handleResetButton,
type: 'button',
};

return (
Expand Down Expand Up @@ -244,22 +220,21 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
<S.Deadline aria-label="마감시간 설정">
<S.DeadlineDescription
aria-label={getDeadlineMessage({
hour: userSelectTime.hour,
day: userSelectTime.day,
minute: userSelectTime.minute,
hour: userSelectedDHMTime.hour,
day: userSelectedDHMTime.day,
minute: userSelectedDHMTime.minute,
})}
aria-live="polite"
>
{getDeadlineMessage({
hour: userSelectTime.hour,
day: userSelectTime.day,
minute: userSelectTime.minute,
hour: userSelectedDHMTime.hour,
day: userSelectedDHMTime.day,
minute: userSelectedDHMTime.minute,
})}
{data && (
<S.Description tabIndex={0}>
현재 시간으로부터 글 작성일({createTime})로부터 {MAX_DEADLINE}일 이내 (
{addTimeToDate({ day: MAX_DEADLINE, hour: 0, minute: 0 }, baseTime)})까지만 선택
가능합니다.
{getLimitDeadline()})까지만 선택 가능합니다.
</S.Description>
)}
{data && (
Expand All @@ -278,7 +253,7 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
key={option.name}
type="button"
onClick={() => handleDeadlineButtonClick(option)}
theme={selectTimeOption === option.name ? 'fill' : 'blank'}
theme={selectedTimeOption === option.name ? 'fill' : 'blank'}
>
{option.name}
</SquareButton>
Expand All @@ -287,7 +262,7 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
<SquareButton
type="button"
onClick={openComponent}
theme={selectTimeOption === '사용자지정' ? 'fill' : 'blank'}
theme={selectedTimeOption === '사용자지정' ? 'fill' : 'blank'}
>
사용자 지정
</SquareButton>
Expand Down Expand Up @@ -320,7 +295,7 @@ export default function PostForm({ data, mutate, isSubmitting }: PostFormProps)
<S.Description aria-label={POST_DEADLINE_POLICY.DEFAULT} tabIndex={0}>
{POST_DEADLINE_POLICY.DEFAULT}
</S.Description>
<TimePickerOptionList time={userSelectTime} setTime={setUserSelectTime} />
<TimePickerOptionList time={userSelectedDHMTime} setTime={changeDeadlinePicker} />
</S.ModalBody>
</Modal>
)}
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/components/PostForm/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StringDate } from '@type/time';

import { WritingVoteOptionType } from '@hooks/useWritingOption';

import { Option } from '@components/common/MultiSelect/types';

interface OptionWithIsServerId extends WritingVoteOptionType {
isServerId: boolean;
}

export interface WritingPostInfo {
categoryOptionList: Option[];
title: string;
content: string;
imageUrl: string;
optionList: OptionWithIsServerId[];
deadline: StringDate;
fileInputList: HTMLInputElement[];
}
29 changes: 12 additions & 17 deletions frontend/src/components/PostForm/validation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { DHMTime } from '@type/time';

import { WritingVoteOptionType } from '@hooks/useWritingOption';

import { Option } from '@components/common/MultiSelect/types';

import {
POST_CATEGORY_POLICY,
POST_CONTENT_POLICY,
Expand All @@ -12,15 +6,17 @@ import {
POST_TITLE_POLICY,
} from '@constants/policyMessage';

export const checkValidationPost = (
categoryList: Option[],
title: string,
content: string,
optionList: WritingVoteOptionType[],
time: DHMTime
) => {
if (categoryList.length < 1) return POST_CATEGORY_POLICY.MIN;
if (categoryList.length > 3) return POST_CATEGORY_POLICY.MAX;
import { WritingPostInfo } from './type';

export const checkValidationPost = ({
categoryOptionList,
title,
content,
optionList,
deadline,
}: WritingPostInfo) => {
if (categoryOptionList.length < 1) return POST_CATEGORY_POLICY.MIN;
if (categoryOptionList.length > 3) return POST_CATEGORY_POLICY.MAX;

if (title.trim() === '') return POST_TITLE_POLICY.REQUIRED;

Expand All @@ -30,6 +26,5 @@ export const checkValidationPost = (
if (optionList.length > 5) return POST_OPTION_POLICY.MAX;
if (optionList.some(option => option.text.trim() === '')) return POST_OPTION_POLICY.REQUIRED;

const isTimeOptionZero = Object.values(time).reduce((a, b) => a + b, 0) < 1;
if (isTimeOptionZero) return POST_DEADLINE_POLICY.REQUIRED;
if (Number(new Date(deadline)) <= Date.now()) return POST_DEADLINE_POLICY.DEFAULT;
};
19 changes: 16 additions & 3 deletions frontend/src/components/common/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const meta: Meta<typeof Modal> = {

export default meta;

export const Default = () => {
export const Deadline = () => {
const [isOpen, setIsOpen] = useState(true);

const openModal = () => {
Expand Down Expand Up @@ -163,7 +163,7 @@ export const CloseByESC = () => {
);
};

export const WithTimePicker = () => {
export const WithDeadlineTimePicker = () => {
const [time, setTime] = useState({
day: 2,
hour: 7,
Expand Down Expand Up @@ -202,6 +202,19 @@ export const WithTimePicker = () => {
handleClick: handleResetButton,
};

const changeDeadlinePicker = ({
option,
updatedTime,
}: {
option: string;
updatedTime: number;
}) => {
setTime(prev => ({
...prev,
[option]: updatedTime,
}));
};

return (
<>
<SquareButton onClick={openModal} theme="blank">
Expand All @@ -217,7 +230,7 @@ export const WithTimePicker = () => {
>
<S.Body>
<S.Description>최대 {MAX_DEADLINE}일을 넘을 수 없습니다.</S.Description>
<TimePickerOptionList time={time} setTime={setTime} />
<TimePickerOptionList time={time} setTime={changeDeadlinePicker} />
</S.Body>
</Modal>
)}
Expand Down
Loading

0 comments on commit fdca208

Please sign in to comment.