diff --git a/backend/src/main/java/com/votogether/infra/image/LocalUploader.java b/backend/src/main/java/com/votogether/infra/image/LocalUploader.java index 3b9123b8c..a932bf2ae 100644 --- a/backend/src/main/java/com/votogether/infra/image/LocalUploader.java +++ b/backend/src/main/java/com/votogether/infra/image/LocalUploader.java @@ -26,7 +26,6 @@ public LocalUploader( public String upload(final MultipartFile image) { final File directory = loadDirectory(getImageStorePath()); - if (isEmptyImage(image)) { return null; } @@ -75,7 +74,6 @@ public void delete(final String path) { private String getImageLocalPath(final String fullPath) { final int urlIndex = fullPath.lastIndexOf(url); - if (urlIndex == -1) { throw new ImageException(ImageExceptionType.IMAGE_URL); } diff --git a/frontend/__test__/convertTimeToWord.test.ts b/frontend/__test__/convertTimeToWord.test.ts new file mode 100644 index 000000000..76be9ad6e --- /dev/null +++ b/frontend/__test__/convertTimeToWord.test.ts @@ -0,0 +1,61 @@ +import { convertTimeToWord } from '@utils/time'; + +describe('게시글 작성시간을 숫자 문자열로 받아 현재 시간과 비교해 반올림한 차이를 한글로 반환하는 유틸함수를 테스트한다.', () => { + test('2023-01-01 12:00에 작성한 글은 2023-01-01 12:05을 기준으로 "5분"이 반환된다.', () => { + const nowDate = new Date('2023-01-01 12:05'); + const result = convertTimeToWord('2023-01-01 12:00', nowDate); + + expect(result).toBe('5분 전 작성'); + }); + + test('2023-01-01 12:00에 작성한 글은 2023-01-01 20:10을 기준으로 "8시간"이 반환된다.', () => { + const nowDate = new Date('2023-01-01 20:10'); + const result = convertTimeToWord('2023-01-01 12:00', nowDate); + + expect(result).toBe('8시간 전 작성'); + }); + + test('2023-01-01 12:00에 작성한 글은 2023-01-02 13:00을 기준으로 "1일"이 반환된다.', () => { + const nowDate = new Date('2023-01-02 13:00'); + const result = convertTimeToWord('2023-01-01 12:00', nowDate); + + expect(result).toBe('1일 전 작성'); + }); + + test('2023-01-01 12:00에 작성한 글은 2023-01-12 13:00을 기준으로 "11일"이 반환된다.', () => { + const nowDate = new Date('2023-01-12 13:00'); + const result = convertTimeToWord('2023-01-01 12:00', nowDate); + + expect(result).toBe('11일 전 작성'); + }); + + test('작성된지 30일 이상이라면 작성 날짜를 반환한다.', () => { + const nowDate = new Date('2023-02-01 13:00'); + const result = convertTimeToWord('2023-01-01 12:00', nowDate); + + expect(result).toBe('2023-01-01'); + }); +}); + +describe('게시글 마감시간을 숫자 문자열로 받아 현재 시간과 비교해 반올림한 차이를 한글로 반환하는 유틸함수를 테스트한다.', () => { + test('2023-01-01 12:00 기준으로 2023-01-01 12:10가 마감인 경우 "10분 후 마감"이 반환된다.', () => { + const nowDate = new Date('2023-01-01 12:00'); + const result = convertTimeToWord('2023-01-01 12:10', nowDate); + + expect(result).toBe('10분 후 마감'); + }); + + test('2023-01-01 12:00 기준으로 2023-01-01 18:00가 마감인 경우 "6시간 후 마감"이 반환된다.', () => { + const nowDate = new Date('2023-01-01 12:00'); + const result = convertTimeToWord('2023-01-01 18:00', nowDate); + + expect(result).toBe('6시간 후 마감'); + }); + + test('2023-01-01 12:00 기준으로 2023-01-03 12:00가 마감인 경우 "2일 후 마감"이 반환된다.', () => { + const nowDate = new Date('2023-01-01 12:00'); + const result = convertTimeToWord('2023-01-03 12:00', nowDate); + + expect(result).toBe('2일 후 마감'); + }); +}); diff --git a/frontend/__test__/deleteOverlappingNewLine.test.ts b/frontend/__test__/deleteOverlappingNewLine.test.ts new file mode 100644 index 000000000..b228dda1a --- /dev/null +++ b/frontend/__test__/deleteOverlappingNewLine.test.ts @@ -0,0 +1,59 @@ +import { deleteOverlappingNewLine } from '@utils/post/deleteOverlappingNewLine'; + +describe('연속된 개행은 하나의 개행으로 처리하는 유틸함수를 테스트한다.', () => { + test('개행이 없는 문자열은 인자와 동일한 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다. 동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(text); + }); + + test('연속된 개행이 없는 문자열은 인자와 동일한 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(text); + }); + + test('2회 연속된 개행이 있는 문자열은 인자와 동일한 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const expectText = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(expectText); + }); + + test('5회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 5회 개행으로 바뀐 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const expectText = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(expectText); + }); + + test('7회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 5회 개행으로 바뀐 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const expectText = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(expectText); + }); + + test('10회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 5회 개행으로 바뀐 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const expectText = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(expectText); + }); +}); diff --git a/frontend/__test__/getSelectedTimeOption.test.ts b/frontend/__test__/getSelectedTimeOption.test.ts index 7324c2ebc..46422f454 100644 --- a/frontend/__test__/getSelectedTimeOption.test.ts +++ b/frontend/__test__/getSelectedTimeOption.test.ts @@ -1,64 +1,64 @@ import { getSelectedTimeOption } from '@utils/post/getSelectedTimeOption'; -describe('getSelectedTimeOption 함수에서 day, hour, minute 객체를 입력받아 "10분" | "30분" | "1시간" | "6시간" | "1일" | "사용자 지정" | null 을 반환한다.', () => { - test('10분 객체를 입력했을 때 10분을 반환한다.', () => { +describe('getSelectedTimeOption 함수에서 day, hour, minute 객체를 입력받아 "1일" | "3일" | "5일" | "7일" | "14일" | "사용자 지정" | null 을 반환한다.', () => { + test('1일 객체를 입력했을 때 1일을 반환한다.', () => { const time = { - day: 0, + day: 1, hour: 0, - minute: 10, + minute: 0, }; const result = getSelectedTimeOption(time); - expect(result).toBe('10분'); + expect(result).toBe('1일'); }); - test('30분 객체를 입력했을 때 30분을 반환한다.', () => { + test('3일 객체를 입력했을 때 3일을 반환한다.', () => { const time = { - day: 0, + day: 3, hour: 0, - minute: 30, + minute: 0, }; const result = getSelectedTimeOption(time); - expect(result).toBe('30분'); + expect(result).toBe('3일'); }); - test('1시간 객체를 입력했을 때 1시간을 반환한다.', () => { + test('5일 객체를 입력했을 때 5일을 반환한다.', () => { const time = { - day: 0, - hour: 1, + day: 5, + hour: 0, minute: 0, }; const result = getSelectedTimeOption(time); - expect(result).toBe('1시간'); + expect(result).toBe('5일'); }); - test('6시간 객체를 입력했을 때 6시간을 반환한다.', () => { + test('7일 객체를 입력했을 때 7일을 반환한다.', () => { const time = { - day: 0, - hour: 6, + day: 7, + hour: 0, minute: 0, }; const result = getSelectedTimeOption(time); - expect(result).toBe('6시간'); + expect(result).toBe('7일'); }); - test('1일 객체를 입력했을 때 1일을 반환한다.', () => { + test('14일 객체를 입력했을 때 14일을 반환한다.', () => { const time = { - day: 1, + day: 14, hour: 0, minute: 0, }; const result = getSelectedTimeOption(time); - expect(result).toBe('1일'); + expect(result).toBe('14일'); }); test('2일 객체를 입력했을 때 사용자지정을 반환한다.', () => { diff --git a/frontend/__test__/hooks/useWritingOption.test.tsx b/frontend/__test__/hooks/useWritingOption.test.tsx index 1741b6885..f0908cc12 100644 --- a/frontend/__test__/hooks/useWritingOption.test.tsx +++ b/frontend/__test__/hooks/useWritingOption.test.tsx @@ -44,7 +44,9 @@ describe('useWritingOption 훅을 테스트 한다.', () => { const { optionList } = result.current; - expect(optionList).toEqual(MOCK_MIN_VOTE_OPTION); + expect(optionList).toEqual( + MOCK_MIN_VOTE_OPTION.map(option => ({ ...option, isServerId: true })) + ); }); test('투표 선택지를 추가할 수 있어야 한다. 생성된 선택지는 text와 imageUrl 값을 가지고 있다.', () => { @@ -76,7 +78,9 @@ describe('useWritingOption 훅을 테스트 한다.', () => { const { optionList } = result.current; - expect(optionList).toEqual(MOCK_MAX_VOTE_OPTION); + expect(optionList).toEqual( + MOCK_MAX_VOTE_OPTION.map(option => ({ ...option, isServerId: true })) + ); }); test('투표 선택지가 3개 이상일때는 투표 선택지의 아이디를 이용하여 삭제할 수 있다.', () => { @@ -90,7 +94,9 @@ describe('useWritingOption 훅을 테스트 한다.', () => { const { optionList } = result.current; - expect(optionList).toEqual(MOCK_MAX_VOTE_OPTION.slice(1, 5)); + expect(optionList).toEqual( + MOCK_MAX_VOTE_OPTION.slice(1, 5).map(option => ({ ...option, isServerId: true })) + ); }); test('투표 선택지가 2개일때는 삭제할 수 없다.', () => { @@ -104,7 +110,9 @@ describe('useWritingOption 훅을 테스트 한다.', () => { const { optionList } = result.current; - expect(optionList).toEqual(MOCK_MIN_VOTE_OPTION); + expect(optionList).toEqual( + MOCK_MIN_VOTE_OPTION.map(option => ({ ...option, isServerId: true })) + ); }); test('선택한 이미지가 있을 때 취소할 수 있다.', () => { diff --git a/frontend/src/components/PostForm/ContentImageSection/index.tsx b/frontend/src/components/PostForm/ContentImageSection/index.tsx index a66c0e93f..611bc70f1 100644 --- a/frontend/src/components/PostForm/ContentImageSection/index.tsx +++ b/frontend/src/components/PostForm/ContentImageSection/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, MouseEvent, MutableRefObject } from 'react'; +import { ChangeEvent, MutableRefObject } from 'react'; import { Size } from '@type/style'; @@ -18,11 +18,6 @@ interface ContentImageSectionProps { export default function ContentImageSection({ contentImageHook, size }: ContentImageSectionProps) { const { contentImage, contentInputRef, removeImage, handleUploadImage } = contentImageHook; - const handleButtonClick = (e: MouseEvent) => { - e.preventDefault(); - contentInputRef.current && contentInputRef.current.click(); - }; - return ( <> {contentImage && ( @@ -35,12 +30,7 @@ export default function ContentImageSection({ contentImageHook, size }: ContentI )} { - + 본문에 사진 넣기 { data?: PostInfo; @@ -64,22 +63,19 @@ export default function PostForm({ data, mutate }: PostFormProps) { } = data ?? {}; const navigate = useNavigate(); - const contentImageHook = useContentImage( - serverImageUrl && convertImageUrlToServerUrl(serverImageUrl) - ); + const contentImageHook = useContentImage(serverImageUrl); const { handlePasteImage } = contentImageHook; - const writingOptionHook = useWritingOption( serverVoteInfo?.options.map(option => ({ ...option, - imageUrl: option.imageUrl ? convertImageUrlToServerUrl(option.imageUrl) : '', + imageUrl: option.imageUrl ?? '', })) ); const { isToastOpen, openToast, toastMessage } = useToast(); - const [selectTimeOption, setSelectTimeOption] = useState( - getSelectedTimeOption(calculateDeadlineTime(createTime, deadline)) - ); + const [selectTimeOption, setSelectTimeOption] = useState< + DeadlineOptionName | '사용자지정' | null + >(getSelectedTimeOption(calculateDeadlineTime(createTime, deadline))); const { isOpen, openComponent, closeComponent } = useToggle(); const [time, setTime] = useState(calculateDeadlineTime(createTime, deadline)); const baseTime = createTime ? new Date(createTime) : new Date(); @@ -107,12 +103,12 @@ export default function PostForm({ data, mutate }: PostFormProps) { } = useText(content ?? ''); const multiSelectHook = useMultiSelect(categoryIds ?? [], CATEGORY_COUNT_LIMIT); - const handleDeadlineButtonClick = (option: DeadlineOption) => { - const targetTime = formatTimeWithOption(option); + const handleDeadlineButtonClick = (option: DeadlineOptionInfo) => { + const targetTime = option.time; if (data && checkIrreplaceableTime(targetTime, data.createTime)) return openToast('마감시간 지정 조건을 다시 확인해주세요.'); - setSelectTimeOption(option); + setSelectTimeOption(option.name); setTime(targetTime); }; @@ -135,66 +131,50 @@ export default function PostForm({ data, mutate }: PostFormProps) { e.preventDefault(); const formData = new FormData(); - const writingOptionList = writingOptionHook.optionList.map(({ text, imageUrl }, index) => { - return { content: text, imageUrl: convertServerUrlToImageUrl(imageUrl) }; - }); - - const imageUrlList = [ - convertServerUrlToImageUrl(contentImageHook.contentImage), - ...writingOptionList.map(option => option.imageUrl), - ]; - //예외처리 const { selectedOptionList } = multiSelectHook; - if (selectedOptionList.length < 1) return openToast('카테고리를 최소 1개 골라주세요.'); - if (selectedOptionList.length > 3) return openToast('카테고리를 최대 3개 골라주세요.'); - if (writingTitle.trim() === '') return openToast('제목은 필수로 입력해야 합니다.'); - if (writingContent.trim() === '') return openToast('내용은 필수로 입력해야 합니다.'); - if (writingOptionList.length < 2) return openToast('선택지는 최소 2개 입력해주세요.'); - if (writingOptionList.length > 5) return openToast('선택지는 최대 5개 입력할 수 있습니다.'); - if (writingOptionList.some(option => option.content.trim() === '')) - return openToast('선택지에 글을 입력해주세요.'); - if (Object.values(time).reduce((a, b) => a + b, 0) < 1) - return openToast('시간은 필수로 입력해야 합니다.'); + const errorMessage = checkValidationPost( + selectedOptionList, + writingTitle, + writingContent, + writingOptionHook.optionList, + time + ); + if (errorMessage) return openToast(errorMessage); + + const writingOptionList = writingOptionHook.optionList.map( + ({ id, isServerId, text, imageUrl }, index) => { + return { id, isServerId, content: text, imageUrl }; + } + ); if (e.target instanceof HTMLFormElement) { - const optionImageFileInputs = - e.target.querySelectorAll('input[type="file"]'); - const fileInputList: HTMLInputElement[] = [...optionImageFileInputs]; - const contentImageFileList: File[] = []; - const optionImageFileList: File[] = []; - fileInputList.forEach((item, index) => { + const imageFileInputs = e.target.querySelectorAll('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(time, baseTime)); + + fileInputList.forEach((item: HTMLInputElement, index: number) => { if (!item.files) return; if (index === 0) { - //사진url이 ""거나 undefined이거나 기존 url과 동일한 url이면 true - !imageUrlList[index] || imageUrlList[index] === serverImageUrl - ? contentImageFileList.push(new File(['없는사진'], '없는사진.jpg')) - : contentImageFileList.push(item.files[0]); + item.files[0] && formData.append('imageFile', item.files[0]); } else { - //사진url이 ""거나 undefined이거나 기존 url과 동일한 url이면 true - !imageUrlList[index] || - imageUrlList[index] === serverVoteInfo?.options[index - 1].imageUrl - ? optionImageFileList.push(new File(['없는사진'], '없는사진.jpg')) - : optionImageFileList.push(item.files[0]); + item.files[0] && formData.append(`postOptions[${index - 1}].imageFile`, item.files[0]); } }); - contentImageFileList.map(file => formData.append('contentImages', file)); - optionImageFileList.map(file => formData.append('optionImages', file)); - - const updatedPostTexts = { - categoryIds: selectedOptionList.map(option => option.id), - title: writingTitle, - imageUrl: convertServerUrlToImageUrl(contentImageHook.contentImage), - content: writingContent, - postOptions: writingOptionList, - deadline: addTimeToDate(time, baseTime), - // 글 수정의 경우 작성시간을 기준으로 마감시간 옵션을 더한다. - // 마감시간 옵션을 선택 안했다면 기존의 마감 시간을 유지한다. - }; - formData.append('request', JSON.stringify(updatedPostTexts)); - mutate(formData); } }; @@ -205,10 +185,10 @@ export default function PostForm({ data, mutate }: PostFormProps) { <> - navigate('/')}>취소 - + navigate('/')}>취소 + 저장 - +
@@ -263,14 +243,14 @@ export default function PostForm({ data, mutate }: PostFormProps) { {getDeadlineTime({ hour: time.hour, day: time.day, minute: time.minute })} {data && ( - 현재 시간으로부터 글 작성일({createTime})로부터 3일 이내 ( - {addTimeToDate({ day: 3, hour: 0, minute: 0 }, baseTime)})까지만 선택 + 현재 시간으로부터 글 작성일({createTime})로부터 {MAX_DEADLINE}일 이내 ( + {addTimeToDate({ day: MAX_DEADLINE, hour: 0, minute: 0 }, baseTime)})까지만 선택 가능합니다. )} {data && ( - * 작성일시로부터 마감시간이 계산됩니다.{' '} + * 작성일시로부터 마감시간이 계산됩니다. )} {data && ( @@ -280,13 +260,13 @@ export default function PostForm({ data, mutate }: PostFormProps) { {DEADLINE_OPTION.map(option => ( handleDeadlineButtonClick(option)} - theme={selectTimeOption === option ? 'fill' : 'blank'} + theme={selectTimeOption === option.name ? 'fill' : 'blank'} > - {option} + {option.name} ))} { diff --git a/frontend/src/components/PostForm/style.ts b/frontend/src/components/PostForm/style.ts index 5c46bca19..811496a3a 100644 --- a/frontend/src/components/PostForm/style.ts +++ b/frontend/src/components/PostForm/style.ts @@ -26,10 +26,16 @@ export const HeaderButton = styled.button` export const Wrapper = styled.div` display: grid; grid-template-columns: 1fr; + justify-items: center; + justify-content: center; gap: 20px; padding: 70px 10px 20px 10px; + & > * { + width: 100%; + } + @media (min-width: ${theme.breakpoint.sm}) { grid-template-columns: 2fr 1fr; gap: 30px; diff --git a/frontend/src/components/PostForm/validation.ts b/frontend/src/components/PostForm/validation.ts new file mode 100644 index 000000000..a56016a03 --- /dev/null +++ b/frontend/src/components/PostForm/validation.ts @@ -0,0 +1,27 @@ +import { Time } from '@type/post'; + +import { WritingVoteOptionType } from '@hooks/useWritingOption'; + +import { Option } from '@components/common/MultiSelect/types'; + +export const checkValidationPost = ( + categoryList: Option[], + title: string, + content: string, + optionList: WritingVoteOptionType[], + time: Time +) => { + if (categoryList.length < 1) return '카테고리를 최소 1개 골라주세요.'; + if (categoryList.length > 3) return '카테고리를 최대 3개 골라주세요.'; + + if (title.trim() === '') return '제목은 필수로 입력해야 합니다.'; + + if (content.trim() === '') return '내용은 필수로 입력해야 합니다.'; + + if (optionList.length < 2) return '선택지는 최소 2개 입력해주세요.'; + if (optionList.length > 5) return '선택지는 최대 5개 입력할 수 있습니다.'; + if (optionList.some(option => option.text.trim() === '')) return '선택지에 글을 입력해주세요.'; + + const isTimeOptionZero = Object.values(time).reduce((a, b) => a + b, 0) < 1; + if (isTimeOptionZero) return '시간은 필수로 입력해야 합니다.'; +}; diff --git a/frontend/src/components/comment/CommentList/CommentTextForm/index.tsx b/frontend/src/components/comment/CommentList/CommentTextForm/index.tsx index dc0a66553..01d552896 100644 --- a/frontend/src/components/comment/CommentList/CommentTextForm/index.tsx +++ b/frontend/src/components/comment/CommentList/CommentTextForm/index.tsx @@ -13,6 +13,8 @@ import Toast from '@components/common/Toast'; import { COMMENT } from '@constants/comment'; +import { deleteOverlappingNewLine } from '@utils/post/deleteOverlappingNewLine'; + import * as S from './style'; interface CommentTextFormProps { commentId: number; @@ -48,10 +50,10 @@ export default function CommentTextForm({ const updateComment = isEdit ? () => { - editComment({ ...initialComment, content }); + editComment({ ...initialComment, content: deleteOverlappingNewLine(content) }); } : () => { - createComment({ content }); + createComment({ content: deleteOverlappingNewLine(content) }); }; useEffect(() => { diff --git a/frontend/src/components/common/Modal/Modal.stories.tsx b/frontend/src/components/common/Modal/Modal.stories.tsx index 2a4870dbf..5d884f9a5 100644 --- a/frontend/src/components/common/Modal/Modal.stories.tsx +++ b/frontend/src/components/common/Modal/Modal.stories.tsx @@ -4,6 +4,8 @@ import { useEffect, useState } from 'react'; import { styled } from 'styled-components'; +import { MAX_DEADLINE } from '@constants/post'; + import SquareButton from '../SquareButton'; import TimePickerOptionList from '../TimePickerOptionList'; @@ -181,7 +183,7 @@ export const WithTimePicker = () => { X - 최대 3일을 넘을 수 없습니다. + 최대 {MAX_DEADLINE}일을 넘을 수 없습니다. diff --git a/frontend/src/components/common/Post/index.tsx b/frontend/src/components/common/Post/index.tsx index 667511ac4..ca8889897 100644 --- a/frontend/src/components/common/Post/index.tsx +++ b/frontend/src/components/common/Post/index.tsx @@ -12,7 +12,6 @@ import WrittenVoteOptionList from '@components/optionList/WrittenVoteOptionList' import { PATH } from '@constants/path'; import { POST } from '@constants/vote'; -import { convertImageUrlToServerUrl } from '@utils/post/convertImageUrlToServerUrl'; import { linkifyText } from '@utils/post/formatContentLink'; import { checkClosedPost, convertTimeToWord } from '@utils/time'; @@ -144,7 +143,7 @@ export default function Post({ postInfo, isPreview }: PostProps) { aria-label={`작성일시 ${convertTimeToWord(createTime)}`} tabIndex={isPreviewTabIndex} > - {convertTimeToWord(createTime)} + {`${convertTimeToWord(createTime)} |`} {linkifyText(content)} - {!isPreview && imageUrl && ( - - )} + {!isPreview && imageUrl && } ` `; export const Image = styled.img` - width: 100%; + width: 80%; border-radius: 4px; margin-bottom: 10px; + border: 1px solid var(--gray); + align-self: center; aspect-ratio: 1/1; object-fit: contain; diff --git a/frontend/src/components/common/TimePickerOptionList/TimePickerOption/constants.ts b/frontend/src/components/common/TimePickerOptionList/TimePickerOption/constants.ts index db46ae862..8e8ff7e0a 100644 --- a/frontend/src/components/common/TimePickerOptionList/TimePickerOption/constants.ts +++ b/frontend/src/components/common/TimePickerOptionList/TimePickerOption/constants.ts @@ -1,7 +1,9 @@ +import { MAX_DEADLINE } from '@constants/post'; + export const TIMEBOX_CHILD_HEIGHT = 33; export const TIME_UNIT: { [key: string]: number } = { - day: 3, + day: MAX_DEADLINE, hour: 24, minute: 60, }; diff --git a/frontend/src/components/common/TimePickerOptionList/index.tsx b/frontend/src/components/common/TimePickerOptionList/index.tsx index 7969dedf2..99ed2354d 100644 --- a/frontend/src/components/common/TimePickerOptionList/index.tsx +++ b/frontend/src/components/common/TimePickerOptionList/index.tsx @@ -1,21 +1,20 @@ import React, { Dispatch } from 'react'; +import { Time } from '@type/post'; + +import { MAX_DEADLINE } from '@constants/post'; + import * as S from './style'; import TimePickerOption from './TimePickerOption'; -interface Time { - day: number; - hour: number; - minute: number; -} - interface TimePickerOptionListProps { time: Time; setTime: Dispatch>; } export default function TimePickerOptionList({ time, setTime }: TimePickerOptionListProps) { - const { day, hour, minute } = time; + const changedTime = + time.day === MAX_DEADLINE ? { day: MAX_DEADLINE - 1, hour: 23, minute: 59 } : time; const updateTime = (option: string, updatedTime: number) => { setTime(prev => ({ @@ -27,19 +26,19 @@ export default function TimePickerOptionList({ time, setTime }: TimePickerOption return ( - {Object.entries(time).map(([key, value]) => ( + {Object.entries(changedTime).map(([key, value]) => ( + /> ))} -

{day}일

-

{hour}시

-

{minute}분

후 마감 +

{changedTime.day}일

+

{changedTime.hour}시

+

{changedTime.minute}분

후 마감
); diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx index d3329025f..52a123c22 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx @@ -1,4 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta } from '@storybook/react'; + +import { useRef } from 'react'; import OptionUploadImageButton from '.'; @@ -7,8 +9,16 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; -export const Default: Story = { - render: () => , +export const Default = () => { + const ref = useRef([]); + + return ( + + ); }; diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx index 7cf34b3d7..ff9437305 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useRef } from 'react'; +import React, { MutableRefObject } from 'react'; import photoIcon from '@assets/photo_white.svg'; @@ -7,29 +7,36 @@ import * as S from './style'; interface OptionUploadImageButtonProps extends React.InputHTMLAttributes { optionId: number; isImageVisible: boolean; + contentInputRefList: MutableRefObject; + index: number; } export default function OptionUploadImageButton({ optionId, isImageVisible, + contentInputRefList, + index, ...rest }: OptionUploadImageButtonProps) { - const inputRef = useRef(null); const id = optionId.toString(); - const handleButtonClick = (e: MouseEvent) => { - e.preventDefault(); - inputRef.current && inputRef.current.click(); - }; - return ( - - + { + contentInputRefList.current[index] = ele; + }} + {...rest} + /> ); } diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx index 7ce3b3f9d..5cfda953e 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx @@ -1,4 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta } from '@storybook/react'; + +import { useRef } from 'react'; import WritingVoteOption from '.'; @@ -7,10 +9,11 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; -export const IsDeletable: Story = { - render: () => ( +export const IsDeletable = () => { + const ref = useRef([]); + + return ( - ), + ); }; -export const IsNotDeletable: Story = { - render: () => ( +export const IsNotDeletable = () => { + const ref = useRef([]); + + return ( - ), + ); }; -export const ShowImage: Story = { - render: () => ( +export const ShowImage = () => { + const ref = useRef([]); + + return ( {}} @@ -56,6 +67,8 @@ export const ShowImage: Story = { 만 장마가 와서 취소했어요. 여행을 별로 좋" isDeletable={true} imageUrl="https://source.unsplash.com/random" + contentInputRefList={ref} + index={0} /> - ), + ); }; diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx index d3bfa6eba..ddd51bfb7 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from 'react'; +import { ChangeEvent, MutableRefObject } from 'react'; import { POST_OPTION_POLICY } from '@constants/policyMessage'; @@ -16,6 +16,8 @@ interface WritingVoteOptionProps { handleRemoveImageClick: () => void; handleUploadImage: (event: ChangeEvent) => void; imageUrl: string; + contentInputRefList: MutableRefObject; + index: number; } const MAX_WRITING_LENGTH = 50; @@ -30,6 +32,8 @@ export default function WritingVoteOption({ handleRemoveImageClick, handleUploadImage, imageUrl, + contentInputRefList, + index, }: WritingVoteOptionProps) { return ( @@ -52,6 +56,8 @@ export default function WritingVoteOption({ isImageVisible={imageUrl.length > 0} optionId={optionId} onChange={handleUploadImage} + contentInputRefList={contentInputRefList} + index={index} /> {imageUrl && ( diff --git a/frontend/src/components/optionList/WritingVoteOptionList/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/index.tsx index a579ee04a..d6944bc35 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/index.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from 'react'; +import { ChangeEvent, MutableRefObject } from 'react'; import { WritingVoteOptionType } from '@hooks/useWritingOption'; @@ -20,12 +20,20 @@ interface WritingVoteOptionListProps { deleteOption: (optionId: number) => void; removeImage: (optionId: number) => void; handleUploadImage: (event: ChangeEvent, optionId: number) => void; + contentInputRefList: MutableRefObject; }; } export default function WritingVoteOptionList({ writingOptionHook }: WritingVoteOptionListProps) { - const { optionList, addOption, writingOption, deleteOption, removeImage, handleUploadImage } = - writingOptionHook; + const { + optionList, + addOption, + writingOption, + deleteOption, + removeImage, + handleUploadImage, + contentInputRefList, + } = writingOptionHook; const isDeletable = optionList.length > MINIMUM_COUNT; return ( @@ -44,6 +52,8 @@ export default function WritingVoteOptionList({ writingOptionHook }: WritingVote handleUploadImage(event, optionItem.id) } imageUrl={optionItem.imageUrl} + contentInputRefList={contentInputRefList} + index={index} /> ))} {optionList.length < MAXIMUM_COUNT && ( diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx index fa6f6223d..63e0b4dd9 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/index.tsx @@ -1,5 +1,3 @@ -import { convertImageUrlToServerUrl } from '@utils/post/convertImageUrlToServerUrl'; - import ProgressBar from './ProgressBar'; import * as S from './style'; @@ -33,9 +31,7 @@ export default function WrittenVoteOption({ $isSelected={isSelected} onClick={handleVoteClick} > - {!isPreview && imageUrl && ( - - )} + {!isPreview && imageUrl && } {isPreview ? ( {text} ) : ( diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts index abe19cfdc..1694f14b9 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts @@ -25,9 +25,10 @@ export const Container = styled.button<{ $isSelected: boolean }>` export const Image = styled.img` border-radius: 4px; + border: 1px solid var(--gray); margin-bottom: 10px; - width: 100%; + width: 80%; aspect-ratio: 1/1; object-fit: contain; diff --git a/frontend/src/constants/policyMessage.ts b/frontend/src/constants/policyMessage.ts index 1351df7f8..4a8f7f693 100644 --- a/frontend/src/constants/policyMessage.ts +++ b/frontend/src/constants/policyMessage.ts @@ -1,3 +1,5 @@ +import { MAX_DEADLINE } from './post'; + export const NICKNAME_POLICY = { LETTER_AMOUNT: '2자에서 15자 이내로 입력해주세요.', NO_DUPLICATION: '중복된 닉네임은 사용할 수 없습니다.', @@ -33,7 +35,7 @@ export const POST_OPTION_POLICY = { }; export const POST_DEADLINE_POLICY = { - DEFAULT: '3일 이내로 마감시간을 정해주세요.', + DEFAULT: `${MAX_DEADLINE}일 이내로 마감시간을 정해주세요.`, }; export const CONTENT_PLACEHOLDER = [ diff --git a/frontend/src/constants/post.ts b/frontend/src/constants/post.ts index cca99a1bc..ae4f7cc59 100644 --- a/frontend/src/constants/post.ts +++ b/frontend/src/constants/post.ts @@ -66,4 +66,5 @@ export const DEFAULT_KEYWORD = ''; export const CATEGORY_COUNT_LIMIT = 3; -export const IMAGE_BASE_URL = `${process.env.VOTOGETHER_BASE_URL.replace(/api\./, '')}/`; +//단위는 0일 +export const MAX_DEADLINE = 14; diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index 4083c08ff..6a91a986a 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, useState } from 'react'; +import React, { ChangeEvent, useRef, useState } from 'react'; import { uploadImage } from '@utils/post/uploadImage'; @@ -14,19 +14,24 @@ const MIN_COUNT = 2; const MAX_COUNT = 5; const INIT_OPTION_LIST = [ - { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' }, - { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' }, + { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '', isServerId: false }, + { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '', isServerId: false }, ]; -export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = INIT_OPTION_LIST) => { - const [optionList, setOptionList] = useState(initialOptionList); +export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => { + const [optionList, setOptionList] = useState( + initialOptionList + ? initialOptionList.map(option => ({ ...option, isServerId: true })) + : INIT_OPTION_LIST + ); + const contentInputRefList = useRef([]); const addOption = () => { if (optionList.length >= MAX_COUNT) return; const updatedOptionList = [ ...optionList, - { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' }, + { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '', isServerId: false }, ]; setOptionList(updatedOptionList); @@ -76,6 +81,10 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN }); setOptionList(updatedOptionList); + contentInputRefList.current && + contentInputRefList.current.forEach(inputElement => { + if (inputElement?.id === optionId.toString()) inputElement.value = ''; + }); }; const setPreviewImageUrl = (optionId: number) => (imageUrl: string) => { @@ -107,5 +116,13 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN }); }; - return { optionList, addOption, writingOption, deleteOption, removeImage, handleUploadImage }; + return { + optionList, + addOption, + writingOption, + deleteOption, + removeImage, + handleUploadImage, + contentInputRefList, + }; }; diff --git a/frontend/src/styles/globalStyle.ts b/frontend/src/styles/globalStyle.ts index 107e4a39f..6ed9b3b6f 100644 --- a/frontend/src/styles/globalStyle.ts +++ b/frontend/src/styles/globalStyle.ts @@ -40,8 +40,8 @@ export const GlobalStyle = createGlobalStyle` /* Fonts *****************************************/ --text-title: 600 2rem/2.4rem san-serif; --text-subtitle: 600 1.8rem/2.8rem san-serif; - --text-body: 400 1.6rem/2.4rem san-serif; - --text-caption: 400 1.4rem/2rem san-serif; + --text-body: 400 1.7rem/2.4rem san-serif; + --text-caption: 400 1.6rem/2rem san-serif; --text-small: 400 1.2rem/1.8rem san-serif; } `; diff --git a/frontend/src/types/post.ts b/frontend/src/types/post.ts index 8d1cb8d22..3e35c3dd3 100644 --- a/frontend/src/types/post.ts +++ b/frontend/src/types/post.ts @@ -69,3 +69,9 @@ export interface PostListByOptionalOption { categoryId: number; keyword: string; } + +export interface Time { + day: number; + hour: number; + minute: number; +} diff --git a/frontend/src/utils/post/convertImageUrlToServerUrl.ts b/frontend/src/utils/post/convertImageUrlToServerUrl.ts deleted file mode 100644 index ad991319e..000000000 --- a/frontend/src/utils/post/convertImageUrlToServerUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IMAGE_BASE_URL } from '@constants/post'; - -export const convertImageUrlToServerUrl = (imageUrl: string) => { - return `${IMAGE_BASE_URL}${imageUrl}`; -}; - -export const convertServerUrlToImageUrl = (imageUrl: string) => { - return imageUrl.replace(IMAGE_BASE_URL, ''); -}; diff --git a/frontend/src/utils/post/deleteOverlappingNewLine.ts b/frontend/src/utils/post/deleteOverlappingNewLine.ts new file mode 100644 index 000000000..65bb26e20 --- /dev/null +++ b/frontend/src/utils/post/deleteOverlappingNewLine.ts @@ -0,0 +1,3 @@ +export const deleteOverlappingNewLine = (text: string) => { + return text.replace(/(\n{5,})/g, '\n\n\n\n\n'); +}; diff --git a/frontend/src/utils/post/formatTime.ts b/frontend/src/utils/post/formatTime.ts index 8353817c7..15d185224 100644 --- a/frontend/src/utils/post/formatTime.ts +++ b/frontend/src/utils/post/formatTime.ts @@ -1,12 +1,7 @@ -interface Time { - day: number; - hour: number; - minute: number; -} +import { Time } from '@type/post'; export function addTimeToDate(addTime: Time, baseTime: Date) { const { day, hour, minute } = addTime; - if (day === 0 && hour === 0 && minute === 0) return; const newTime = new Date(baseTime); @@ -22,11 +17,3 @@ export function addTimeToDate(addTime: Time, baseTime: Date) { return `${newYear}-${newMonth}-${newDay} ${newHour}:${newMinute}`; } - -export function formatTimeWithOption(option: string) { - if (option === '10분') return { day: 0, hour: 0, minute: 10 }; - else if (option === '30분') return { day: 0, hour: 0, minute: 30 }; - else if (option === '1시간') return { day: 0, hour: 1, minute: 0 }; - else if (option === '6시간') return { day: 0, hour: 6, minute: 0 }; - else return { day: 1, hour: 0, minute: 0 }; -} diff --git a/frontend/src/utils/post/getDeadlineTime.ts b/frontend/src/utils/post/getDeadlineTime.ts index 4952300ff..d6675c31e 100644 --- a/frontend/src/utils/post/getDeadlineTime.ts +++ b/frontend/src/utils/post/getDeadlineTime.ts @@ -1,12 +1,6 @@ -export const getDeadlineTime = ({ - day, - hour, - minute, -}: { - day: number; - hour: number; - minute: number; -}) => { +import { Time } from '@type/post'; + +export const getDeadlineTime = ({ day, hour, minute }: Time) => { const timeMessage = []; if (day < 0 || hour < 0 || minute < 0) { diff --git a/frontend/src/utils/post/getSelectedTimeOption.ts b/frontend/src/utils/post/getSelectedTimeOption.ts index 6729b144c..d5c0e2c6e 100644 --- a/frontend/src/utils/post/getSelectedTimeOption.ts +++ b/frontend/src/utils/post/getSelectedTimeOption.ts @@ -1,20 +1,13 @@ -import { DeadlineOption } from '@components/PostForm/constants'; +import { Time } from '@type/post'; -export const getSelectedTimeOption = ({ - day, - hour, - minute, -}: { - day: number; - hour: number; - minute: number; -}): DeadlineOption | '사용자지정' | null => { - if (day === 0 && hour === 0 && minute === 0) return null; - if (day === 0 && hour === 0 && minute === 10) return '10분'; - if (day === 0 && hour === 0 && minute === 30) return '30분'; - if (day === 0 && hour === 1 && minute === 0) return '1시간'; - if (day === 0 && hour === 6 && minute === 0) return '6시간'; - if (day === 1 && hour === 0 && minute === 0) return '1일'; +import { DEADLINE_OPTION, DeadlineOptionName } from '@components/PostForm/constants'; - return '사용자지정'; +export const getSelectedTimeOption = (time: Time): DeadlineOptionName | '사용자지정' | null => { + if (time.day === 0 && time.hour === 0 && time.minute === 0) return null; + + const stringTime = JSON.stringify(time); + + return ( + DEADLINE_OPTION.find(option => JSON.stringify(option.time) === stringTime)?.name ?? '사용자지정' + ); }; diff --git a/frontend/src/utils/time.ts b/frontend/src/utils/time.ts index 6be18c6b3..4da6783e8 100644 --- a/frontend/src/utils/time.ts +++ b/frontend/src/utils/time.ts @@ -1,3 +1,5 @@ +import { MAX_DEADLINE } from '@constants/post'; + import { addTimeToDate } from './post/formatTime'; const convertNowTimeToNumber = () => { @@ -16,6 +18,7 @@ const convertTimeFromStringToNumber = (date: string) => { const dateComponents = date.split(' '); const datePieces = dateComponents[0].split('-'); const timePieces = dateComponents[1].split(':'); + return Number([...datePieces, ...timePieces].join('')); }; @@ -29,40 +32,48 @@ type TimeType = 'day' | 'hour' | 'minute'; //시간 수정을 할 수 없다면 true export const checkIrreplaceableTime = (addTime: Record, createTime: string) => { - const changedDeadline = addTimeToDate(addTime, new Date(createTime)); - // changedDeadline가 undefined인 경우는 작성일시에서 시간이 더해지지 않았을 경우라 거절 - if (!changedDeadline) return true; + const transCreateTime = createTime.split('-').join('/'); + const changedDeadline = addTimeToDate(addTime, new Date(transCreateTime)); - const limitDeadline = addTimeToDate({ day: 3, hour: 0, minute: 0 }, new Date(createTime))!; + //마감시한이 0시간 0분 0초 추가된다면 거절 + if (Object.values(addTime).every(time => time === 0)) return true; + + const limitDeadline = addTimeToDate( + { day: MAX_DEADLINE, hour: 0, minute: 0 }, + new Date(transCreateTime) + )!; const changedDeadlineNumber = convertTimeFromStringToNumber(changedDeadline); const limitDeadlineNumber = convertTimeFromStringToNumber(limitDeadline); - //작성일시로부터 3일된 일시보다 지정하고자 하는 일시가 크다면 거절 - if (changedDeadlineNumber >= limitDeadlineNumber) return true; + //작성일시로부터 마감시간 최대일시보다 지정하고자 하는 일시가 크다면 거절 + if (changedDeadlineNumber > limitDeadlineNumber) return true; //지금 일시보다 지정하고자 하는 일시가 작다면 거절 return changedDeadlineNumber <= convertNowTimeToNumber(); }; const time = { - day: 3, hour: 24, minute: 60, }; -export const convertTimeToWord = (date: string) => { - const targetDate = new Date(date); - const currentDate = new Date(); +export const convertTimeToWord = (date: string, currentDate: Date = new Date()) => { + const targetDate = new Date(date.split('-').join('/')); //분 단위로 산출됨 const timeDifference = Math.floor((targetDate.getTime() - currentDate.getTime()) / 60000); if (timeDifference === 0) return '지금'; - const afterBefore = timeDifference > 0 ? '후 마감' : '전 작성 |'; + const afterBefore = timeDifference > 0 ? '후 마감' : '전 작성'; const positiveTimeDifference = Math.abs(timeDifference); + if (Math.round(positiveTimeDifference / (time.hour * time.minute)) > 0) { + const day = Math.round(positiveTimeDifference / (time.hour * time.minute)); + return day >= 30 ? `${date.split(' ')[0]}` : `${day}일 ${afterBefore}`; + } + if (Math.round(positiveTimeDifference / (time.hour * time.minute)) > 0) return `${Math.round(positiveTimeDifference / (time.hour * time.minute))}일 ${afterBefore}`;