From 18fdd264d59d02935d1633e470014c470ffd6397 Mon Sep 17 00:00:00 2001 From: chsua Date: Fri, 8 Sep 2023 14:49:33 +0900 Subject: [PATCH 01/48] =?UTF-8?q?design:=20(#541)=20=EA=B8=80=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=98=A4=EB=A5=B8?= =?UTF-8?q?=EC=AA=BD=20=ED=8C=A8=EB=94=A9=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F?= =?UTF-8?q?=20=ED=97=A4=EB=8D=94=20=EA=B8=80=EC=94=A8=20=ED=81=AC=EA=B8=B0?= =?UTF-8?q?=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - grid 속성 설정 + 내부 자식 width: 100% 설정 - 헤더 내 취소/제출 버튼을 헤더 글씨 버튼 컴포넌트로 수정(다른 헤더와 통일성 위해) --- frontend/src/components/PostForm/index.tsx | 7 ++++--- frontend/src/components/PostForm/style.ts | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 390aad3b4..63806c8ac 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -14,6 +14,7 @@ import { useWritingOption } from '@hooks/useWritingOption'; import ErrorBoundary from '@pages/ErrorBoundary'; +import HeaderTextButton from '@components/common/HeaderTextButton'; import Modal from '@components/common/Modal'; import NarrowTemplateHeader from '@components/common/NarrowTemplateHeader'; import SquareButton from '@components/common/SquareButton'; @@ -195,10 +196,10 @@ export default function PostForm({ data, mutate }: PostFormProps) { <> - navigate('/')}>취소 - + navigate('/')}>취소 + 저장 - +
diff --git a/frontend/src/components/PostForm/style.ts b/frontend/src/components/PostForm/style.ts index 95b3540b9..74e0c52ec 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; From 14203f875f5bb4a110b308a1f804bb7d0f95bb5b Mon Sep 17 00:00:00 2001 From: chsua Date: Fri, 8 Sep 2023 15:15:25 +0900 Subject: [PATCH 02/48] =?UTF-8?q?design:=20(#482)=20=20safari=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B8=80=20=EC=93=B8=EB=95=8C=20=ED=99=95=EB=8C=80?= =?UTF-8?q?=EB=90=98=EC=96=B4=EC=84=9C=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인: 사파리에서는 input, textArea의 폰트 크기가 16px 미만이면 자동 zoom이 됨 --- frontend/src/styles/globalStyle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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; } `; From c29654be5572f57a9eb464d7c0647ecfe0d6dcd2 Mon Sep 17 00:00:00 2001 From: chsua Date: Fri, 8 Sep 2023 16:14:50 +0900 Subject: [PATCH 03/48] =?UTF-8?q?fix:=20(#482)=20=EC=82=AC=ED=8C=8C?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EC=84=9C=20=EB=82=A0=EC=A7=9C=EA=B0=80=20nan?= =?UTF-8?q?=EB=9C=A8=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + 현재와 30일 이상 차이나는 날짜는 "yyyy-mm-dd"형식 문자열로 그려지게 수정 --- frontend/src/components/common/Post/index.tsx | 2 +- frontend/src/utils/time.ts | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/common/Post/index.tsx b/frontend/src/components/common/Post/index.tsx index 4d78c9509..0e4f5cd98 100644 --- a/frontend/src/components/common/Post/index.tsx +++ b/frontend/src/components/common/Post/index.tsx @@ -147,7 +147,7 @@ export default function Post({ postInfo, isPreview }: PostProps) { aria-label={`작성일시 ${convertTimeToWord(createTime)}`} tabIndex={isPreviewTabIndex} > - {convertTimeToWord(createTime)} + {`${convertTimeToWord(createTime)} |`} { const dateComponents = date.split(' '); const datePieces = dateComponents[0].split('-'); const timePieces = dateComponents[1].split(':'); + return Number([...datePieces, ...timePieces].join('')); }; @@ -29,11 +30,12 @@ type TimeType = 'day' | 'hour' | 'minute'; //시간 수정을 할 수 없다면 true export const checkIrreplaceableTime = (addTime: Record, createTime: string) => { - const changedDeadline = addTimeToDate(addTime, new Date(createTime)); + const transCreateTime = createTime.split('-').join('/'); + const changedDeadline = addTimeToDate(addTime, new Date(transCreateTime)); // changedDeadline가 undefined인 경우는 작성일시에서 시간이 더해지지 않았을 경우라 거절 if (!changedDeadline) return true; - const limitDeadline = addTimeToDate({ day: 3, hour: 0, minute: 0 }, new Date(createTime))!; + const limitDeadline = addTimeToDate({ day: 3, hour: 0, minute: 0 }, new Date(transCreateTime))!; const changedDeadlineNumber = convertTimeFromStringToNumber(changedDeadline); const limitDeadlineNumber = convertTimeFromStringToNumber(limitDeadline); @@ -50,19 +52,23 @@ const time = { 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}`; From e523cfb55766ddacf7854f9f886d694200625979 Mon Sep 17 00:00:00 2001 From: chsua Date: Fri, 8 Sep 2023 16:15:46 +0900 Subject: [PATCH 04/48] =?UTF-8?q?test:=20=EB=82=A0=EC=A7=9C=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=EA=B3=BC=20=ED=98=84=EC=9E=AC=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EC=9D=84=20=EB=B9=84=EA=B5=90=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=EC=9D=84=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/__test__/utilTimeTest.test.ts | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 frontend/__test__/utilTimeTest.test.ts diff --git a/frontend/__test__/utilTimeTest.test.ts b/frontend/__test__/utilTimeTest.test.ts new file mode 100644 index 000000000..76be9ad6e --- /dev/null +++ b/frontend/__test__/utilTimeTest.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일 후 마감'); + }); +}); From 50dfd03babee2e939d1b2f3e43e1f16b2b96ce69 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 15:38:54 +0900 Subject: [PATCH 05/48] =?UTF-8?q?design:=20(#541)=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=EB=90=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=86=8D=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EA=B0=80=EB=A1=9C=20=EA=B8=B8=EC=9D=B4=2080%?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Post/style.ts | 4 +++- .../WrittenVoteOptionList/WrittenVoteOption/style.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/common/Post/style.ts b/frontend/src/components/common/Post/style.ts index df7a5c798..eba21dda4 100644 --- a/frontend/src/components/common/Post/style.ts +++ b/frontend/src/components/common/Post/style.ts @@ -121,9 +121,11 @@ export const DetailLink = styled(Link)<{ $isPreview: boolean }>` `; 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: cover; diff --git a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts index a600844f1..c4dd18f85 100644 --- a/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts +++ b/frontend/src/components/optionList/WrittenVoteOptionList/WrittenVoteOption/style.ts @@ -25,12 +25,14 @@ 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: cover; + align-self: center; @media (min-width: ${theme.breakpoint.md}) { margin-bottom: 20px; From 4cd149d56e1dbc8c66074d2e540c918626c70603 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 16:55:34 +0900 Subject: [PATCH 06/48] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=A7=88=EA=B0=90=20=EC=A0=9C=ED=95=9C=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=A0=95=EC=B1=85=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=203=EC=9D=BC=20->=2014=EC=9D=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공통된 Time interface type폴더로 이동 및 적용 - 마감 제한 시간 상수화 - 마감일자 optionList 형태 변경 --- frontend/src/components/PostForm/constants.ts | 52 ++++++++++++++++++- frontend/src/components/PostForm/index.tsx | 32 ++++++------ .../components/common/Modal/Modal.stories.tsx | 4 +- .../TimePickerOption/constants.ts | 4 +- .../common/TimePickerOptionList/index.tsx | 10 ++-- frontend/src/constants/policyMessage.ts | 4 +- frontend/src/constants/post.ts | 3 ++ frontend/src/types/post.ts | 6 +++ frontend/src/utils/post/formatTime.ts | 6 +-- frontend/src/utils/post/getDeadlineTime.ts | 12 ++--- .../src/utils/post/getSelectedTimeOption.ts | 23 +++----- frontend/src/utils/time.ts | 10 ++-- 12 files changed, 104 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/PostForm/constants.ts b/frontend/src/components/PostForm/constants.ts index 595ecad57..60ce5c42b 100644 --- a/frontend/src/components/PostForm/constants.ts +++ b/frontend/src/components/PostForm/constants.ts @@ -1,5 +1,53 @@ -export type DeadlineOption = '10분' | '30분' | '1시간' | '6시간' | '1일'; +import { Time } from '@type/post'; -export const DEADLINE_OPTION: DeadlineOption[] = ['10분', '30분', '1시간', '6시간', '1일']; +export type DeadlineOptionName = '1일' | '3일' | '5일' | '7일' | '14일'; + +export interface DeadlineOptionInfo { + name: DeadlineOptionName; + time: Time; +} + +export const DEADLINE_OPTION: DeadlineOptionInfo[] = [ + { + name: '1일', + time: { + day: 1, + hour: 0, + minute: 0, + }, + }, + { + name: '3일', + time: { + day: 3, + hour: 0, + minute: 0, + }, + }, + { + name: '5일', + time: { + day: 5, + hour: 0, + minute: 0, + }, + }, + { + name: '7일', + time: { + day: 7, + hour: 0, + minute: 0, + }, + }, + { + name: '14일', + time: { + day: 14, + hour: 0, + minute: 0, + }, + }, +]; export const MAX_FILE_SIZE = 1500000; diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 63806c8ac..95f764cf4 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -28,7 +28,7 @@ import { POST_DEADLINE_POLICY, POST_TITLE_POLICY, } from '@constants/policyMessage'; -import { CATEGORY_COUNT_LIMIT, POST_CONTENT, POST_TITLE } from '@constants/post'; +import { CATEGORY_COUNT_LIMIT, MAX_DEADLINE, POST_CONTENT, POST_TITLE } from '@constants/post'; import { calculateDeadlineTime } from '@utils/post/calculateDeadlineTime'; import { checkWriter } from '@utils/post/checkWriter'; @@ -36,13 +36,13 @@ import { convertImageUrlToServerUrl, convertServerUrlToImageUrl, } from '@utils/post/convertImageUrlToServerUrl'; -import { addTimeToDate, formatTimeWithOption } from '@utils/post/formatTime'; +import { addTimeToDate } from '@utils/post/formatTime'; import { getDeadlineTime } from '@utils/post/getDeadlineTime'; import { getSelectedTimeOption } from '@utils/post/getSelectedTimeOption'; import { checkIrreplaceableTime } from '@utils/time'; import CategoryWrapper from './CategoryWrapper'; -import { DEADLINE_OPTION, DeadlineOption } from './constants'; +import { DEADLINE_OPTION, DeadlineOptionInfo, DeadlineOptionName } from './constants'; import ContentImagePart from './ContentImageSection'; import * as S from './style'; @@ -76,9 +76,9 @@ export default function PostForm({ data, mutate }: PostFormProps) { ); 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(); @@ -102,12 +102,12 @@ export default function PostForm({ data, mutate }: PostFormProps) { const { text: writingContent, handleTextChange: handleContentChange } = 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); }; @@ -248,14 +248,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 && ( @@ -265,13 +265,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/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/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..57258248d 100644 --- a/frontend/src/components/common/TimePickerOptionList/index.tsx +++ b/frontend/src/components/common/TimePickerOptionList/index.tsx @@ -1,14 +1,10 @@ import React, { Dispatch } from 'react'; +import { Time } from '@type/post'; + import * as S from './style'; import TimePickerOption from './TimePickerOption'; -interface Time { - day: number; - hour: number; - minute: number; -} - interface TimePickerOptionListProps { time: Time; setTime: Dispatch>; @@ -33,7 +29,7 @@ export default function TimePickerOptionList({ time, setTime }: TimePickerOption currentTime={value} option={key} handlePickTime={updateTime} - > + /> ))} diff --git a/frontend/src/constants/policyMessage.ts b/frontend/src/constants/policyMessage.ts index 2565e0ba5..c293faa47 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: '중복된 닉네임은 사용할 수 없습니다.', @@ -32,7 +34,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..290ec675c 100644 --- a/frontend/src/constants/post.ts +++ b/frontend/src/constants/post.ts @@ -67,3 +67,6 @@ 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/types/post.ts b/frontend/src/types/post.ts index 44dd2ede5..fc6fe0eaa 100644 --- a/frontend/src/types/post.ts +++ b/frontend/src/types/post.ts @@ -65,3 +65,9 @@ export interface PostListByOptionalOption { categoryId: number; keyword: string; } + +export interface Time { + day: number; + hour: number; + minute: number; +} diff --git a/frontend/src/utils/post/formatTime.ts b/frontend/src/utils/post/formatTime.ts index 8353817c7..17c898c5b 100644 --- a/frontend/src/utils/post/formatTime.ts +++ b/frontend/src/utils/post/formatTime.ts @@ -1,8 +1,4 @@ -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; 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..21e136798 100644 --- a/frontend/src/utils/post/getSelectedTimeOption.ts +++ b/frontend/src/utils/post/getSelectedTimeOption.ts @@ -1,20 +1,9 @@ -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; + + return DEADLINE_OPTION.find(option => option.time === time)?.name ?? '사용자지정'; }; diff --git a/frontend/src/utils/time.ts b/frontend/src/utils/time.ts index ce18732ea..b5a43dbc9 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 = () => { @@ -35,11 +37,14 @@ export const checkIrreplaceableTime = (addTime: Record, create // changedDeadline가 undefined인 경우는 작성일시에서 시간이 더해지지 않았을 경우라 거절 if (!changedDeadline) return true; - const limitDeadline = addTimeToDate({ day: 3, hour: 0, minute: 0 }, new Date(transCreateTime))!; + 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; //지금 일시보다 지정하고자 하는 일시가 작다면 거절 @@ -47,7 +52,6 @@ export const checkIrreplaceableTime = (addTime: Record, create }; const time = { - day: 3, hour: 24, minute: 60, }; From e60f3254f143898efac5ce302a6379993fb45c9f Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 16:57:31 +0900 Subject: [PATCH 07/48] =?UTF-8?q?feat:=20=EB=B3=B8=EB=AC=B8=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=EC=97=90=20=EC=97=B0=EC=86=8D=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=EC=9D=80=201=ED=9A=8C=20=EA=B0=9C=ED=96=89?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B2=98=EB=A6=AC=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/index.tsx | 3 ++- frontend/src/utils/post/deleteOverlappingNewLine.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/post/deleteOverlappingNewLine.ts diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 95f764cf4..57071edc6 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -36,6 +36,7 @@ import { convertImageUrlToServerUrl, convertServerUrlToImageUrl, } from '@utils/post/convertImageUrlToServerUrl'; +import { deleteOverlappingNewLine } from '@utils/post/deleteOverlappingNewLine'; import { addTimeToDate } from '@utils/post/formatTime'; import { getDeadlineTime } from '@utils/post/getDeadlineTime'; import { getSelectedTimeOption } from '@utils/post/getSelectedTimeOption'; @@ -178,7 +179,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { categoryIds: selectedOptionList.map(option => option.id), title: writingTitle, imageUrl: convertServerUrlToImageUrl(contentImageHook.contentImage), - content: writingContent, + content: deleteOverlappingNewLine(writingContent), postOptions: writingOptionList, deadline: addTimeToDate(time, baseTime), // 글 수정의 경우 작성시간을 기준으로 마감시간 옵션을 더한다. diff --git a/frontend/src/utils/post/deleteOverlappingNewLine.ts b/frontend/src/utils/post/deleteOverlappingNewLine.ts new file mode 100644 index 000000000..58e1490d5 --- /dev/null +++ b/frontend/src/utils/post/deleteOverlappingNewLine.ts @@ -0,0 +1,3 @@ +export const deleteOverlappingNewLine = (text: string) => { + return text.replace(/(\n{2,})/g, '\n'); +}; From 7d8131e8272487efe19e8f5b5879aa70cac5970b Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 16:58:01 +0900 Subject: [PATCH 08/48] =?UTF-8?q?test:=20=EC=97=B0=EC=86=8D=EB=90=9C=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=EC=9D=84=201=ED=9A=8C=20=EA=B0=9C=ED=96=89?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=ED=95=A8=EC=88=98=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/deleteOverlappingNewLine.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/__test__/deleteOverlappingNewLine.test.ts diff --git a/frontend/__test__/deleteOverlappingNewLine.test.ts b/frontend/__test__/deleteOverlappingNewLine.test.ts new file mode 100644 index 000000000..7e2cb65f0 --- /dev/null +++ b/frontend/__test__/deleteOverlappingNewLine.test.ts @@ -0,0 +1,39 @@ +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회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 1회 개행으로 바뀐 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const expectText = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(expectText); + }); + + test('3회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 1회 개행으로 바뀐 결과를 반환한다.', () => { + const text = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const expectText = + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + const changedText = deleteOverlappingNewLine(text); + + expect(changedText).toBe(expectText); + }); +}); From 5b43aec0eedb82b23a1260e933615f9fe7cc8078 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 17:39:40 +0900 Subject: [PATCH 09/48] =?UTF-8?q?test:=20=EB=A7=88=EA=B0=90=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=B2=84=ED=8A=BC=EC=9A=A9=20=EB=B0=B0=EC=97=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/getSelectedTimeOption.test.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) 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일 객체를 입력했을 때 사용자지정을 반환한다.', () => { From 07ab5f82c22432598222516598cf03626fa2b633 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 17:41:08 +0900 Subject: [PATCH 10/48] =?UTF-8?q?fix:=20=EC=84=A0=ED=83=9D=EB=90=9C=20time?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=ED=99=95=EC=9D=B8=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EB=B9=84=EA=B5=90=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EA=B9=8A=EC=9D=80=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/post/getSelectedTimeOption.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/post/getSelectedTimeOption.ts b/frontend/src/utils/post/getSelectedTimeOption.ts index 21e136798..d5c0e2c6e 100644 --- a/frontend/src/utils/post/getSelectedTimeOption.ts +++ b/frontend/src/utils/post/getSelectedTimeOption.ts @@ -5,5 +5,9 @@ import { DEADLINE_OPTION, DeadlineOptionName } from '@components/PostForm/consta export const getSelectedTimeOption = (time: Time): DeadlineOptionName | '사용자지정' | null => { if (time.day === 0 && time.hour === 0 && time.minute === 0) return null; - return DEADLINE_OPTION.find(option => option.time === time)?.name ?? '사용자지정'; + const stringTime = JSON.stringify(time); + + return ( + DEADLINE_OPTION.find(option => JSON.stringify(option.time) === stringTime)?.name ?? '사용자지정' + ); }; From 5053dfb60fcfc5333926f0177f2739ae0566cea2 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 17:42:52 +0900 Subject: [PATCH 11/48] =?UTF-8?q?fix:=20=EC=B5=9C=EB=8C=80=20=EB=A7=88?= =?UTF-8?q?=EA=B0=90=EC=9D=BC=EC=9E=90=20=EB=B2=84=ED=8A=BC=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=ED=9B=84=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20ui=EA=B0=80=20=EC=B6=9C=EB=A0=A5=EB=90=98=EB=8A=94?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/TimePickerOptionList/index.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/common/TimePickerOptionList/index.tsx b/frontend/src/components/common/TimePickerOptionList/index.tsx index 57258248d..99ed2354d 100644 --- a/frontend/src/components/common/TimePickerOptionList/index.tsx +++ b/frontend/src/components/common/TimePickerOptionList/index.tsx @@ -2,6 +2,8 @@ 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'; @@ -11,7 +13,8 @@ interface TimePickerOptionListProps { } 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 => ({ @@ -23,7 +26,7 @@ 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}분

후 마감
); From 82fb1017a4840daf5687975a63be95a22ede128e Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 20:19:47 +0900 Subject: [PATCH 12/48] =?UTF-8?q?fix:=20(#280)=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=82=AC=EC=A7=84=20=EB=93=B1=EB=A1=9D/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=9B=84=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=98=AC=EB=A6=B4=EC=8B=9C=20=EC=95=88?= =?UTF-8?q?=EC=98=AC=EB=9D=BC=EA=B0=80=EB=8A=94=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptionUploadImageButton.stories.tsx | 18 +++++++--- .../WritingVoteOption.stories.tsx | 35 +++++++++++++------ .../WritingVoteOption/index.tsx | 8 ++++- .../WritingVoteOptionList/index.tsx | 16 +++++++-- frontend/src/hooks/useWritingOption.tsx | 17 +++++++-- 5 files changed, 73 insertions(+), 21 deletions(-) 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..ebd896c6b 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(null); + + return ( + + ); }; diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx index 7ce3b3f9d..cb0b71952 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(null); + + return ( - ), + ); }; -export const IsNotDeletable: Story = { - render: () => ( +export const IsNotDeletable = () => { + const ref = useRef(null); + + return ( - ), + ); }; -export const ShowImage: Story = { - render: () => ( +export const ShowImage = () => { + const ref = useRef(null); + + 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..789fc679d 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..b1af98919 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/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index 7211b24ce..c38f04515 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 { MAX_FILE_SIZE } from '@components/PostForm/constants'; @@ -20,6 +20,7 @@ const INIT_OPTION_LIST = [ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = INIT_OPTION_LIST) => { const [optionList, setOptionList] = useState(initialOptionList); + const contentInputRefList = useRef([]); const addOption = () => { if (optionList.length >= MAX_COUNT) return; @@ -76,6 +77,10 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN }); setOptionList(updatedOptionList); + contentInputRefList.current && + contentInputRefList.current.forEach(inputElement => { + if (inputElement?.id === optionId.toString()) inputElement.value = ''; + }); }; const handleUploadImage = (event: React.ChangeEvent, optionId: number) => { @@ -112,5 +117,13 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN }; }; - return { optionList, addOption, writingOption, deleteOption, removeImage, handleUploadImage }; + return { + optionList, + addOption, + writingOption, + deleteOption, + removeImage, + handleUploadImage, + contentInputRefList, + }; }; From c6d54587ed40415cfdbc780a3774ca2afcda7a3d Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 22:19:01 +0900 Subject: [PATCH 13/48] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=EC=8B=9C=EA=B0=84=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=ED=95=A8=EC=88=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/PostForm/validation.ts | 24 +++++++++++++++++++ frontend/src/utils/post/formatTime.ts | 9 ------- 2 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/PostForm/validation.ts diff --git a/frontend/src/components/PostForm/validation.ts b/frontend/src/components/PostForm/validation.ts new file mode 100644 index 000000000..d8fa6ab0f --- /dev/null +++ b/frontend/src/components/PostForm/validation.ts @@ -0,0 +1,24 @@ +import { Time } from '@type/post'; + +import { Option } from '@components/common/MultiSelect/types'; + +export const checkValidationPost = ( + categoryList: Option[], + title: string, + content: string, + optionList: any[], + 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.content.trim() === '')) return '선택지에 글을 입력해주세요.'; + + if (Object.values(time).reduce((a, b) => a + b, 0) < 1) return '시간은 필수로 입력해야 합니다.'; +}; diff --git a/frontend/src/utils/post/formatTime.ts b/frontend/src/utils/post/formatTime.ts index 17c898c5b..15d185224 100644 --- a/frontend/src/utils/post/formatTime.ts +++ b/frontend/src/utils/post/formatTime.ts @@ -2,7 +2,6 @@ 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); @@ -18,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 }; -} From 147d9f05ead06782f79e9f808abac505a0fc2c04 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 22:20:04 +0900 Subject: [PATCH 14/48] =?UTF-8?q?fix:=20=EB=A7=88=EA=B0=90=20=EC=B5=9C?= =?UTF-8?q?=EB=8C=80=20=EC=8B=9C=EA=B0=84(14=EC=9D=BC)=EC=9D=B8=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=A7=88=EA=B0=90=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EC=97=90=20?= =?UTF-8?q?=EA=B1=B8=EB=A6=AC=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/time.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/time.ts b/frontend/src/utils/time.ts index b5a43dbc9..bfd3d38a9 100644 --- a/frontend/src/utils/time.ts +++ b/frontend/src/utils/time.ts @@ -34,8 +34,9 @@ type TimeType = 'day' | 'hour' | 'minute'; export const checkIrreplaceableTime = (addTime: Record, createTime: string) => { const transCreateTime = createTime.split('-').join('/'); const changedDeadline = addTimeToDate(addTime, new Date(transCreateTime)); - // changedDeadline가 undefined인 경우는 작성일시에서 시간이 더해지지 않았을 경우라 거절 - if (!changedDeadline) return true; + + //마감시한이 0시간 0분 0초 추가된다면 거절 + if (Object.values(addTime).every(time => time === 0)) return true; const limitDeadline = addTimeToDate( { day: MAX_DEADLINE, hour: 0, minute: 0 }, @@ -45,7 +46,7 @@ export const checkIrreplaceableTime = (addTime: Record, create const limitDeadlineNumber = convertTimeFromStringToNumber(limitDeadline); //작성일시로부터 마감시간 최대일시보다 지정하고자 하는 일시가 크다면 거절 - if (changedDeadlineNumber >= limitDeadlineNumber) return true; + if (changedDeadlineNumber > limitDeadlineNumber) return true; //지금 일시보다 지정하고자 하는 일시가 작다면 거절 return changedDeadlineNumber <= convertNowTimeToNumber(); From fb575874f80389dac3515f936f13f7ead3af15e1 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 22:22:04 +0900 Subject: [PATCH 15/48] =?UTF-8?q?fix:=20(#280)=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=82=AC=EC=A7=84=20=EB=93=B1=EB=A1=9D/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=9B=84=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=98=AC=EB=9D=BC=EA=B0=80=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 누락된 파일 커밋 --- .../OptionUploadImageButton/index.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx index 7cf34b3d7..483c94b8c 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, { MouseEvent, MutableRefObject } from 'react'; import photoIcon from '@assets/photo_white.svg'; @@ -7,19 +7,22 @@ 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(); + contentInputRefList.current && contentInputRefList.current[index].click(); }; return ( @@ -29,7 +32,18 @@ export default function OptionUploadImageButton({ - + { + if (contentInputRefList.current) { + contentInputRefList.current[index] = ele; + } + }} + {...rest} + /> ); } From ecd874edd9c99f0117e0fd43e928daf7eccae568 Mon Sep 17 00:00:00 2001 From: chsua Date: Sun, 10 Sep 2023 22:22:58 +0900 Subject: [PATCH 16/48] =?UTF-8?q?feat:=20=EA=B8=80=20=EB=93=B1=EB=A1=9D/?= =?UTF-8?q?=EC=88=98=EC=A0=95=20api=20=EC=88=98=EC=A0=95=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20formData=20=ED=98=95=EC=8B=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/index.tsx | 72 ++++++++-------------- 1 file changed, 27 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 57071edc6..6ce12f793 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -46,6 +46,7 @@ import CategoryWrapper from './CategoryWrapper'; import { DEADLINE_OPTION, DeadlineOptionInfo, DeadlineOptionName } from './constants'; import ContentImagePart from './ContentImageSection'; import * as S from './style'; +import { checkValidationPost } from './validation'; interface PostFormProps extends HTMLAttributes { data?: PostInfo; @@ -131,62 +132,43 @@ export default function PostForm({ data, mutate }: PostFormProps) { 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, + writingOptionList, + time + ); + if (errorMessage) return openToast(errorMessage); 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', convertServerUrlToImageUrl(contentImageHook.contentImage)); + writingOptionList.forEach((option, index) => { + formData.append(`postOptions[${index}].content`, 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]); + 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]); + 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: deleteOverlappingNewLine(writingContent), - postOptions: writingOptionList, - deadline: addTimeToDate(time, baseTime), - // 글 수정의 경우 작성시간을 기준으로 마감시간 옵션을 더한다. - // 마감시간 옵션을 선택 안했다면 기존의 마감 시간을 유지한다. - }; - formData.append('request', JSON.stringify(updatedPostTexts)); - mutate(formData); } }; From 1c95560e8ca8b1322a6ab37d5fa9e961cb0dce2f Mon Sep 17 00:00:00 2001 From: chsua Date: Mon, 11 Sep 2023 12:48:44 +0900 Subject: [PATCH 17/48] =?UTF-8?q?feat:=20(#541)=20imageUrl=20=EC=95=9E=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존: 받은 imageUrl에 http-를 붙여야 image가 보여짐. 반대로 http-를 떼서 수정 api를 보내야 함 - 수정: 서버에서 보내주는 url 그대로 받아서 image를 보이고 수정 api를 보냄 --- frontend/src/components/PostForm/index.tsx | 18 ++++++------------ frontend/src/components/common/Post/index.tsx | 5 +---- .../WrittenVoteOption/index.tsx | 6 +----- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 6ce12f793..8a9bae9d8 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -32,10 +32,6 @@ import { CATEGORY_COUNT_LIMIT, MAX_DEADLINE, POST_CONTENT, POST_TITLE } from '@c import { calculateDeadlineTime } from '@utils/post/calculateDeadlineTime'; import { checkWriter } from '@utils/post/checkWriter'; -import { - convertImageUrlToServerUrl, - convertServerUrlToImageUrl, -} from '@utils/post/convertImageUrlToServerUrl'; import { deleteOverlappingNewLine } from '@utils/post/deleteOverlappingNewLine'; import { addTimeToDate } from '@utils/post/formatTime'; import { getDeadlineTime } from '@utils/post/getDeadlineTime'; @@ -67,13 +63,11 @@ export default function PostForm({ data, mutate }: PostFormProps) { } = data ?? {}; const navigate = useNavigate(); - const contentImageHook = useContentImage( - serverImageUrl && convertImageUrlToServerUrl(serverImageUrl) - ); + const contentImageHook = useContentImage(serverImageUrl); const writingOptionHook = useWritingOption( serverVoteInfo?.options.map(option => ({ ...option, - imageUrl: option.imageUrl ? convertImageUrlToServerUrl(option.imageUrl) : '', + imageUrl: option.imageUrl ?? '', })) ); @@ -129,7 +123,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { const formData = new FormData(); const writingOptionList = writingOptionHook.optionList.map(({ text, imageUrl }, index) => { - return { content: text, imageUrl: convertServerUrlToImageUrl(imageUrl) }; + return { content: text, imageUrl }; }); //예외처리 @@ -152,7 +146,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { ); formData.append('title', writingTitle); formData.append('content', deleteOverlappingNewLine(writingContent)); - formData.append('imageUrl', convertServerUrlToImageUrl(contentImageHook.contentImage)); + formData.append('imageUrl', contentImageHook.contentImage); writingOptionList.forEach((option, index) => { formData.append(`postOptions[${index}].content`, option.content); formData.append(`postOptions[${index}].imageUrl`, option.imageUrl); @@ -163,9 +157,9 @@ export default function PostForm({ data, mutate }: PostFormProps) { if (!item.files) return; if (index === 0) { - formData.append('imageFile', item.files[0]); + item.files[0] && formData.append('imageFile', item.files[0]); } else { - formData.append(`postOptions[${index - 1}].imageFile`, item.files[0]); + item.files[0] && formData.append(`postOptions[${index - 1}].imageFile`, item.files[0]); } }); diff --git a/frontend/src/components/common/Post/index.tsx b/frontend/src/components/common/Post/index.tsx index 0e4f5cd98..5d831dfbd 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 { checkClosedPost, convertTimeToWord } from '@utils/time'; import photoIcon from '@assets/photo_white.svg'; @@ -164,9 +163,7 @@ export default function Post({ postInfo, isPreview }: PostProps) { > {content} - {!isPreview && imageUrl && ( - - )} + {!isPreview && imageUrl && } - {!isPreview && imageUrl && ( - - )} + {!isPreview && imageUrl && } {isPreview ? ( {text} ) : ( From 239c41c2dab4d7604e57b13fa18a35d4ff6d4d0d Mon Sep 17 00:00:00 2001 From: chsua Date: Mon, 11 Sep 2023 13:10:06 +0900 Subject: [PATCH 18/48] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=EC=9D=B8=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=88=98=EC=A0=95=20api=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=EC=97=90=20optionId=EB=A5=BC=20=ED=95=A8=EA=BB=98=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 8a9bae9d8..95905e1ae 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -122,8 +122,8 @@ 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 }; + const writingOptionList = writingOptionHook.optionList.map(({ id, text, imageUrl }, index) => { + return { id, content: text, imageUrl }; }); //예외처리 @@ -148,6 +148,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { formData.append('content', deleteOverlappingNewLine(writingContent)); formData.append('imageUrl', contentImageHook.contentImage); writingOptionList.forEach((option, index) => { + serverVoteInfo && formData.append(`postOptions[${index}].id`, option.id.toString()); formData.append(`postOptions[${index}].content`, option.content); formData.append(`postOptions[${index}].imageUrl`, option.imageUrl); }); From 4987bc1cfb65e74d384dc1b600b5f8a2a6805d90 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 15:04:26 +0900 Subject: [PATCH 19/48] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EC=BB=A8=EB=B2=A4=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/{utilTimeTest.test.ts => convertTimeToWord.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/__test__/{utilTimeTest.test.ts => convertTimeToWord.test.ts} (100%) diff --git a/frontend/__test__/utilTimeTest.test.ts b/frontend/__test__/convertTimeToWord.test.ts similarity index 100% rename from frontend/__test__/utilTimeTest.test.ts rename to frontend/__test__/convertTimeToWord.test.ts From f57798d9f27f431eb6505a43da7ead5ad2c6d03b Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 15:05:10 +0900 Subject: [PATCH 20/48] =?UTF-8?q?feat:=20=EA=B0=9C=ED=96=89=205=EB=B2=88?= =?UTF-8?q?=20=EC=9D=B4=EC=83=81=20=EC=97=B0=EC=86=8D=EB=90=98=EB=A9=B4=20?= =?UTF-8?q?5=EB=B2=88=20=EA=B0=9C=ED=96=89=EC=9C=BC=EB=A1=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/post/deleteOverlappingNewLine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/post/deleteOverlappingNewLine.ts b/frontend/src/utils/post/deleteOverlappingNewLine.ts index 58e1490d5..65bb26e20 100644 --- a/frontend/src/utils/post/deleteOverlappingNewLine.ts +++ b/frontend/src/utils/post/deleteOverlappingNewLine.ts @@ -1,3 +1,3 @@ export const deleteOverlappingNewLine = (text: string) => { - return text.replace(/(\n{2,})/g, '\n'); + return text.replace(/(\n{5,})/g, '\n\n\n\n\n'); }; From 04ad0703e2f5c08f493ab614895d35d9d0552b48 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 15:06:38 +0900 Subject: [PATCH 21/48] =?UTF-8?q?feat:=20=EA=B0=9C=ED=96=89=EC=9D=B4=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20textarea=EC=9D=98=20value?= =?UTF-8?q?=EB=8A=94=205=ED=9A=8C=EC=9D=B4=EC=83=81=20=EC=97=B0=EC=86=8D?= =?UTF-8?q?=EA=B0=9C=ED=96=89=EC=9D=84=20=EA=B2=80=EC=97=B4=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=ED=86=B5=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코멘트, 선택지 내용 추가 --- frontend/src/components/PostForm/index.tsx | 2 +- .../comment/CommentList/CommentTextForm/index.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 95905e1ae..d8ca1a048 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -149,7 +149,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { formData.append('imageUrl', contentImageHook.contentImage); writingOptionList.forEach((option, index) => { serverVoteInfo && formData.append(`postOptions[${index}].id`, option.id.toString()); - formData.append(`postOptions[${index}].content`, option.content); + formData.append(`postOptions[${index}].content`, deleteOverlappingNewLine(option.content)); formData.append(`postOptions[${index}].imageUrl`, option.imageUrl); }); formData.append('deadline', addTimeToDate(time, baseTime)); 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(() => { From 8ef75160735e13cde6d1aa9a0cb6415ed3b50ab5 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 15:40:07 +0900 Subject: [PATCH 22/48] =?UTF-8?q?refactor:=20=EB=A7=88=EA=B0=90=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A4=91=20=EB=B3=80=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8=ED=95=98=EC=97=AC=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/validation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/PostForm/validation.ts b/frontend/src/components/PostForm/validation.ts index d8fa6ab0f..c13f6d24e 100644 --- a/frontend/src/components/PostForm/validation.ts +++ b/frontend/src/components/PostForm/validation.ts @@ -20,5 +20,6 @@ export const checkValidationPost = ( if (optionList.length > 5) return '선택지는 최대 5개 입력할 수 있습니다.'; if (optionList.some(option => option.content.trim() === '')) return '선택지에 글을 입력해주세요.'; - if (Object.values(time).reduce((a, b) => a + b, 0) < 1) return '시간은 필수로 입력해야 합니다.'; + const isTimeOptionZero = Object.values(time).reduce((a, b) => a + b, 0) < 1; + if (isTimeOptionZero) return '시간은 필수로 입력해야 합니다.'; }; From 6317aa7b0f60278303adc99d3280da8e802e24a7 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 15:47:50 +0900 Subject: [PATCH 23/48] =?UTF-8?q?refactor:=20=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1/=EC=88=98=EC=A0=95=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20type=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/index.tsx | 10 +++++----- frontend/src/components/PostForm/validation.ts | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index d8ca1a048..ccaf0a7bb 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -122,21 +122,21 @@ export default function PostForm({ data, mutate }: PostFormProps) { e.preventDefault(); const formData = new FormData(); - const writingOptionList = writingOptionHook.optionList.map(({ id, text, imageUrl }, index) => { - return { id, content: text, imageUrl }; - }); - //예외처리 const { selectedOptionList } = multiSelectHook; const errorMessage = checkValidationPost( selectedOptionList, writingTitle, writingContent, - writingOptionList, + writingOptionHook.optionList, time ); if (errorMessage) return openToast(errorMessage); + const writingOptionList = writingOptionHook.optionList.map(({ id, text, imageUrl }, index) => { + return { id, content: text, imageUrl }; + }); + if (e.target instanceof HTMLFormElement) { const imageFileInputs = e.target.querySelectorAll('input[type="file"]'); const fileInputList = [...imageFileInputs]; diff --git a/frontend/src/components/PostForm/validation.ts b/frontend/src/components/PostForm/validation.ts index c13f6d24e..a56016a03 100644 --- a/frontend/src/components/PostForm/validation.ts +++ b/frontend/src/components/PostForm/validation.ts @@ -1,12 +1,14 @@ 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: any[], + optionList: WritingVoteOptionType[], time: Time ) => { if (categoryList.length < 1) return '카테고리를 최소 1개 골라주세요.'; @@ -18,7 +20,7 @@ export const checkValidationPost = ( if (optionList.length < 2) return '선택지는 최소 2개 입력해주세요.'; if (optionList.length > 5) return '선택지는 최대 5개 입력할 수 있습니다.'; - if (optionList.some(option => option.content.trim() === '')) return '선택지에 글을 입력해주세요.'; + if (optionList.some(option => option.text.trim() === '')) return '선택지에 글을 입력해주세요.'; const isTimeOptionZero = Object.values(time).reduce((a, b) => a + b, 0) < 1; if (isTimeOptionZero) return '시간은 필수로 입력해야 합니다.'; From 4c9c3158b6bde356d6e1bb8c6131062f4ec7c3dc Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 15:55:53 +0900 Subject: [PATCH 24/48] =?UTF-8?q?refactor:=20contentInputRefList=20?= =?UTF-8?q?=EC=9D=98=20type=EC=A4=91=20null=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptionUploadImageButton.stories.tsx | 2 +- .../WritingVoteOption/OptionUploadImageButton/index.tsx | 8 +++----- .../WritingVoteOption/WritingVoteOption.stories.tsx | 6 +++--- .../WritingVoteOptionList/WritingVoteOption/index.tsx | 2 +- .../components/optionList/WritingVoteOptionList/index.tsx | 2 +- frontend/src/hooks/useWritingOption.tsx | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) 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 ebd896c6b..52a123c22 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx @@ -11,7 +11,7 @@ const meta: Meta = { export default meta; export const Default = () => { - const ref = useRef(null); + const ref = useRef([]); return ( { optionId: number; isImageVisible: boolean; - contentInputRefList: MutableRefObject; + contentInputRefList: MutableRefObject; index: number; } @@ -22,7 +22,7 @@ export default function OptionUploadImageButton({ const handleButtonClick = (e: MouseEvent) => { e.preventDefault(); - contentInputRefList.current && contentInputRefList.current[index].click(); + contentInputRefList.current[index].click(); }; return ( @@ -38,9 +38,7 @@ export default function OptionUploadImageButton({ accept="image/*" tabIndex={-1} ref={(ele: HTMLInputElement) => { - if (contentInputRefList.current) { - contentInputRefList.current[index] = ele; - } + 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 cb0b71952..5cfda953e 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx @@ -11,7 +11,7 @@ const meta: Meta = { export default meta; export const IsDeletable = () => { - const ref = useRef(null); + const ref = useRef([]); return ( { }; export const IsNotDeletable = () => { - const ref = useRef(null); + const ref = useRef([]); return ( { }; export const ShowImage = () => { - const ref = useRef(null); + const ref = useRef([]); return ( void; handleUploadImage: (event: ChangeEvent) => void; imageUrl: string; - contentInputRefList: MutableRefObject; + contentInputRefList: MutableRefObject; index: number; } diff --git a/frontend/src/components/optionList/WritingVoteOptionList/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/index.tsx index b1af98919..d6944bc35 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/index.tsx +++ b/frontend/src/components/optionList/WritingVoteOptionList/index.tsx @@ -20,7 +20,7 @@ interface WritingVoteOptionListProps { deleteOption: (optionId: number) => void; removeImage: (optionId: number) => void; handleUploadImage: (event: ChangeEvent, optionId: number) => void; - contentInputRefList: MutableRefObject; + contentInputRefList: MutableRefObject; }; } diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index c38f04515..55f51a2c2 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -20,7 +20,7 @@ const INIT_OPTION_LIST = [ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = INIT_OPTION_LIST) => { const [optionList, setOptionList] = useState(initialOptionList); - const contentInputRefList = useRef([]); + const contentInputRefList = useRef([]); const addOption = () => { if (optionList.length >= MAX_COUNT) return; From 265ba238caa075480374dad2491ec0b120d49493 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 16:07:35 +0900 Subject: [PATCH 25/48] =?UTF-8?q?test:=20=EA=B0=9C=ED=96=89=20=EA=B2=80?= =?UTF-8?q?=EC=97=B4=20=EC=9C=A0=ED=8B=B8=ED=95=A8=EC=88=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/deleteOverlappingNewLine.test.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/frontend/__test__/deleteOverlappingNewLine.test.ts b/frontend/__test__/deleteOverlappingNewLine.test.ts index 7e2cb65f0..b228dda1a 100644 --- a/frontend/__test__/deleteOverlappingNewLine.test.ts +++ b/frontend/__test__/deleteOverlappingNewLine.test.ts @@ -17,21 +17,41 @@ describe('연속된 개행은 하나의 개행으로 처리하는 유틸함수 expect(changedText).toBe(text); }); - test('2회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 1회 개행으로 바뀐 결과를 반환한다.', () => { + test('2회 연속된 개행이 있는 문자열은 인자와 동일한 결과를 반환한다.', () => { const text = '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; const expectText = - '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; const changedText = deleteOverlappingNewLine(text); expect(changedText).toBe(expectText); }); - test('3회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 1회 개행으로 바뀐 결과를 반환한다.', () => { + test('5회 연속된 개행이 있는 문자열은 인자와 연속된 개행이 5회 개행으로 바뀐 결과를 반환한다.', () => { const text = - '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n\n\n\n\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; const expectText = - '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\n동일한 결과물이 나와야 옳은 작동을 하는 유틸함수 입니다.'; + '안녕하세요. 이것은 유틸함수를 테스트하기 위한 문자열입니다.\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); From d23e65e210cb7793053b91ef0e4676d1969f36fe Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 17:20:38 +0900 Subject: [PATCH 26/48] =?UTF-8?q?fix:=20(#541)=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=20=EC=84=A0=ED=83=9D=EC=A7=80?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20id=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B3=A0,=20=EC=95=84=EB=8B=88=EB=A9=B4=20=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/index.tsx | 10 ++++++---- frontend/src/hooks/useWritingOption.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index ccaf0a7bb..94127da63 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -133,9 +133,11 @@ export default function PostForm({ data, mutate }: PostFormProps) { ); if (errorMessage) return openToast(errorMessage); - const writingOptionList = writingOptionHook.optionList.map(({ id, text, imageUrl }, index) => { - return { id, content: text, imageUrl }; - }); + 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('input[type="file"]'); @@ -148,7 +150,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { formData.append('content', deleteOverlappingNewLine(writingContent)); formData.append('imageUrl', contentImageHook.contentImage); writingOptionList.forEach((option, index) => { - serverVoteInfo && formData.append(`postOptions[${index}].id`, option.id.toString()); + 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); }); diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index 55f51a2c2..8ebd7190c 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -14,12 +14,14 @@ 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); + const [optionList, setOptionList] = useState( + initialOptionList.map(option => ({ ...option, isServerId: true })) + ); const contentInputRefList = useRef([]); const addOption = () => { @@ -27,7 +29,7 @@ export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = IN const updatedOptionList = [ ...optionList, - { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' }, + { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '', isServerId: false }, ]; setOptionList(updatedOptionList); From e0f3f811b978f3d548120404788fd65261c6f51e Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 17:24:32 +0900 Subject: [PATCH 27/48] =?UTF-8?q?test:=20=EC=84=A0=ED=83=9D=EC=A7=80=20?= =?UTF-8?q?=ED=9B=85=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/hooks/useWritingOption.test.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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('선택한 이미지가 있을 때 취소할 수 있다.', () => { From fc165f7ed5dd474662f5739325edb8a6293347d6 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 12 Sep 2023 17:34:10 +0900 Subject: [PATCH 28/48] =?UTF-8?q?fix:=20=EC=84=A0=ED=83=9D=EC=A7=80=20?= =?UTF-8?q?=ED=9B=85=EC=97=90=EC=84=9C=20isServerId=EA=B0=80=20=EB=AC=B4?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20true=EC=9D=B8=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useWritingOption.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index 8ebd7190c..eef793d6d 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -18,9 +18,11 @@ const INIT_OPTION_LIST = [ { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '', isServerId: false }, ]; -export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = INIT_OPTION_LIST) => { +export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => { const [optionList, setOptionList] = useState( - initialOptionList.map(option => ({ ...option, isServerId: true })) + initialOptionList + ? initialOptionList.map(option => ({ ...option, isServerId: true })) + : INIT_OPTION_LIST ); const contentInputRefList = useRef([]); From 7c15999f2e96629fcb18b6ed9fce0967648c60ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?= <80146176+Gilpop8663@users.noreply.github.com> Date: Thu, 14 Sep 2023 15:20:02 +0900 Subject: [PATCH 29/48] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=ED=86=B5=EA=B3=84,=20=ED=9A=8C=EC=9B=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20lazy=20import=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(#571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (#556) lazy import 설정 및 트리쉐이킹 설정 * feat: (#556) 로그인, 투표 통계, 회원정보 입력 페이지 lazy import 적용 * feat: (#556) 번들 이름 매번 바뀌도록 변경 및 수정되었던 코드 복구 * feat: (#556) Suspense 코드 복구 --------- Co-authored-by: jero_kang <81199414+inyeong-kang@users.noreply.github.com> --- frontend/public/seo/sitemap.xml | 6 +++--- frontend/src/App.tsx | 6 +++++- frontend/src/routes/router.tsx | 8 +++++--- frontend/webpack.common.js | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/frontend/public/seo/sitemap.xml b/frontend/public/seo/sitemap.xml index 33657e5c2..7f4f6d631 100644 --- a/frontend/public/seo/sitemap.xml +++ b/frontend/public/seo/sitemap.xml @@ -6,16 +6,16 @@ > https://votogether.com/ - 2023-09-13T04:22:25.347Z + 2023-09-14T06:06:57.224Z https://votogether.com/login - 2023-09-13T04:22:25.347Z + 2023-09-14T06:06:57.224Z https://votogether.com/ranking - 2023-09-13T04:22:25.347Z + 2023-09-14T06:06:57.224Z diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bb8b11cc5..0d7708033 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -11,6 +12,7 @@ import router from '@routes/router'; import ErrorBoundaryForTopClass from '@pages/ErrorBoundaryForTopClass'; import ChannelTalk from '@components/ChannelTalk'; +import Skeleton from '@components/common/Skeleton'; import { GlobalStyle } from '@styles/globalStyle'; import { theme } from '@styles/theme'; @@ -29,7 +31,9 @@ const App = () => ( - + }> + + diff --git a/frontend/src/routes/router.tsx b/frontend/src/routes/router.tsx index 62234a4a3..6b1d4897a 100644 --- a/frontend/src/routes/router.tsx +++ b/frontend/src/routes/router.tsx @@ -1,7 +1,7 @@ +import { lazy } from 'react'; import { createBrowserRouter } from 'react-router-dom'; import Announcement from '@pages/Announcement'; -import Login from '@pages/auth/Login'; import Redirection from '@pages/auth/Redirection'; import Error from '@pages/Error'; import Home from '@pages/Home'; @@ -11,8 +11,6 @@ import CreatePostPage from '@pages/post/CreatePostPage'; import EditPostPage from '@pages/post/EditPostPage'; import PostDetailPage from '@pages/post/PostDetail'; import Ranking from '@pages/Ranking'; -import RegisterPersonalInfo from '@pages/user/RegisterPersonalInfo'; -import VoteStatisticsPage from '@pages/VoteStatisticsPage'; import ScrollToTop from '@components/common/ScrollToTop'; import RouteChangeTracker from '@components/RouteChangeTracker'; @@ -21,6 +19,10 @@ import { PATH } from '@constants/path'; import PrivateRoute from './PrivateRoute'; +const Login = lazy(() => import('@pages/auth/Login')); +const RegisterPersonalInfo = lazy(() => import('@pages/user/RegisterPersonalInfo')); +const VoteStatisticsPage = lazy(() => import('@pages/VoteStatisticsPage')); + const router = createBrowserRouter([ { path: PATH.HOME, diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 8c02bd7d1..7ac442629 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -11,7 +11,7 @@ module.exports = { mode: 'development', entry: './src/index.tsx', output: { - filename: 'bundle.js', + filename: '[contenthash].bundle.js', path: path.resolve(__dirname, 'dist'), clean: true, publicPath: '/', From a68c06bcfa7cbc960c7c8817b1bba934b6b0f2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?= <80146176+Gilpop8663@users.noreply.github.com> Date: Thu, 14 Sep 2023 15:26:25 +0900 Subject: [PATCH 30/48] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EB=B3=B4=EB=82=BC=20=EB=95=8C=20webp=20?= =?UTF-8?q?=EB=A1=9C=20=EC=95=95=EC=B6=95=ED=95=98=EC=97=AC=20=EC=84=B1?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20(#614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (#555) browser-image-compression 설치 및 본문 이미지 훅에 적용 * feat: (#555) 선택지 옵션 사진을 webp로 변환하도록 구현 --- frontend/package.json | 1 + frontend/src/hooks/useContentImage.ts | 18 +++++++++++++----- frontend/src/hooks/useWritingOption.tsx | 22 ++++++++++++++++------ frontend/src/utils/resizeImage.ts | 17 +++++++++++++++++ 4 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/resizeImage.ts diff --git a/frontend/package.json b/frontend/package.json index 36966bd41..edfcc6404 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@tanstack/react-query": "^4.29.19", + "browser-image-compression": "^2.0.2", "dotenv": "^16.3.1", "msw": "^1.2.3", "react": "^18.2.0", diff --git a/frontend/src/hooks/useContentImage.ts b/frontend/src/hooks/useContentImage.ts index 85dfd196e..8a859eed2 100644 --- a/frontend/src/hooks/useContentImage.ts +++ b/frontend/src/hooks/useContentImage.ts @@ -2,6 +2,8 @@ import { ChangeEvent, useRef, useState } from 'react'; import { MAX_FILE_SIZE } from '@components/PostForm/constants'; +import { convertImageToWebP } from '@utils/resizeImage'; + export const useContentImage = (imageUrl: string = '') => { const [contentImage, setContentImage] = useState(imageUrl); const contentInputRef = useRef(null); @@ -11,13 +13,23 @@ export const useContentImage = (imageUrl: string = '') => { if (contentInputRef.current) contentInputRef.current.value = ''; }; - const handleUploadImage = (event: ChangeEvent) => { + const handleUploadImage = async (event: ChangeEvent) => { const { files } = event.target; if (!files) return; const file = files[0]; + const webpFileList = await convertImageToWebP(file); + + event.target.files = webpFileList; + + const reader = new FileReader(); + + const webpFile = webpFileList[0]; + + reader.readAsDataURL(webpFile); + event.target.setCustomValidity(''); if (file.size > MAX_FILE_SIZE) { @@ -27,10 +39,6 @@ export const useContentImage = (imageUrl: string = '') => { return; } - const reader = new FileReader(); - - reader.readAsDataURL(file); - reader.onloadend = () => { setContentImage(reader.result?.toString() ?? ''); }; diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index eef793d6d..6f66236e1 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -2,6 +2,8 @@ import React, { ChangeEvent, useRef, useState } from 'react'; import { MAX_FILE_SIZE } from '@components/PostForm/constants'; +import { convertImageToWebP } from '@utils/resizeImage'; + const MAX_WRITING_LENGTH = 50; export interface WritingVoteOptionType { @@ -87,13 +89,26 @@ export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => }); }; - const handleUploadImage = (event: React.ChangeEvent, optionId: number) => { + const handleUploadImage = async ( + event: React.ChangeEvent, + optionId: number + ) => { const { files } = event.target; if (!files) return; const file = files[0]; + const webpFileList = await convertImageToWebP(file); + + event.target.files = webpFileList; + + const reader = new FileReader(); + + const webpFile = webpFileList[0]; + + reader.readAsDataURL(webpFile); + event.target.setCustomValidity(''); if (file.size > MAX_FILE_SIZE) { @@ -103,11 +118,6 @@ export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => return; } - const reader = new FileReader(); - - // readAsDataURL 메서드를 통해 파일을 모두 읽고 나면 reader의 loadend 이벤트에서 이미지 미리보기 결과를 확인할 수 있습니다. - reader.readAsDataURL(file); - reader.onloadend = () => { const updatedOptionList = optionList.map(optionItem => { if (optionItem.id === optionId) { diff --git a/frontend/src/utils/resizeImage.ts b/frontend/src/utils/resizeImage.ts new file mode 100644 index 000000000..383db9a9a --- /dev/null +++ b/frontend/src/utils/resizeImage.ts @@ -0,0 +1,17 @@ +import imageCompression from 'browser-image-compression'; + +export const convertImageToWebP = async (imageFile: File) => { + const compressedBlob = await imageCompression(imageFile, { + maxWidthOrHeight: 1280, + initialQuality: 0.5, + fileType: 'image/webp', + }); + + const outputWebpFile = new File([compressedBlob], `${Date.now().toString()}.webp`); + + const dataTransfer = new DataTransfer(); + + dataTransfer.items.add(outputWebpFile); + + return dataTransfer.files; +}; From 46aa064b5f111ae029fd2258fa711bfc7895c469 Mon Sep 17 00:00:00 2001 From: Jun-Hyeok Sin Date: Thu, 14 Sep 2023 17:34:17 +0900 Subject: [PATCH 31/48] =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: (#513) Report 도메인 코드 가독성 리팩토링 * refactor: (#513) Report 도메인 관련 테스트 코드 리팩토링 * refactor: (#513) Report의 Controller, Service를 컨벤션에 맞춰 클래스명 개선 * refactor: (#513) 전략 패턴 클래스들에 대한 테스트 코드 추가 * refactor: (#513) Transactional 어노테이션 클래스에 붙는 것으로 위치 개선 * refactor: (#513) 닉네임 3회 신고 받은 후 해당 멤버에 대한 닉네임 신고 기록 삭제 * refactor: (#513) 각 신고 전략에 관한 상수는 각 클래스에 두는 것으로 개선 * refactor: (#513) 파라미터가 2개인 메서드의 선언문 개행 되돌리기 * refactor: (#513) deleteByReportTypeAndTargetId 메서드의 테스트 코드 추가 * refactor: (#513) 파라미터 2개인 메서드 선언부 개행 삭제 --- .../domain/post/entity/comment/Content.java | 2 +- ...va => ReportCommandCommandController.java} | 8 +- ....java => ReportCommandControllerDocs.java} | 2 +- .../report/repository/ReportRepository.java | 2 + .../report/service/ReportCommandService.java | 37 +++ .../domain/report/service/ReportService.java | 164 ------------ .../strategy/ReportCommentStrategy.java | 55 ++++ .../strategy/ReportNicknameStrategy.java | 59 +++++ .../service/strategy/ReportPostStrategy.java | 55 ++++ .../service/strategy/ReportStrategy.java | 41 +++ .../member/service/MemberServiceTest.java | 6 - .../repository/CommentRepositoryTest.java | 18 +- ....java => ReportCommandControllerTest.java} | 35 +-- .../repository/ReportRepositoryTest.java | 74 ++++-- ...est.java => ReportCommandServiceTest.java} | 168 ++++++------- .../strategy/ReportCommentStrategyTest.java | 234 ++++++++++++++++++ .../strategy/ReportNicknameStrategyTest.java | 100 ++++++++ .../strategy/ReportPostStrategyTest.java | 209 ++++++++++++++++ .../test/persister/CommentTestPersister.java | 52 ++++ .../test/persister/PostTestPersister.java | 13 +- .../test/persister/ReportTestPersister.java | 59 +++++ 21 files changed, 1083 insertions(+), 310 deletions(-) rename backend/src/main/java/com/votogether/domain/report/controller/{ReportController.java => ReportCommandCommandController.java} (73%) rename backend/src/main/java/com/votogether/domain/report/controller/{ReportControllerDocs.java => ReportCommandControllerDocs.java} (96%) create mode 100644 backend/src/main/java/com/votogether/domain/report/service/ReportCommandService.java delete mode 100644 backend/src/main/java/com/votogether/domain/report/service/ReportService.java create mode 100644 backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java create mode 100644 backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java create mode 100644 backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java create mode 100644 backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java rename backend/src/test/java/com/votogether/domain/report/controller/{ReportControllerTest.java => ReportCommandControllerTest.java} (87%) rename backend/src/test/java/com/votogether/domain/report/service/{ReportServiceTest.java => ReportCommandServiceTest.java} (79%) create mode 100644 backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java create mode 100644 backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java create mode 100644 backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java create mode 100644 backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java create mode 100644 backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java diff --git a/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java b/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java index e6e0fd3b8..c994cfe2b 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/comment/Content.java @@ -11,7 +11,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Embeddable -class Content { +public class Content { private static final int MAXIMUM_LENGTH = 500; diff --git a/backend/src/main/java/com/votogether/domain/report/controller/ReportController.java b/backend/src/main/java/com/votogether/domain/report/controller/ReportCommandCommandController.java similarity index 73% rename from backend/src/main/java/com/votogether/domain/report/controller/ReportController.java rename to backend/src/main/java/com/votogether/domain/report/controller/ReportCommandCommandController.java index fc94dfd68..941ce0740 100644 --- a/backend/src/main/java/com/votogether/domain/report/controller/ReportController.java +++ b/backend/src/main/java/com/votogether/domain/report/controller/ReportCommandCommandController.java @@ -2,7 +2,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.report.dto.request.ReportRequest; -import com.votogether.domain.report.service.ReportService; +import com.votogether.domain.report.service.ReportCommandService; import com.votogether.global.jwt.Auth; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -13,13 +13,13 @@ @RequiredArgsConstructor @RestController -public class ReportController implements ReportControllerDocs { +public class ReportCommandCommandController implements ReportCommandControllerDocs { - private final ReportService reportService; + private final ReportCommandService reportCommandService; @PostMapping("/report") public ResponseEntity report(@Valid @RequestBody final ReportRequest request, @Auth final Member member) { - reportService.report(member, request); + reportCommandService.report(member, request); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/votogether/domain/report/controller/ReportControllerDocs.java b/backend/src/main/java/com/votogether/domain/report/controller/ReportCommandControllerDocs.java similarity index 96% rename from backend/src/main/java/com/votogether/domain/report/controller/ReportControllerDocs.java rename to backend/src/main/java/com/votogether/domain/report/controller/ReportCommandControllerDocs.java index a07c32024..aab4566b5 100644 --- a/backend/src/main/java/com/votogether/domain/report/controller/ReportControllerDocs.java +++ b/backend/src/main/java/com/votogether/domain/report/controller/ReportCommandControllerDocs.java @@ -12,7 +12,7 @@ import org.springframework.http.ResponseEntity; @Tag(name = "신고", description = "신고 API") -public interface ReportControllerDocs { +public interface ReportCommandControllerDocs { @Operation(summary = "신고", description = "게시글, 댓글, 닉네임을 신고한다.") @ApiResponses({ diff --git a/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java b/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java index 9f56a5d0b..c575552d0 100644 --- a/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java +++ b/backend/src/main/java/com/votogether/domain/report/repository/ReportRepository.java @@ -21,4 +21,6 @@ Optional findByMemberAndReportTypeAndTargetId( List findAllByReportTypeAndTargetId(final ReportType reportType, final Long targetId); + void deleteByReportTypeAndTargetId(final ReportType reportType, final Long targetId); + } diff --git a/backend/src/main/java/com/votogether/domain/report/service/ReportCommandService.java b/backend/src/main/java/com/votogether/domain/report/service/ReportCommandService.java new file mode 100644 index 000000000..c89e0d658 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/report/service/ReportCommandService.java @@ -0,0 +1,37 @@ +package com.votogether.domain.report.service; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.domain.report.service.strategy.ReportCommentStrategy; +import com.votogether.domain.report.service.strategy.ReportNicknameStrategy; +import com.votogether.domain.report.service.strategy.ReportPostStrategy; +import com.votogether.domain.report.service.strategy.ReportStrategy; +import java.util.EnumMap; +import java.util.Map; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Service +public class ReportCommandService { + + private final Map reportActions; + + public ReportCommandService( + final ReportPostStrategy reportPostStrategy, + final ReportCommentStrategy reportCommentStrategy, + final ReportNicknameStrategy reportNicknameStrategy + ) { + this.reportActions = new EnumMap<>(ReportType.class); + this.reportActions.put(ReportType.POST, reportPostStrategy); + this.reportActions.put(ReportType.COMMENT, reportCommentStrategy); + this.reportActions.put(ReportType.NICKNAME, reportNicknameStrategy); + } + + public void report(final Member reporter, final ReportRequest request) { + final ReportStrategy reportStrategy = reportActions.get(request.type()); + reportStrategy.report(reporter, request); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/report/service/ReportService.java b/backend/src/main/java/com/votogether/domain/report/service/ReportService.java deleted file mode 100644 index 5357b6c02..000000000 --- a/backend/src/main/java/com/votogether/domain/report/service/ReportService.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.votogether.domain.report.service; - -import com.votogether.domain.member.entity.Member; -import com.votogether.domain.member.exception.MemberExceptionType; -import com.votogether.domain.member.repository.MemberRepository; -import com.votogether.domain.post.entity.Post; -import com.votogether.domain.post.entity.comment.Comment; -import com.votogether.domain.post.exception.CommentExceptionType; -import com.votogether.domain.post.exception.PostExceptionType; -import com.votogether.domain.post.repository.CommentRepository; -import com.votogether.domain.post.repository.PostRepository; -import com.votogether.domain.report.dto.request.ReportRequest; -import com.votogether.domain.report.entity.Report; -import com.votogether.domain.report.entity.vo.ReportType; -import com.votogether.domain.report.exception.ReportExceptionType; -import com.votogether.domain.report.repository.ReportRepository; -import com.votogether.global.exception.BadRequestException; -import com.votogether.global.exception.NotFoundException; -import java.util.Objects; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Service -public class ReportService { - - private final ReportRepository reportRepository; - private final PostRepository postRepository; - private final CommentRepository commentRepository; - private final MemberRepository memberRepository; - - @Transactional - public void report(final Member reporter, final ReportRequest request) { - if (request.type() == ReportType.POST) { - reportPost(reporter, request); - } - if (request.type() == ReportType.COMMENT) { - reportComment(reporter, request); - } - if (request.type() == ReportType.NICKNAME) { - reportNickname(reporter, request); - } - } - - private void reportPost( - final Member reporter, - final ReportRequest request - ) { - final Post reportedPost = postRepository.findById(request.id()) - .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); - validatePost(reporter, reportedPost, request); - - saveReport(reporter, request); - blindPost(request, reportedPost); - } - - private void validatePost( - final Member reporter, - final Post reportedPost, - final ReportRequest request - ) { - reportedPost.validateMine(reporter); - reportedPost.validateHidden(); - validateDuplicatedReport(reporter, request, ReportExceptionType.DUPLICATE_POST_REPORT); - } - - private void validateDuplicatedReport( - final Member reporter, - final ReportRequest request, - final ReportExceptionType exceptionType - ) { - reportRepository.findByMemberAndReportTypeAndTargetId(reporter, request.type(), request.id()) - .ifPresent(report -> { - throw new BadRequestException(exceptionType); - }); - } - - private void saveReport(final Member reporter, final ReportRequest request) { - final Report report = Report.builder() - .member(reporter) - .reportType(request.type()) - .targetId(request.id()) - .reason(request.reason()) - .build(); - reportRepository.save(report); - } - - private void blindPost( - final ReportRequest request, - final Post reportedPost - ) { - final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), request.id()); - if (reportCount >= 5) { - reportedPost.blind(); - } - } - - private void reportComment( - final Member reporter, - final ReportRequest request - ) { - final Comment reportedComment = commentRepository.findById(request.id()) - .orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND)); - validateComment(reporter, request, reportedComment); - - saveReport(reporter, request); - blindComment(request, reportedComment); - } - - private void validateComment( - final Member reporter, - final ReportRequest request, - final Comment reportedComment - ) { - reportedComment.validateMine(reporter); - reportedComment.validateHidden(); - validateDuplicatedReport(reporter, request, ReportExceptionType.DUPLICATE_COMMENT_REPORT); - } - - private void blindComment( - final ReportRequest request, - final Comment reportedComment - ) { - final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), request.id()); - if (reportCount >= 5) { - reportedComment.blind(); - } - } - - private void reportNickname( - final Member reporter, - final ReportRequest request - ) { - final Member reportedMember = memberRepository.findById(request.id()) - .orElseThrow(() -> new NotFoundException(MemberExceptionType.NONEXISTENT_MEMBER)); - validateNickname(reporter, request); - - saveReport(reporter, request); - changeNicknameByReport(reportedMember, request.type()); - } - - private void validateNickname( - final Member reporter, - final ReportRequest request - ) { - validateMyNickname(reporter, request); - validateDuplicatedReport(reporter, request, ReportExceptionType.DUPLICATE_NICKNAME_REPORT); - } - - private void validateMyNickname(final Member reporter, final ReportRequest request) { - if (Objects.equals(reporter.getId(), request.id())) { - throw new BadRequestException(ReportExceptionType.REPORT_MY_NICKNAME); - } - } - - private void changeNicknameByReport(final Member reportedMember, final ReportType reportType) { - final int reportCount = reportRepository.countByReportTypeAndTargetId(reportType, reportedMember.getId()); - if (reportCount >= 3) { - reportedMember.changeNicknameByReport(); - } - } - -} diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java new file mode 100644 index 000000000..5fa576365 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportCommentStrategy.java @@ -0,0 +1,55 @@ +package com.votogether.domain.report.service.strategy; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.comment.Comment; +import com.votogether.domain.post.exception.CommentExceptionType; +import com.votogether.domain.post.repository.CommentRepository; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.exception.ReportExceptionType; +import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ReportCommentStrategy implements ReportStrategy { + + private static final int NUMBER_OF_COMMENT_BLIND_BASED_REPORTS = 5; + + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + + @Override + public void report(final Member reporter, final ReportRequest request) { + final Comment reportedComment = commentRepository.findById(request.id()) + .orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND)); + validateComment(reporter, request, reportedComment); + + saveReport(reporter, request, reportRepository); + blindComment(request, reportedComment); + } + + private void validateComment( + final Member reporter, + final ReportRequest request, + final Comment reportedComment + ) { + reportedComment.validateMine(reporter); + reportedComment.validateHidden(); + validateDuplicatedReport( + reporter, + request, + ReportExceptionType.DUPLICATE_COMMENT_REPORT, + reportRepository + ); + } + + private void blindComment(final ReportRequest request, final Comment reportedComment) { + final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), request.id()); + if (reportCount >= NUMBER_OF_COMMENT_BLIND_BASED_REPORTS) { + reportedComment.blind(); + } + } + +} diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java new file mode 100644 index 000000000..7d3361fa9 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategy.java @@ -0,0 +1,59 @@ +package com.votogether.domain.report.service.strategy; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.exception.MemberExceptionType; +import com.votogether.domain.member.repository.MemberRepository; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.domain.report.exception.ReportExceptionType; +import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ReportNicknameStrategy implements ReportStrategy { + + private static final int NUMBER_OF_NICKNAME_CHANGE_REPORTS = 3; + + private final MemberRepository memberRepository; + private final ReportRepository reportRepository; + + @Override + public void report(final Member reporter, final ReportRequest request) { + final Member reportedMember = memberRepository.findById(request.id()) + .orElseThrow(() -> new NotFoundException(MemberExceptionType.NONEXISTENT_MEMBER)); + validateNickname(reporter, request); + + saveReport(reporter, request, reportRepository); + changeNicknameByReport(reportedMember, request); + } + + private void validateNickname(final Member reporter, final ReportRequest request) { + validateMyNickname(reporter, request); + validateDuplicatedReport( + reporter, + request, + ReportExceptionType.DUPLICATE_NICKNAME_REPORT, + reportRepository + ); + } + + private void validateMyNickname(final Member reporter, final ReportRequest request) { + if (Objects.equals(reporter.getId(), request.id())) { + throw new BadRequestException(ReportExceptionType.REPORT_MY_NICKNAME); + } + } + + private void changeNicknameByReport(final Member reportedMember, final ReportRequest request) { + final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), reportedMember.getId()); + if (reportCount >= NUMBER_OF_NICKNAME_CHANGE_REPORTS) { + reportedMember.changeNicknameByReport(); + reportRepository.deleteByReportTypeAndTargetId(ReportType.NICKNAME, request.id()); + } + } + +} diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java new file mode 100644 index 000000000..be85c7219 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportPostStrategy.java @@ -0,0 +1,55 @@ +package com.votogether.domain.report.service.strategy; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.exception.PostExceptionType; +import com.votogether.domain.post.repository.PostRepository; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.exception.ReportExceptionType; +import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ReportPostStrategy implements ReportStrategy { + + private static final int NUMBER_OF_POST_BLIND_BASED_REPORTS = 5; + + private final PostRepository postRepository; + private final ReportRepository reportRepository; + + @Override + public void report(final Member reporter, final ReportRequest request) { + final Post reportedPost = postRepository.findById(request.id()) + .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); + validatePost(reporter, reportedPost, request); + + saveReport(reporter, request, reportRepository); + blindPost(request, reportedPost); + } + + private void validatePost( + final Member reporter, + final Post reportedPost, + final ReportRequest request + ) { + reportedPost.validateMine(reporter); + reportedPost.validateHidden(); + validateDuplicatedReport( + reporter, + request, + ReportExceptionType.DUPLICATE_POST_REPORT, + reportRepository + ); + } + + private void blindPost(final ReportRequest request, final Post reportedPost) { + final int reportCount = reportRepository.countByReportTypeAndTargetId(request.type(), request.id()); + if (reportCount >= NUMBER_OF_POST_BLIND_BASED_REPORTS) { + reportedPost.blind(); + } + } + +} diff --git a/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java new file mode 100644 index 000000000..9b5f7c464 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/report/service/strategy/ReportStrategy.java @@ -0,0 +1,41 @@ +package com.votogether.domain.report.service.strategy; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.entity.Report; +import com.votogether.domain.report.exception.ReportExceptionType; +import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.BadRequestException; + +@FunctionalInterface +public interface ReportStrategy { + + void report(final Member reporter, final ReportRequest request); + + default void validateDuplicatedReport( + final Member reporter, + final ReportRequest request, + final ReportExceptionType reportExceptionType, + final ReportRepository reportRepository + ) { + reportRepository.findByMemberAndReportTypeAndTargetId(reporter, request.type(), request.id()) + .ifPresent(report -> { + throw new BadRequestException(reportExceptionType); + }); + } + + default void saveReport( + final Member reporter, + final ReportRequest request, + final ReportRepository reportRepository + ) { + final Report report = Report.builder() + .member(reporter) + .reportType(request.type()) + .targetId(request.id()) + .reason(request.reason()) + .build(); + reportRepository.save(report); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java index bba1e64e5..d3507d63a 100644 --- a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java @@ -62,15 +62,9 @@ class MemberServiceTest { @Autowired CommentRepository commentRepository; - @Autowired - MemberTestPersister memberTestPersister; - @Autowired PostTestPersister postTestPersister; - @Autowired - VoteTestPersister voteTestPersister; - @Autowired EntityManager em; diff --git a/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java index c50d89c9d..a09f72d60 100644 --- a/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/post/repository/CommentRepositoryTest.java @@ -9,6 +9,7 @@ import com.votogether.domain.post.entity.comment.Comment; import com.votogether.test.annotation.RepositoryTest; import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.persister.PostTestPersister; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -27,18 +28,21 @@ class CommentRepositoryTest { @Autowired PostRepository postRepository; + @Autowired + PostTestPersister postTestPersister; + @Test @DisplayName("게시글의 댓글 목록을 조회한다.") void findAllByPost() { // given Member member = memberRepository.save(MemberFixtures.MALE_20.get()); - Post post = postRepository.save( - Post.builder() - .writer(member) - .postBody(PostBody.builder().title("titleA").content("contentA").build()) - .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build() - ); + + final Post post = postTestPersister.builder() + .writer(member) + .postBody(PostBody.builder().title("titleA").content("contentA").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + Comment commentA = commentRepository.save( Comment.builder() .member(member) diff --git a/backend/src/test/java/com/votogether/domain/report/controller/ReportControllerTest.java b/backend/src/test/java/com/votogether/domain/report/controller/ReportCommandControllerTest.java similarity index 87% rename from backend/src/test/java/com/votogether/domain/report/controller/ReportControllerTest.java rename to backend/src/test/java/com/votogether/domain/report/controller/ReportCommandControllerTest.java index 699f90f96..d66dd0d68 100644 --- a/backend/src/test/java/com/votogether/domain/report/controller/ReportControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/report/controller/ReportCommandControllerTest.java @@ -11,7 +11,7 @@ import com.votogether.domain.member.service.MemberService; import com.votogether.domain.report.dto.request.ReportRequest; import com.votogether.domain.report.entity.vo.ReportType; -import com.votogether.domain.report.service.ReportService; +import com.votogether.domain.report.service.ReportCommandService; import com.votogether.global.jwt.TokenPayload; import com.votogether.global.jwt.TokenProcessor; import io.restassured.http.ContentType; @@ -29,11 +29,11 @@ import org.springframework.http.HttpStatus; import org.springframework.web.context.WebApplicationContext; -@WebMvcTest(ReportController.class) -class ReportControllerTest { +@WebMvcTest(ReportCommandCommandController.class) +class ReportCommandControllerTest { @MockBean - ReportService reportService; + ReportCommandService reportCommandService; @MockBean TokenProcessor tokenProcessor; @@ -43,7 +43,7 @@ class ReportControllerTest { @BeforeEach void setUp(final WebApplicationContext webApplicationContext) { - RestAssuredMockMvc.standaloneSetup(new ReportController(reportService)); + RestAssuredMockMvc.standaloneSetup(new ReportCommandCommandController(reportCommandService)); RestAssuredMockMvc.webAppContextSetup(webApplicationContext); } @@ -69,7 +69,7 @@ void reportPost() throws Exception { given(memberService.findById(anyLong())).willReturn(member); ReportRequest request = new ReportRequest(ReportType.POST, 1L, "불건전한 게시글"); - willDoNothing().given(reportService).report(member, request); + willDoNothing().given(reportCommandService).report(member, request); // when, then RestAssuredMockMvc @@ -79,7 +79,8 @@ void reportPost() throws Exception { .body(request) .when().post("/report") .then().log().all() - .statusCode(HttpStatus.OK.value()); + .assertThat() + .status(HttpStatus.OK); } @Test @@ -100,7 +101,7 @@ void reportComment() throws Exception { given(memberService.findById(anyLong())).willReturn(member); ReportRequest request = new ReportRequest(ReportType.COMMENT, 1L, "불건전한 댓글"); - willDoNothing().given(reportService).report(member, request); + willDoNothing().given(reportCommandService).report(member, request); // when, then RestAssuredMockMvc @@ -110,7 +111,8 @@ void reportComment() throws Exception { .body(request) .when().post("/report") .then().log().all() - .statusCode(HttpStatus.OK.value()); + .assertThat() + .status(HttpStatus.OK); } @Test @@ -131,7 +133,7 @@ void reportNickname() throws Exception { given(memberService.findById(anyLong())).willReturn(member); ReportRequest request = new ReportRequest(ReportType.NICKNAME, 1L, "불건전한 닉네임"); - willDoNothing().given(reportService).report(member, request); + willDoNothing().given(reportCommandService).report(member, request); // when, then RestAssuredMockMvc @@ -141,7 +143,8 @@ void reportNickname() throws Exception { .body(request) .when().post("/report") .then().log().all() - .statusCode(HttpStatus.OK.value()); + .assertThat() + .status(HttpStatus.OK); } } @@ -165,7 +168,7 @@ void report(Long id) throws Exception { given(memberService.findById(anyLong())).willReturn(member); ReportRequest request = new ReportRequest(ReportType.COMMENT, id, "불건전한 게시글"); - willDoNothing().given(reportService).report(member, request); + willDoNothing().given(reportCommandService).report(member, request); // when, then RestAssuredMockMvc @@ -175,7 +178,8 @@ void report(Long id) throws Exception { .body(request) .when().post("/report") .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); + .assertThat() + .status(HttpStatus.BAD_REQUEST); } @ParameterizedTest @@ -197,7 +201,7 @@ void reportBadRequest(String reason) throws Exception { given(memberService.findById(anyLong())).willReturn(member); ReportRequest request = new ReportRequest(ReportType.POST, 1L, reason); - willDoNothing().given(reportService).report(member, request); + willDoNothing().given(reportCommandService).report(member, request); // when, then RestAssuredMockMvc @@ -207,7 +211,8 @@ void reportBadRequest(String reason) throws Exception { .body(request) .when().post("/report") .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()); + .assertThat() + .status(HttpStatus.BAD_REQUEST); } } diff --git a/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java b/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java index 2c1422730..6e489bd15 100644 --- a/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/report/repository/ReportRepositoryTest.java @@ -12,7 +12,10 @@ import com.votogether.domain.report.entity.vo.ReportType; import com.votogether.test.annotation.RepositoryTest; import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.persister.PostTestPersister; +import com.votogether.test.persister.ReportTestPersister; import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,34 +32,37 @@ class ReportRepositoryTest { @Autowired PostRepository postRepository; + @Autowired + PostTestPersister postTestPersister; + + @Autowired + ReportTestPersister reportTestPersister; + + @Test @DisplayName("회원, 신고타입, 대상ID를 통해서 신고 횟수를 반환한다.") void countByMemberAndReportTypeAndTargetId() { // given Member member = MemberFixtures.MALE_20.get(); ReportType reportType = ReportType.POST; - PostBody postBody = PostBody.builder() .title("title") .content("content") .build(); - Post post = Post.builder() + memberRepository.save(member); + Post post = postTestPersister.builder() .writer(member) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - memberRepository.save(member); - postRepository.save(post); + .save(); - Report report = Report.builder() + reportTestPersister.builder() .member(member) .reportType(reportType) .targetId(post.getId()) .reason("불건전한 게시글") - .build(); - reportRepository.save(report); + .save(); // when int reportCount = reportRepository.countByReportTypeAndTargetId(reportType, post.getId()); @@ -76,22 +82,19 @@ void findByMemberAndReportTypeAndTargetId() { .content("content") .build(); - Post post = Post.builder() + memberRepository.save(member); + Post post = postTestPersister.builder() .writer(member) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); + .save(); - memberRepository.save(member); - postRepository.save(post); - - Report report = Report.builder() - .targetId(post.getId()) - .reportType(ReportType.POST) + reportTestPersister.builder() .member(member) + .reportType(ReportType.POST) + .targetId(post.getId()) .reason("불건전한 게시글") - .build(); - reportRepository.save(report); + .save(); // when Report actualReport = reportRepository.findByMemberAndReportTypeAndTargetId( @@ -108,4 +111,37 @@ void findByMemberAndReportTypeAndTargetId() { ); } + @Test + @DisplayName("신고유형, 신고대상ID를 통해 관련된 신고정보를 모두 삭제한다.") + void deleteByReportTypeAndTargetId() { + // given + Member member = MemberFixtures.FEMALE_30.get(); + Member reporterA = MemberFixtures.MALE_30.get(); + Member reporterB = MemberFixtures.FEMALE_20.get(); + + memberRepository.save(member); + memberRepository.save(reporterA); + memberRepository.save(reporterB); + + reportTestPersister.builder() + .member(reporterA) + .reportType(ReportType.NICKNAME) + .targetId(member.getId()) + .reason("불건전한 게시글") + .save(); + + reportTestPersister.builder() + .member(reporterB) + .reportType(ReportType.NICKNAME) + .targetId(member.getId()) + .reason("불건전한 게시글") + .save(); + + // when + reportRepository.deleteByReportTypeAndTargetId(ReportType.NICKNAME, member.getId()); + + // then + assertThat(reportRepository.findAll()).isEmpty(); + } + } diff --git a/backend/src/test/java/com/votogether/domain/report/service/ReportServiceTest.java b/backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java similarity index 79% rename from backend/src/test/java/com/votogether/domain/report/service/ReportServiceTest.java rename to backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java index 74374aed9..19853b5ae 100644 --- a/backend/src/test/java/com/votogether/domain/report/service/ReportServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/report/service/ReportCommandServiceTest.java @@ -11,6 +11,7 @@ import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; +import com.votogether.domain.post.entity.comment.Content; import com.votogether.domain.post.entity.vo.PostClosingType; import com.votogether.domain.post.entity.vo.PostSortType; import com.votogether.domain.post.repository.CommentRepository; @@ -24,6 +25,8 @@ import com.votogether.global.exception.NotFoundException; import com.votogether.test.annotation.ServiceTest; import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.persister.CommentTestPersister; +import com.votogether.test.persister.PostTestPersister; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -32,10 +35,10 @@ import org.springframework.beans.factory.annotation.Autowired; @ServiceTest -class ReportServiceTest { +class ReportCommandServiceTest { @Autowired - ReportService reportService; + ReportCommandService reportCommandService; @Autowired MemberRepository memberRepository; @@ -55,6 +58,12 @@ class ReportServiceTest { @Autowired PostCommentService postCommentService; + @Autowired + PostTestPersister postTestPersister; + + @Autowired + CommentTestPersister commentTestPersister; + @Nested @DisplayName("게시글 신고기능은") class ReportPost { @@ -71,18 +80,16 @@ void reportPost() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - postRepository.save(post); + .save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then - assertDoesNotThrow(() -> reportService.report(reporter, request)); + assertDoesNotThrow(() -> reportCommandService.report(reporter, request)); } @Test @@ -94,7 +101,7 @@ void reportNonExistPostThrowsException() { ReportRequest request = new ReportRequest(ReportType.POST, -1L, "불건전한 게시글"); // when, then - assertThatThrownBy(() -> reportService.report(writer, request)) + assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(NotFoundException.class) .hasMessage("해당 게시글이 존재하지 않습니다."); } @@ -110,18 +117,16 @@ void reportOwnPostThrowsException() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - postRepository.save(post); + .save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then - assertThatThrownBy(() -> reportService.report(writer, request)) + assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(BadRequestException.class) .hasMessage("자신의 게시글은 신고할 수 없습니다."); } @@ -138,20 +143,18 @@ void reportHiddenPost() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - post.blind(); - - postRepository.save(post); + .blind() + .save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when, then - assertThatThrownBy(() -> reportService.report(reporter, request)) + assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) .hasMessage("이미 블라인드 처리된 글입니다."); } @@ -168,21 +171,19 @@ void reportDuplicated() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - postRepository.save(post); + .save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when - reportService.report(reporter, request); + reportCommandService.report(reporter, request); // then - assertThatThrownBy(() -> reportService.report(reporter, request)) + assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) .hasMessage("하나의 글에 대해서 중복하여 신고할 수 없습니다."); } @@ -203,22 +204,20 @@ void reportAndBlind() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); - - postRepository.save(post); + .save(); ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); // when - reportService.report(reporter1, request); - reportService.report(reporter2, request); - reportService.report(reporter3, request); - reportService.report(reporter4, request); - reportService.report(reporter5, request); + reportCommandService.report(reporter1, request); + reportCommandService.report(reporter2, request); + reportCommandService.report(reporter3, request); + reportCommandService.report(reporter4, request); + reportCommandService.report(reporter5, request); // then final List responses = postService.getPostsGuest( @@ -252,25 +251,22 @@ void reportComment() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); + .save(); - Comment comment = Comment.builder() + Comment comment = commentTestPersister.builder() .post(post) .member(writer) - .content("으어어어어") - .build(); - - postRepository.save(post); - commentRepository.save(comment); + .content(new Content("으어어어어")) + .save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 게시글"); // when, then - assertDoesNotThrow(() -> reportService.report(reporter, request)); + assertDoesNotThrow(() -> reportCommandService.report(reporter, request)); } @Test @@ -282,7 +278,7 @@ void reportNonExistCommentThrowsException() { ReportRequest request = new ReportRequest(ReportType.COMMENT, -1L, "불건전한 댓글"); // when, then - assertThatThrownBy(() -> reportService.report(writer, request)) + assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(NotFoundException.class) .hasMessage("해당 댓글이 존재하지 않습니다."); } @@ -298,25 +294,22 @@ void reportOwnCommentThrowsException() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); + .save(); - Comment comment = Comment.builder() + Comment comment = commentTestPersister.builder() .post(post) .member(writer) - .content("으어어어어") - .build(); - - postRepository.save(post); - commentRepository.save(comment); + .content(new Content("으어어어어")) + .save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when, then - assertThatThrownBy(() -> reportService.report(writer, request)) + assertThatThrownBy(() -> reportCommandService.report(writer, request)) .isInstanceOf(BadRequestException.class) .hasMessage("자신의 댓글은 신고할 수 없습니다."); } @@ -333,26 +326,23 @@ void reportHiddenComment() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); + .save(); - Comment comment = Comment.builder() + Comment comment = commentTestPersister.builder() .post(post) .member(writer) - .content("으어어어어") - .build(); - - postRepository.save(post); - commentRepository.save(comment); + .content(new Content("으어어어어")) + .save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when, then comment.blind(); - assertThatThrownBy(() -> reportService.report(reporter, request)) + assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) .hasMessage("이미 블라인드 처리된 댓글입니다."); } @@ -369,28 +359,25 @@ void reportDuplicated() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); + .save(); - Comment comment = Comment.builder() + Comment comment = commentTestPersister.builder() .post(post) .member(writer) - .content("으어어어어") - .build(); - - postRepository.save(post); - commentRepository.save(comment); + .content(new Content("으어어어어")) + .save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when - reportService.report(reporter, request); + reportCommandService.report(reporter, request); // then - assertThatThrownBy(() -> reportService.report(reporter, request)) + assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) .hasMessage("하나의 댓글에 대해서 중복하여 신고할 수 없습니다."); } @@ -411,29 +398,26 @@ void reportAndBlind() { .content("content") .build(); - Post post = Post.builder() + Post post = postTestPersister.builder() .writer(writer) .postBody(postBody) .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) - .build(); + .save(); - Comment comment = Comment.builder() + Comment comment = commentTestPersister.builder() .post(post) .member(writer) - .content("으어어어어") - .build(); - - postRepository.save(post); - commentRepository.save(comment); + .content(new Content("으어어어어")) + .save(); ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); // when - reportService.report(reporter1, request); - reportService.report(reporter2, request); - reportService.report(reporter3, request); - reportService.report(reporter4, request); - reportService.report(reporter5, request); + reportCommandService.report(reporter1, request); + reportCommandService.report(reporter2, request); + reportCommandService.report(reporter3, request); + reportCommandService.report(reporter4, request); + reportCommandService.report(reporter5, request); // then assertAll( @@ -458,7 +442,7 @@ void reportNickname() { ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); // when, then - assertDoesNotThrow(() -> reportService.report(reporter, request)); + assertDoesNotThrow(() -> reportCommandService.report(reporter, request)); } @Test @@ -470,7 +454,7 @@ void reportOwnNicknameThrowsException() { ReportRequest request = new ReportRequest(ReportType.NICKNAME, reporter.getId(), "불건전한 닉네임"); // when, then - assertThatThrownBy(() -> reportService.report(reporter, request)) + assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) .hasMessage("자신의 닉네임은 신고할 수 없습니다."); } @@ -485,10 +469,10 @@ void reportDuplicated() { ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); // when - reportService.report(reporter, request); + reportCommandService.report(reporter, request); // then - assertThatThrownBy(() -> reportService.report(reporter, request)) + assertThatThrownBy(() -> reportCommandService.report(reporter, request)) .isInstanceOf(BadRequestException.class) .hasMessage("하나의 닉네임에 대해서 중복하여 신고할 수 없습니다."); } @@ -505,9 +489,9 @@ void reportAndBlind() { ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); // when - reportService.report(reporter1, request); - reportService.report(reporter2, request); - reportService.report(reporter3, request); + reportCommandService.report(reporter1, request); + reportCommandService.report(reporter2, request); + reportCommandService.report(reporter3, request); // then assertThat(reported.getNickname()).contains("Pause1"); diff --git a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java new file mode 100644 index 000000000..bfd3deb04 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportCommentStrategyTest.java @@ -0,0 +1,234 @@ +package com.votogether.domain.report.service.strategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.repository.MemberRepository; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostBody; +import com.votogether.domain.post.entity.comment.Comment; +import com.votogether.domain.post.entity.comment.Content; +import com.votogether.domain.post.service.PostCommentService; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import com.votogether.test.annotation.ServiceTest; +import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.persister.CommentTestPersister; +import com.votogether.test.persister.PostTestPersister; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +@DisplayName("댓글 신고기능은") +class ReportCommentStrategyTest { + + @Autowired + ReportCommentStrategy reportCommentStrategy; + + @Autowired + MemberRepository memberRepository; + + @Autowired + PostTestPersister postTestPersister; + + @Autowired + PostCommentService postCommentService; + + @Autowired + CommentTestPersister commentTestPersister; + + @Test + @DisplayName("정상적으로 동작한다.") + void reportComment() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + Comment comment = commentTestPersister.builder() + .post(post) + .member(writer) + .content(new Content("으어어어어")) + .save(); + + ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 게시글"); + + // when, then + assertDoesNotThrow(() -> reportCommentStrategy.report(reporter, request)); + } + + @Test + @DisplayName("없는 댓글을 신고하는 경우 예외가 발생한다.") + void reportNonExistCommentThrowsException() { + // given + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + ReportRequest request = new ReportRequest(ReportType.COMMENT, -1L, "불건전한 댓글"); + + // when, then + assertThatThrownBy(() -> reportCommentStrategy.report(writer, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("해당 댓글이 존재하지 않습니다."); + } + + @Test + @DisplayName("자신의 댓글을 신고하는 경우 예외가 발생한다.") + void reportOwnCommentThrowsException() { + // given + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + Comment comment = commentTestPersister.builder() + .post(post) + .member(writer) + .content(new Content("으어어어어")) + .save(); + + ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); + + // when, then + assertThatThrownBy(() -> reportCommentStrategy.report(writer, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("자신의 댓글은 신고할 수 없습니다."); + } + + @Test + @DisplayName("블라인드 처리된 댓글을 신고하는 경우 예외가 발생한다.") + void reportHiddenComment() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + Comment comment = commentTestPersister.builder() + .post(post) + .member(writer) + .content(new Content("으어어어어")) + .save(); + + ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); + + // when, then + comment.blind(); + assertThatThrownBy(() -> reportCommentStrategy.report(reporter, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 블라인드 처리된 댓글입니다."); + } + + @Test + @DisplayName("하나의 회원이 댓글을 중복하여 신고하면 예외를 던진다.") + void reportDuplicated() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + Comment comment = commentTestPersister.builder() + .post(post) + .member(writer) + .content(new Content("으어어어어")) + .save(); + + ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); + + // when + reportCommentStrategy.report(reporter, request); + + // then + assertThatThrownBy(() -> reportCommentStrategy.report(reporter, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("하나의 댓글에 대해서 중복하여 신고할 수 없습니다."); + } + + @Test + @DisplayName("댓글 신고가 5회가 되면 블라인드 처리가 된다.") + void reportAndBlind() { + // given + Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); + Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); + Member reporter4 = memberRepository.save(MemberFixtures.FEMALE_50.get()); + Member reporter5 = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + Comment comment = commentTestPersister.builder() + .post(post) + .member(writer) + .content(new Content("으어어어어")) + .save(); + + ReportRequest request = new ReportRequest(ReportType.COMMENT, comment.getId(), "불건전한 댓글"); + + // when + reportCommentStrategy.report(reporter1, request); + reportCommentStrategy.report(reporter2, request); + reportCommentStrategy.report(reporter3, request); + reportCommentStrategy.report(reporter4, request); + reportCommentStrategy.report(reporter5, request); + + // then + assertAll( + () -> assertThat(comment.isHidden()).isTrue(), + () -> assertThat(postCommentService.getComments(post.getId())).isEmpty() + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java new file mode 100644 index 000000000..b8b47981f --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportNicknameStrategyTest.java @@ -0,0 +1,100 @@ +package com.votogether.domain.report.service.strategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.repository.MemberRepository; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.domain.report.repository.ReportRepository; +import com.votogether.global.exception.BadRequestException; +import com.votogether.test.annotation.ServiceTest; +import com.votogether.test.fixtures.MemberFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +@DisplayName("닉네임 신고기능은") +class ReportNicknameStrategyTest { + + @Autowired + ReportNicknameStrategy reportNicknameStrategy; + + @Autowired + ReportRepository reportRepository; + + @Autowired + MemberRepository memberRepository; + + @Test + @DisplayName("정상적으로 동작한다.") + void reportNickname() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member reported = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); + + // when, then + assertDoesNotThrow(() -> reportNicknameStrategy.report(reporter, request)); + } + + @Test + @DisplayName("자신의 닉네임을 신고하는 경우 예외가 발생한다.") + void reportOwnNicknameThrowsException() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + ReportRequest request = new ReportRequest(ReportType.NICKNAME, reporter.getId(), "불건전한 닉네임"); + + // when, then + assertThatThrownBy(() -> reportNicknameStrategy.report(reporter, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("자신의 닉네임은 신고할 수 없습니다."); + } + + @Test + @DisplayName("하나의 회원이 다른 회원의 닉네임을 중복하여 신고하면 예외를 던진다.") + void reportDuplicated() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Member reported = memberRepository.save(MemberFixtures.FEMALE_10.get()); + + ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); + + // when + reportNicknameStrategy.report(reporter, request); + + // then + assertThatThrownBy(() -> reportNicknameStrategy.report(reporter, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("하나의 닉네임에 대해서 중복하여 신고할 수 없습니다."); + } + + @Test + @DisplayName("닉네임 신고가 3회가 되면 닉네임이 자동변경처리가 된다.") + void reportAndBlind() { + // given + Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); + Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); + Member reported = memberRepository.save(MemberFixtures.FEMALE_10.get()); + + ReportRequest request = new ReportRequest(ReportType.NICKNAME, reported.getId(), "불건전한 닉네임"); + + // when + reportNicknameStrategy.report(reporter1, request); + reportNicknameStrategy.report(reporter2, request); + reportNicknameStrategy.report(reporter3, request); + + // then + assertAll( + () -> assertThat(reported.getNickname()).contains("Pause1"), + () -> assertThat(reportRepository.findAll()).isEmpty() + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java new file mode 100644 index 000000000..fee18d357 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/report/service/strategy/ReportPostStrategyTest.java @@ -0,0 +1,209 @@ +package com.votogether.domain.report.service.strategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.repository.MemberRepository; +import com.votogether.domain.post.dto.response.post.PostResponse; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.PostBody; +import com.votogether.domain.post.entity.vo.PostClosingType; +import com.votogether.domain.post.entity.vo.PostSortType; +import com.votogether.domain.post.service.PostService; +import com.votogether.domain.report.dto.request.ReportRequest; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; +import com.votogether.test.annotation.ServiceTest; +import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.persister.PostTestPersister; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +@DisplayName("게시글 신고 기능은") +class ReportPostStrategyTest { + + @Autowired + ReportPostStrategy reportPostStrategy; + + @Autowired + MemberRepository memberRepository; + + @Autowired + PostTestPersister postTestPersister; + + @Autowired + PostService postService; + + @Test + @DisplayName("정상적으로 동작한다.") + void reportPost() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); + + // when, then + assertDoesNotThrow(() -> reportPostStrategy.report(reporter, request)); + } + + @Test + @DisplayName("없는 투표글을 신고하는 경우 예외가 발생한다.") + void reportNonExistPostThrowsException() { + // given + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + ReportRequest request = new ReportRequest(ReportType.POST, -1L, "불건전한 게시글"); + + // when, then + assertThatThrownBy(() -> reportPostStrategy.report(writer, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("해당 게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("자신의 투표글을 신고하는 경우 예외가 발생한다.") + void reportOwnPostThrowsException() { + // given + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); + + // when, then + assertThatThrownBy(() -> reportPostStrategy.report(writer, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("자신의 게시글은 신고할 수 없습니다."); + } + + @Test + @DisplayName("블라인드 처리된 투표글을 신고하는 경우 예외가 발생한다.") + void reportHiddenPost() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_30.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .blind() + .save(); + + ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); + + // when, then + + assertThatThrownBy(() -> reportPostStrategy.report(reporter, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 블라인드 처리된 글입니다."); + } + + @Test + @DisplayName("하나의 회원이 투표글을 중복하여 신고하면 예외를 던진다.") + void reportDuplicated() { + // given + Member reporter = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); + + // when + reportPostStrategy.report(reporter, request); + + // then + assertThatThrownBy(() -> reportPostStrategy.report(reporter, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("하나의 글에 대해서 중복하여 신고할 수 없습니다."); + } + + @Test + @DisplayName("투표글 신고가 5회가 되면 블라인드 처리가 된다.") + void reportAndBlind() { + // given + Member reporter1 = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Member reporter2 = memberRepository.save(MemberFixtures.FEMALE_30.get()); + Member reporter3 = memberRepository.save(MemberFixtures.FEMALE_40.get()); + Member reporter4 = memberRepository.save(MemberFixtures.FEMALE_50.get()); + Member reporter5 = memberRepository.save(MemberFixtures.FEMALE_60.get()); + Member writer = memberRepository.save(MemberFixtures.FEMALE_10.get()); + + PostBody postBody = PostBody.builder() + .title("title") + .content("content") + .build(); + + Post post = postTestPersister.builder() + .writer(writer) + .postBody(postBody) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .save(); + + ReportRequest request = new ReportRequest(ReportType.POST, post.getId(), "불건전한 게시글"); + + // when + reportPostStrategy.report(reporter1, request); + reportPostStrategy.report(reporter2, request); + reportPostStrategy.report(reporter3, request); + reportPostStrategy.report(reporter4, request); + reportPostStrategy.report(reporter5, request); + + // then + final List responses = postService.getPostsGuest( + 0, + PostClosingType.ALL, + PostSortType.HOT, + null + ); + + assertAll( + () -> assertThat(post.isHidden()).isTrue(), + () -> assertThat(responses).isEmpty() + ); + } + +} diff --git a/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java b/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java new file mode 100644 index 000000000..e01c3dd12 --- /dev/null +++ b/backend/src/test/java/com/votogether/test/persister/CommentTestPersister.java @@ -0,0 +1,52 @@ +package com.votogether.test.persister; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.post.entity.Post; +import com.votogether.domain.post.entity.comment.Comment; +import com.votogether.domain.post.entity.comment.Content; +import com.votogether.domain.post.repository.CommentRepository; +import com.votogether.test.fixtures.MemberFixtures; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Persister +public class CommentTestPersister { + + private final CommentRepository commentRepository; + + public CommentBuilder builder() { + return new CommentTestPersister.CommentBuilder(); + } + + public final class CommentBuilder { + + private Post post; + private Member member; + private Content content; + + public CommentBuilder post(Post post) { + this.post = post; + return this; + } + + public CommentBuilder member(Member member) { + this.member = member; + return this; + } + + public CommentBuilder content(Content content) { + this.content = content; + return this; + } + + public Comment save() { + Comment comment = Comment.builder() + .post(post == null ? Post.builder().build() : post) + .member(member == null ? MemberFixtures.MALE_20.get() : member) + .content(content == null ? "content" : content.getValue()) + .build(); + return commentRepository.save(comment); + } + } + +} diff --git a/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java b/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java index 7f2e3cfb0..2826bd6e9 100644 --- a/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/PostTestPersister.java @@ -4,8 +4,10 @@ import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.repository.PostRepository; +import com.votogether.domain.post.service.PostService; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; @RequiredArgsConstructor @Persister @@ -23,6 +25,7 @@ public final class PostBuilder { private Member writer; private PostBody postBody; private LocalDateTime deadline; + private boolean isHidden; public PostBuilder writer(Member writer) { this.writer = writer; @@ -39,12 +42,21 @@ public PostBuilder deadline(LocalDateTime deadline) { return this; } + public PostBuilder blind() { + this.isHidden = true; + return this; + } + public Post save() { Post post = Post.builder() .writer(writer == null ? memberTestPersister.builder().save() : writer) .postBody(postBody == null ? generatePostBody() : postBody) .deadline(deadline == null ? LocalDateTime.of(2100, 12, 25, 0, 0) : deadline) .build(); + if (isHidden) { + post.blind(); + } + return postRepository.save(post); } @@ -54,7 +66,6 @@ private PostBody generatePostBody() { .content("content") .build(); } - } } diff --git a/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java b/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java new file mode 100644 index 000000000..a2b2c88a5 --- /dev/null +++ b/backend/src/test/java/com/votogether/test/persister/ReportTestPersister.java @@ -0,0 +1,59 @@ +package com.votogether.test.persister; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.report.entity.Report; +import com.votogether.domain.report.entity.vo.ReportType; +import com.votogether.domain.report.repository.ReportRepository; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Persister +public class ReportTestPersister { + + private final ReportRepository reportRepository; + private final MemberTestPersister memberTestPersister; + + public ReportBuilder builder() { + return new ReportTestPersister.ReportBuilder(); + } + + public final class ReportBuilder { + + private Member member; + private ReportType reportType; + private Long targetId; + private String reason; + + public ReportBuilder member(final Member member) { + this.member = member; + return this; + } + + public ReportBuilder reportType(final ReportType reportType) { + this.reportType = reportType; + return this; + } + + public ReportBuilder targetId(final Long targetId) { + this.targetId = targetId; + return this; + } + + public ReportBuilder reason(final String reason) { + this.reason = reason; + return this; + } + + public Report save() { + Report report = Report.builder() + .member(member == null ? memberTestPersister.builder().save() : member) + .reportType(reportType == null ? ReportType.POST : reportType) + .targetId(targetId == null ? 1L : targetId) + .reason(reason == null ? "reason" : reason) + .build(); + + return reportRepository.save(report); + } + } +} From 648931dbc07c0d30a6f7715c8080dcfe48165242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?= <80146176+Gilpop8663@users.noreply.github.com> Date: Fri, 15 Sep 2023 18:19:29 +0900 Subject: [PATCH 32/48] =?UTF-8?q?=EC=A0=95=EC=82=AC=EA=B0=81=ED=98=95?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=82=98=EC=98=A4=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EB=84=88=EB=B9=84=EA=B0=92=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20(#623)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: (#620) package-lock.json 업데이트 * fix: (#621) 스토리북을 이용하려고 할 때 웹팩 chunk 관련 에러가 나는 것 수정 * style: (#620) 이미지의 가로, 세로 비율에 맞게 이미지를 보여주도록 수정 * style: (#620) 게시글 작성 시 선택지 이미지에 적용 * chore: (#620) 변경된 컴포넌트 이름이 적용되어 있지 않던 스토리북 파일 수정 * style: (#620) 게시글 작성시 본문 이미지에도 적용 --- frontend/.storybook/main.ts | 12 +- frontend/package-lock.json | 109 +++++++++++++----- .../PostForm/ContentImageSection/style.ts | 2 +- frontend/src/components/common/Post/style.ts | 2 +- .../WritingVoteOption/style.ts | 2 +- .../ServiceIntroductionSection.stories.tsx | 14 +++ .../StartUsingOurService.stories.tsx | 14 --- 7 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 frontend/src/pages/auth/Login/ServiceIntroductionSection/ServiceIntroductionSection.stories.tsx delete mode 100644 frontend/src/pages/auth/Login/ServiceIntroductionSection/StartUsingOurService.stories.tsx diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index ac95e4da7..06768b5b8 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -2,6 +2,15 @@ import path from 'path'; import type { StorybookConfig } from '@storybook/react-webpack5'; const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const webpack = require('webpack'); + +function disableChunkSplitting(config) { + config.optimization = { splitChunks: { chunks: 'async' } }; + config.output = { ...config.output, chunkFilename: '[chunkhash].chunk.js' }; + config.plugins.push(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); + + return config; +} const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], @@ -31,7 +40,8 @@ const config: StorybookConfig = { configFile: path.resolve(__dirname, '../tsconfig.json'), }) ); - return config; + + return disableChunkSplitting(config); }, staticDirs: ['./public'], env: config => ({ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd8b4dfad..c7e8d3e25 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@tanstack/react-query": "^4.29.19", + "browser-image-compression": "^2.0.2", "dotenv": "^16.3.1", "msw": "^1.2.3", "react": "^18.2.0", @@ -9374,6 +9375,14 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browserify-zlib": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", @@ -22345,7 +22354,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22629,6 +22638,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -23907,7 +23921,8 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true + "dev": true, + "requires": {} }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.18.6", @@ -25001,7 +25016,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "dev": true + "dev": true, + "requires": {} }, "@esbuild/android-arm": { "version": "0.17.19", @@ -26101,7 +26117,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "find-up": { "version": "5.0.0", @@ -26596,7 +26613,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "ansi-styles": { "version": "4.3.0", @@ -28037,7 +28055,8 @@ "version": "7.0.26", "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.0.26.tgz", "integrity": "sha512-heobG4IovYAD9fo7qmUHylCSQjDd1eXDCOaTiy+XVKobHAJgkz1gKqbaFSP6KLkPE4cKyScku2K9mY0tcKIhMw==", - "dev": true + "dev": true, + "requires": {} }, "@storybook/react-webpack5": { "version": "7.0.26", @@ -29337,19 +29356,22 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/serve": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true + "dev": true, + "requires": {} }, "@xmldom/xmldom": { "version": "0.8.9", @@ -29427,13 +29449,15 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -29738,7 +29762,8 @@ "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "dev": true + "dev": true, + "requires": {} }, "babel-jest": { "version": "29.6.0", @@ -30124,6 +30149,14 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, + "browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "requires": { + "uzip": "0.20201231.0" + } + }, "browserify-zlib": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", @@ -32091,7 +32124,8 @@ "version": "8.8.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-config-react-app": { "version": "7.0.1", @@ -32298,7 +32332,8 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-storybook": { "version": "0.6.12", @@ -32878,7 +32913,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "ansi-styles": { "version": "4.3.0", @@ -33640,7 +33676,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "ieee754": { "version": "1.2.1", @@ -35266,7 +35303,8 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "29.4.3", @@ -36698,7 +36736,8 @@ "version": "7.2.1", "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.2.1.tgz", "integrity": "sha512-9HrdzBAo0+sFz9ZYAGT5fB8ilzTW+q6lPocRxrIesMO+aB40V9MgFfbfMXxlGjf22OpRy+IXlvVaQenicdpgbg==", - "dev": true + "dev": true, + "requires": {} }, "mdast-util-definitions": { "version": "4.0.0", @@ -37633,7 +37672,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.3", @@ -37995,7 +38035,8 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", - "dev": true + "dev": true, + "requires": {} }, "react-docgen": { "version": "5.4.3", @@ -38030,7 +38071,8 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", - "dev": true + "dev": true, + "requires": {} }, "react-dom": { "version": "18.2.0", @@ -38075,7 +38117,8 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz", "integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==", - "dev": true + "dev": true, + "requires": {} }, "react-is": { "version": "16.13.1", @@ -39104,7 +39147,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", - "dev": true + "dev": true, + "requires": {} }, "styled-components": { "version": "6.0.2", @@ -39369,7 +39413,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "json-schema-traverse": { "version": "0.4.1", @@ -39711,7 +39756,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true + "devOptional": true }, "uglify-js": { "version": "3.17.4", @@ -39873,7 +39918,8 @@ "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} }, "util": { "version": "0.12.5", @@ -39910,6 +39956,11 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -40044,7 +40095,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "json-schema-traverse": { "version": "0.4.1", @@ -40347,7 +40399,8 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "4.0.0", diff --git a/frontend/src/components/PostForm/ContentImageSection/style.ts b/frontend/src/components/PostForm/ContentImageSection/style.ts index ba5e9ddad..deb772d35 100644 --- a/frontend/src/components/PostForm/ContentImageSection/style.ts +++ b/frontend/src/components/PostForm/ContentImageSection/style.ts @@ -25,7 +25,7 @@ export const ContentImage = styled.img` border-radius: 4px; aspect-ratio: 1/1; - object-fit: cover; + object-fit: contain; `; export const FileInputContainer = styled.div` diff --git a/frontend/src/components/common/Post/style.ts b/frontend/src/components/common/Post/style.ts index 010eef162..ad3c11509 100644 --- a/frontend/src/components/common/Post/style.ts +++ b/frontend/src/components/common/Post/style.ts @@ -126,7 +126,7 @@ export const Image = styled.img` align-self: center; aspect-ratio: 1/1; - object-fit: cover; + object-fit: contain; @media (min-width: ${theme.breakpoint.md}) { margin-bottom: 20px; diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts index 27b301835..2a8bf0897 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts @@ -56,7 +56,7 @@ export const Image = styled.img` border-radius: 4px; aspect-ratio: 1/1; - object-fit: cover; + object-fit: contain; `; export const ImageCancelWrapper = styled.div` diff --git a/frontend/src/pages/auth/Login/ServiceIntroductionSection/ServiceIntroductionSection.stories.tsx b/frontend/src/pages/auth/Login/ServiceIntroductionSection/ServiceIntroductionSection.stories.tsx new file mode 100644 index 000000000..5eb8eccb0 --- /dev/null +++ b/frontend/src/pages/auth/Login/ServiceIntroductionSection/ServiceIntroductionSection.stories.tsx @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import ServiceIntroductionSection from '.'; + +const meta: Meta = { + component: ServiceIntroductionSection, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/frontend/src/pages/auth/Login/ServiceIntroductionSection/StartUsingOurService.stories.tsx b/frontend/src/pages/auth/Login/ServiceIntroductionSection/StartUsingOurService.stories.tsx deleted file mode 100644 index 286405d5f..000000000 --- a/frontend/src/pages/auth/Login/ServiceIntroductionSection/StartUsingOurService.stories.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import StartUsingOurService from '.'; - -const meta: Meta = { - component: StartUsingOurService, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => , -}; From 34c11f217ec10ef39569400ad4c5cc436bd84e1d Mon Sep 17 00:00:00 2001 From: lookh <103165859+aiaiaiai1@users.noreply.github.com> Date: Sat, 16 Sep 2023 16:43:58 +0900 Subject: [PATCH 33/48] =?UTF-8?q?fix:=20(#627)=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=98=A4=EB=A6=AC=EC=A7=84=20url=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/votogether/global/config/WebMvcConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java b/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java index a24c5ff44..1a067f48b 100644 --- a/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java +++ b/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java @@ -12,7 +12,7 @@ public class WebMvcConfig implements WebMvcConfigurer { private static final String LOCALHOST_FRONTEND = "http://localhost:3000"; - private static final String DEV_SERVER = "http://dev.votogether.com"; + private static final String DEV_SERVER = "https://dev.votogether.com"; private static final String PROD_SERVER = "https://votogether.com"; private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver; From 2887b8591b42dddc0cf28907c264e171c9758bfc Mon Sep 17 00:00:00 2001 From: Gilpop8663 Date: Fri, 15 Sep 2023 14:13:04 +0900 Subject: [PATCH 34/48] =?UTF-8?q?style:=20(#622)=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=EA=B0=80=20=EC=99=BC=EC=AA=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9B=80=EC=A7=81=EC=9D=B4=EB=8A=94=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WritingVoteOption/OptionUploadImageButton/style.ts | 4 ++++ .../WritingVoteOptionList/WritingVoteOption/style.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts index 8509c65fd..23f3e08ad 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts @@ -6,6 +6,7 @@ export const Container = styled.div<{ $isVisible: boolean }>` width: 24px; height: 24px; border-radius: 50%; + visibility: ${props => props.$isVisible && 'hidden'}; `; @@ -14,6 +15,9 @@ export const Label = styled.label` `; export const FileInput = styled.input` + position: absolute; + left: 0; + visibility: hidden; `; diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts index 2a8bf0897..b5c1f3af5 100644 --- a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts +++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts @@ -5,6 +5,8 @@ import { theme } from '@styles/theme'; export const Container = styled.li` display: flex; gap: 10px; + + position: relative; `; export const OptionContainer = styled.div` From 59b95cc24596871054599726c08f45a7a43b8ad8 Mon Sep 17 00:00:00 2001 From: lookh <103165859+aiaiaiai1@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:46:04 +0900 Subject: [PATCH 35/48] =?UTF-8?q?fix:=20(#629)=20https=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=ED=98=B8=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A6=AC?= =?UTF-8?q?=EC=A7=84=20url=20=EC=B6=94=EA=B0=80=20(#630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/votogether/global/config/WebMvcConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java b/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java index 1a067f48b..f8c6a29a9 100644 --- a/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java +++ b/backend/src/main/java/com/votogether/global/config/WebMvcConfig.java @@ -12,6 +12,7 @@ public class WebMvcConfig implements WebMvcConfigurer { private static final String LOCALHOST_FRONTEND = "http://localhost:3000"; + private static final String HTTPS_LOCALHOST_FRONTEND = "https://localhost:3000"; private static final String DEV_SERVER = "https://dev.votogether.com"; private static final String PROD_SERVER = "https://votogether.com"; @@ -30,7 +31,7 @@ public void addArgumentResolvers(final List resol public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders("*") - .allowedOrigins(LOCALHOST_FRONTEND, DEV_SERVER, PROD_SERVER) + .allowedOrigins(HTTPS_LOCALHOST_FRONTEND, LOCALHOST_FRONTEND, DEV_SERVER, PROD_SERVER) .allowedMethods("*") .allowCredentials(true) .exposedHeaders(HttpHeaders.LOCATION, HttpHeaders.SET_COOKIE); From 332b35f516db4aac9d5c444b1446a33b3f286622 Mon Sep 17 00:00:00 2001 From: Gilpop8663 Date: Fri, 15 Sep 2023 13:22:22 +0900 Subject: [PATCH 36/48] =?UTF-8?q?feat:=20(#602)=20=EB=B3=B8=EB=AC=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EB=B6=99=EC=97=AC?= =?UTF-8?q?=EB=84=A3=EA=B8=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EC=B2=A8=EB=B6=80?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/PostForm/index.tsx | 3 ++ frontend/src/hooks/useContentImage.ts | 51 ++++++++++------------ frontend/src/utils/post/uploadImage.ts | 38 ++++++++++++++++ 3 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 frontend/src/utils/post/uploadImage.ts diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index 6127dcd48..d337e724e 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -64,6 +64,8 @@ export default function PostForm({ data, mutate }: PostFormProps) { const navigate = useNavigate(); const contentImageHook = useContentImage(serverImageUrl); + const { handlePasteImage } = contentImageHook; + const writingOptionHook = useWritingOption( serverVoteInfo?.options.map(option => ({ ...option, @@ -214,6 +216,7 @@ export default function PostForm({ data, mutate }: PostFormProps) { placeholder={CONTENT_PLACEHOLDER} maxLength={POST_CONTENT.MAX_LENGTH} minLength={POST_CONTENT.MIN_LENGTH} + onPaste={handlePasteImage} required /> diff --git a/frontend/src/hooks/useContentImage.ts b/frontend/src/hooks/useContentImage.ts index 8a859eed2..cf36d375c 100644 --- a/frontend/src/hooks/useContentImage.ts +++ b/frontend/src/hooks/useContentImage.ts @@ -1,48 +1,43 @@ -import { ChangeEvent, useRef, useState } from 'react'; +import { ChangeEvent, ClipboardEvent, useRef, useState } from 'react'; -import { MAX_FILE_SIZE } from '@components/PostForm/constants'; - -import { convertImageToWebP } from '@utils/resizeImage'; +import { uploadImage } from '@utils/post/uploadImage'; export const useContentImage = (imageUrl: string = '') => { const [contentImage, setContentImage] = useState(imageUrl); const contentInputRef = useRef(null); + const handlePasteImage = async (event: ClipboardEvent) => { + const file = event.clipboardData.files[0]; + + if (file.type.slice(0, 5) === 'image') { + event.preventDefault(); + + uploadImage({ + imageFile: file, + inputElement: contentInputRef.current, + setPreviewUrlFunc: setContentImage, + }); + } + }; + const removeImage = () => { setContentImage(''); if (contentInputRef.current) contentInputRef.current.value = ''; }; - const handleUploadImage = async (event: ChangeEvent) => { + const handleUploadImage = (event: ChangeEvent) => { const { files } = event.target; if (!files) return; const file = files[0]; - const webpFileList = await convertImageToWebP(file); - - event.target.files = webpFileList; - - const reader = new FileReader(); - - const webpFile = webpFileList[0]; - - reader.readAsDataURL(webpFile); - - event.target.setCustomValidity(''); - - if (file.size > MAX_FILE_SIZE) { - event.target.setCustomValidity('사진의 용량은 1.5MB 이하만 가능합니다.'); - event.target.reportValidity(); - - return; - } - - reader.onloadend = () => { - setContentImage(reader.result?.toString() ?? ''); - }; + uploadImage({ + imageFile: file, + inputElement: contentInputRef.current, + setPreviewUrlFunc: setContentImage, + }); }; - return { contentImage, contentInputRef, removeImage, handleUploadImage }; + return { contentImage, contentInputRef, removeImage, handleUploadImage, handlePasteImage }; }; diff --git a/frontend/src/utils/post/uploadImage.ts b/frontend/src/utils/post/uploadImage.ts new file mode 100644 index 000000000..d1b4b94d4 --- /dev/null +++ b/frontend/src/utils/post/uploadImage.ts @@ -0,0 +1,38 @@ +import { MAX_FILE_SIZE } from '@constants/post'; + +import { convertImageToWebP } from '@utils/resizeImage'; + +export const uploadImage = async ({ + imageFile, + inputElement, + setPreviewUrlFunc, +}: { + imageFile: File; + inputElement: HTMLInputElement | null; + setPreviewUrlFunc: (previewUrl: string) => void; +}) => { + if (!inputElement) return; + + const webpFileList = await convertImageToWebP(imageFile); + + inputElement.files = webpFileList; + + const reader = new FileReader(); + + const webpFile = webpFileList[0]; + + reader.readAsDataURL(webpFile); + + inputElement.setCustomValidity(''); + + if (imageFile.size > MAX_FILE_SIZE) { + inputElement.setCustomValidity('사진의 용량은 1.5MB 이하만 가능합니다.'); + inputElement.reportValidity(); + + return; + } + + reader.onloadend = () => { + setPreviewUrlFunc(reader.result?.toString() ?? ''); + }; +}; From 5afb4e365448f1d2baf1eb0648fbb055797056bd Mon Sep 17 00:00:00 2001 From: Gilpop8663 Date: Fri, 15 Sep 2023 13:35:26 +0900 Subject: [PATCH 37/48] =?UTF-8?q?refactor:=20(#602)=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=20=EC=9E=91=EC=84=B1=EC=97=90=EC=84=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EB=90=98=EB=8A=94=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=ED=95=9C=20=EA=B2=83?= =?UTF-8?q?=EC=9D=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useWritingOption.tsx | 51 +++++++++---------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index 6f66236e1..1d23564c8 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -1,8 +1,6 @@ import React, { ChangeEvent, useRef, useState } from 'react'; -import { MAX_FILE_SIZE } from '@components/PostForm/constants'; - -import { convertImageToWebP } from '@utils/resizeImage'; +import { uploadImage } from '@utils/post/uploadImage'; const MAX_WRITING_LENGTH = 50; @@ -89,6 +87,18 @@ export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => }); }; + const setPreviewImageUrl = (optionId: number) => (imageUrl: string) => { + const updatedOptionList = optionList.map(optionItem => { + if (optionItem.id === optionId) { + return { ...optionItem, imageUrl }; + } + + return optionItem; + }); + + setOptionList(updatedOptionList); + }; + const handleUploadImage = async ( event: React.ChangeEvent, optionId: number @@ -99,36 +109,11 @@ export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => const file = files[0]; - const webpFileList = await convertImageToWebP(file); - - event.target.files = webpFileList; - - const reader = new FileReader(); - - const webpFile = webpFileList[0]; - - reader.readAsDataURL(webpFile); - - event.target.setCustomValidity(''); - - if (file.size > MAX_FILE_SIZE) { - event.target.setCustomValidity('사진의 용량은 1.5MB 이하만 가능합니다.'); - event.target.reportValidity(); - - return; - } - - reader.onloadend = () => { - const updatedOptionList = optionList.map(optionItem => { - if (optionItem.id === optionId) { - return { ...optionItem, imageUrl: reader.result?.toString() ?? '' }; - } - - return optionItem; - }); - - setOptionList(updatedOptionList); - }; + uploadImage({ + imageFile: file, + inputElement: event.target, + setPreviewUrlFunc: setPreviewImageUrl(optionId), + }); }; return { From 577b8fde4605f449d39e026d11e3b28b78a13d48 Mon Sep 17 00:00:00 2001 From: Gilpop8663 Date: Fri, 15 Sep 2023 13:40:38 +0900 Subject: [PATCH 38/48] =?UTF-8?q?refactor:=20(#602)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20async=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0,=20=EB=B6=80=EC=9E=90=EC=97=B0=EC=8A=A4=EB=9F=AC?= =?UTF-8?q?=EC=9A=B4=20=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useContentImage.ts | 6 +++--- frontend/src/hooks/useWritingOption.tsx | 2 +- frontend/src/utils/post/uploadImage.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/useContentImage.ts b/frontend/src/hooks/useContentImage.ts index cf36d375c..7c0a46675 100644 --- a/frontend/src/hooks/useContentImage.ts +++ b/frontend/src/hooks/useContentImage.ts @@ -6,7 +6,7 @@ export const useContentImage = (imageUrl: string = '') => { const [contentImage, setContentImage] = useState(imageUrl); const contentInputRef = useRef(null); - const handlePasteImage = async (event: ClipboardEvent) => { + const handlePasteImage = (event: ClipboardEvent) => { const file = event.clipboardData.files[0]; if (file.type.slice(0, 5) === 'image') { @@ -15,7 +15,7 @@ export const useContentImage = (imageUrl: string = '') => { uploadImage({ imageFile: file, inputElement: contentInputRef.current, - setPreviewUrlFunc: setContentImage, + setPreviewImageUrl: setContentImage, }); } }; @@ -35,7 +35,7 @@ export const useContentImage = (imageUrl: string = '') => { uploadImage({ imageFile: file, inputElement: contentInputRef.current, - setPreviewUrlFunc: setContentImage, + setPreviewImageUrl: setContentImage, }); }; diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx index 1d23564c8..6a91a986a 100644 --- a/frontend/src/hooks/useWritingOption.tsx +++ b/frontend/src/hooks/useWritingOption.tsx @@ -112,7 +112,7 @@ export const useWritingOption = (initialOptionList?: WritingVoteOptionType[]) => uploadImage({ imageFile: file, inputElement: event.target, - setPreviewUrlFunc: setPreviewImageUrl(optionId), + setPreviewImageUrl: setPreviewImageUrl(optionId), }); }; diff --git a/frontend/src/utils/post/uploadImage.ts b/frontend/src/utils/post/uploadImage.ts index d1b4b94d4..98fdfb650 100644 --- a/frontend/src/utils/post/uploadImage.ts +++ b/frontend/src/utils/post/uploadImage.ts @@ -5,11 +5,11 @@ import { convertImageToWebP } from '@utils/resizeImage'; export const uploadImage = async ({ imageFile, inputElement, - setPreviewUrlFunc, + setPreviewImageUrl, }: { imageFile: File; inputElement: HTMLInputElement | null; - setPreviewUrlFunc: (previewUrl: string) => void; + setPreviewImageUrl: (previewUrl: string) => void; }) => { if (!inputElement) return; @@ -33,6 +33,6 @@ export const uploadImage = async ({ } reader.onloadend = () => { - setPreviewUrlFunc(reader.result?.toString() ?? ''); + setPreviewImageUrl(reader.result?.toString() ?? ''); }; }; From 30fa5eea28e39e1f1d2401c86c93d5f55a22af00 Mon Sep 17 00:00:00 2001 From: chsua <113416448+chsua@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:03:54 +0900 Subject: [PATCH 39/48] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=A6=AC=EC=95=A1?= =?UTF-8?q?=ED=8A=B8=20=EC=BF=BC=EB=A6=AC=20=EC=BA=90=EC=8B=9C=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20fetch=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B8=B0=20(#626)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: (#619) 리액트 쿼리 캐싱 확인을 위한 세팅 * feat: (#619) 마감된 게시물 상세정보 1시간 캐싱하기 * design: 에러 메세지 컴포넌트 어절 단위로 개행 및 상하 마진 수정 * feat: (#619) 통계 서버데이터 형식을 사용처가 아닌 불러오는 곳에서 형식 변경 * feat: (#619) 통계 페이지 리액트 쿼리로 수정 및 서스펜스, 에러바운더리 적용 * fix: 통계 api msw에서 실서버로 요청 전환 * fix: thead, tbody가 없어서 발생하는 에러 해결 * fix: (#627) 개발서버 오리진 url 수정 (#628) * style: (#622) 선택지가 왼쪽으로 움직이는 버그 수정 * fix: (#629) https 로컬호스트 오리진 url 추가 (#630) * feat: (#602) 본문 이미지를 붙여넣기 이벤트로 이미지를 첨부할 수 있도록 구현 * refactor: (#602) 선택지 작성에서 중복되는 업로드 이미지 코드를 리팩터링한 것을 적용 * refactor: (#602) 사용하지 않는 async 제거, 부자연스러운 함수명 변경 * refactor: (#619) 불분명한 변수명 수정 * feat: 열정유저 테이블 내 2개의 api 중 하나라도 로딩/에러 시 테이블 폴백처리 * refactor: (#619) 사용하지 않는 useFetch 삭제 * feat: (#619) 통계 페이지 에러바운더리/ 서스펜스 범위 수정 - 기존: 글 전체 통계에 서스펜스와 에러바운더리 적용, post api에는 에러바운더리만 적용 - 수정: 글 전체 통계와 post Api 한번에 감싸는 서스펜스와 에러바운더리 적용 * refactor: (#619) 서스펜스, 에러바운더리 감싸는 순서를 기존 코드와 통일 - 에러바운더리 안에 서스펜스, 그 안에 api 호출하는 컴포넌트 * feat: 리액트 쿼리 데브 툴 설정 --------- Co-authored-by: lookh <103165859+aiaiaiai1@users.noreply.github.com> Co-authored-by: Gilpop8663 --- frontend/package-lock.json | 160 ++++++++++++++++-- frontend/package.json | 1 + frontend/src/App.tsx | 3 + frontend/src/api/voteResult.ts | 17 +- .../VoteStatistics/VoteStatistics.stories.tsx | 14 +- .../src/components/VoteStatistics/index.tsx | 9 +- .../components/common/ErrorMessage/style.ts | 8 +- frontend/src/constants/queryKey.ts | 1 + .../src/hooks/query/post/usePostDetail.ts | 20 ++- .../src/hooks/query/useVoteStatistics.tsx | 21 +++ frontend/src/hooks/useFetch.ts | 30 ---- .../src/pages/Ranking/PassionUser/index.tsx | 84 ++++----- .../src/pages/Ranking/PassionUser/style.ts | 12 ++ .../src/pages/Ranking/PopularPost/index.tsx | 48 +++--- .../OptionStatistics/index.tsx | 45 ++--- .../OptionWrapper/index.tsx | 35 ++++ .../StatisticsWrapper/index.tsx | 19 +++ .../src/pages/VoteStatisticsPage/index.tsx | 79 +++------ .../src/pages/VoteStatisticsPage/style.ts | 2 +- 19 files changed, 387 insertions(+), 221 deletions(-) create mode 100644 frontend/src/hooks/query/useVoteStatistics.tsx delete mode 100644 frontend/src/hooks/useFetch.ts create mode 100644 frontend/src/pages/VoteStatisticsPage/OptionWrapper/index.tsx create mode 100644 frontend/src/pages/VoteStatisticsPage/StatisticsWrapper/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c7e8d3e25..2d097b870 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,6 +29,7 @@ "@storybook/react": "^7.0.26", "@storybook/react-webpack5": "^7.0.26", "@storybook/testing-library": "^0.0.14-next.2", + "@tanstack/react-query-devtools": "^4.35.3", "@testing-library/react": "^14.0.0", "@types/jest": "^29.5.2", "@types/react": "^18.2.14", @@ -7035,21 +7036,37 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "dev": true, + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, "node_modules/@tanstack/query-core": { - "version": "4.29.19", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.19.tgz", - "integrity": "sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw==", + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.35.3.tgz", + "integrity": "sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "4.29.19", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.19.tgz", - "integrity": "sha512-XiTIOHHQ5Cw1WUlHaD4fmVUMhoWjuNJlAeJGq7eM4BraI5z7y8WkZO+NR8PSuRnQGblpuVdjClQbDFtwxTtTUw==", + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.35.3.tgz", + "integrity": "sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw==", "dependencies": { - "@tanstack/query-core": "4.29.19", + "@tanstack/query-core": "4.35.3", "use-sync-external-store": "^1.2.0" }, "funding": { @@ -7070,6 +7087,26 @@ } } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.35.3.tgz", + "integrity": "sha512-UvLT7qPzCuCZ3NfjwsOqDUVN84JvSOuW6ukrjZmSqgjPqVxD6ra/HUp1CEOatQY2TRvKCp8y1lTVu+trXM30fg==", + "dev": true, + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^4.35.3", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "8.20.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", @@ -10156,6 +10193,21 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -14901,6 +14953,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -20477,6 +20541,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==", + "dev": true + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -21616,6 +21686,18 @@ "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==", "dev": true }, + "node_modules/superjson": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.1.tgz", + "integrity": "sha512-AVH2eknm9DEd3qvxM4Sq+LTCkSXE2ssfh1t11MHMXyYXFQyQ1HLgVvV+guLTsaQnJU3gnaVo34TohHPulY/wLg==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -28265,17 +28347,37 @@ "file-system-cache": "2.3.0" } }, + "@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "dev": true, + "requires": { + "remove-accents": "0.4.2" + } + }, "@tanstack/query-core": { - "version": "4.29.19", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.19.tgz", - "integrity": "sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw==" + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.35.3.tgz", + "integrity": "sha512-PS+WEjd9wzKTyNjjQymvcOe1yg8f3wYc6mD+vb6CKyZAKvu4sIJwryfqfBULITKCla7P9C4l5e9RXePHvZOZeQ==" }, "@tanstack/react-query": { - "version": "4.29.19", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.19.tgz", - "integrity": "sha512-XiTIOHHQ5Cw1WUlHaD4fmVUMhoWjuNJlAeJGq7eM4BraI5z7y8WkZO+NR8PSuRnQGblpuVdjClQbDFtwxTtTUw==", + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.35.3.tgz", + "integrity": "sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw==", + "requires": { + "@tanstack/query-core": "4.35.3", + "use-sync-external-store": "^1.2.0" + } + }, + "@tanstack/react-query-devtools": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.35.3.tgz", + "integrity": "sha512-UvLT7qPzCuCZ3NfjwsOqDUVN84JvSOuW6ukrjZmSqgjPqVxD6ra/HUp1CEOatQY2TRvKCp8y1lTVu+trXM30fg==", + "dev": true, "requires": { - "@tanstack/query-core": "4.29.19", + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", "use-sync-external-store": "^1.2.0" } }, @@ -30714,6 +30816,15 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "requires": { + "is-what": "^4.1.8" + } + }, "copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -34157,6 +34268,12 @@ "get-intrinsic": "^1.1.1" } }, + "is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -38302,6 +38419,12 @@ "unist-util-visit": "^2.0.0" } }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==", + "dev": true + }, "renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -39183,6 +39306,15 @@ "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==", "dev": true }, + "superjson": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.1.tgz", + "integrity": "sha512-AVH2eknm9DEd3qvxM4Sq+LTCkSXE2ssfh1t11MHMXyYXFQyQ1HLgVvV+guLTsaQnJU3gnaVo34TohHPulY/wLg==", + "dev": true, + "requires": { + "copy-anything": "^3.0.2" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index edfcc6404..1a197661b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "@storybook/react": "^7.0.26", "@storybook/react-webpack5": "^7.0.26", "@storybook/testing-library": "^0.0.14-next.2", + "@tanstack/react-query-devtools": "^4.35.3", "@testing-library/react": "^14.0.0", "@types/jest": "^29.5.2", "@types/react": "^18.2.14", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0d7708033..6c761e1e2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ThemeProvider } from 'styled-components'; import { AuthProvider } from '@hooks/context/auth'; @@ -37,8 +38,10 @@ const App = () => ( + + ); export default App; diff --git a/frontend/src/api/voteResult.ts b/frontend/src/api/voteResult.ts index e3960c0f5..b7400528f 100644 --- a/frontend/src/api/voteResult.ts +++ b/frontend/src/api/voteResult.ts @@ -1,11 +1,14 @@ -import { VoteResultResponse } from '@components/VoteStatistics/type'; +import { VoteResult, VoteResultResponse } from '@components/VoteStatistics/type'; +import { transVoteStatisticsFormat } from '@components/VoteStatistics/util'; import { getFetch } from '@utils/fetch'; const BASE_URL = process.env.VOTOGETHER_BASE_URL; -export const getPostStatistics = async (postId: number): Promise => { - return await getFetch(`${BASE_URL}/posts/${postId}/options`); +export const getPostStatistics = async (postId: number): Promise => { + const data = await getFetch(`${BASE_URL}/posts/${postId}/options`); + + return transVoteStatisticsFormat(data); }; export const getOptionStatistics = async ({ @@ -14,6 +17,10 @@ export const getOptionStatistics = async ({ }: { postId: number; optionId: number; -}): Promise => { - return await getFetch(`${BASE_URL}/posts/${postId}/options/${optionId}`); +}): Promise => { + const data = await getFetch( + `${BASE_URL}/posts/${postId}/options/${optionId}` + ); + + return transVoteStatisticsFormat(data); }; diff --git a/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx b/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx index acabbe502..92f496f1d 100644 --- a/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx +++ b/frontend/src/components/VoteStatistics/VoteStatistics.stories.tsx @@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { MOCK_VOTE_RESULT } from '@mocks/mockData/voteResult'; +import { transVoteStatisticsFormat } from './util'; + import VoteStatistics from '.'; const meta: Meta = { @@ -12,13 +14,19 @@ export default meta; type Story = StoryObj; export const SizeSm: Story = { - render: () => , + render: () => ( + + ), }; export const SizeMd: Story = { - render: () => , + render: () => ( + + ), }; export const SizeLg: Story = { - render: () => , + render: () => ( + + ), }; diff --git a/frontend/src/components/VoteStatistics/index.tsx b/frontend/src/components/VoteStatistics/index.tsx index e54db51bb..0cbfcebe3 100644 --- a/frontend/src/components/VoteStatistics/index.tsx +++ b/frontend/src/components/VoteStatistics/index.tsx @@ -5,8 +5,7 @@ import { Size } from '@type/style'; import OneLineGraph from './OneLineGraph'; import * as S from './style'; import TwoLineGraph from './TwoLineGraph'; -import { VoteResultResponse } from './type'; -import { transVoteStatisticsFormat } from './util'; +import { VoteResult } from './type'; interface RadioMode { all: string; @@ -14,7 +13,7 @@ interface RadioMode { } export interface VoteStatisticsProps { - voteResultResponse: VoteResultResponse; + voteResult: VoteResult; size: Size; } @@ -25,13 +24,11 @@ const radioMode: RadioMode = { type RadioCategory = keyof RadioMode; -export default function VoteStatistics({ voteResultResponse, size }: VoteStatisticsProps) { +export default function VoteStatistics({ voteResult, size }: VoteStatisticsProps) { const [currentRadioMode, setCurrentRadioMode] = useState('all'); const radioModeKey = Object.keys(radioMode) as RadioCategory[]; - const voteResult = transVoteStatisticsFormat(voteResultResponse); - const changeMode = (e: MouseEvent) => { const target = e.target as HTMLInputElement; const targetCategory = target.value as RadioCategory; diff --git a/frontend/src/components/common/ErrorMessage/style.ts b/frontend/src/components/common/ErrorMessage/style.ts index fae76a2b0..8116daa16 100644 --- a/frontend/src/components/common/ErrorMessage/style.ts +++ b/frontend/src/components/common/ErrorMessage/style.ts @@ -7,7 +7,7 @@ export const Wrapper = styled.div` flex-direction: column; justify-content: center; align-items: center; - gap: 10px; + gap: 20px; position: relative; `; @@ -26,20 +26,22 @@ export const HeaderWrapper = styled.div` export const Title = styled.h1` width: 90%; - margin-top: 60px; + margin-top: 40px; font-size: 20px; font-weight: bold; text-align: center; + word-break: keep-all; `; export const Description = styled.p` width: 90%; - margin: 20px 0; + margin-bottom: 50px; font: var(--text-body); text-align: center; + word-break: keep-all; `; export const Direction = styled.div` diff --git a/frontend/src/constants/queryKey.ts b/frontend/src/constants/queryKey.ts index 5d4821fa8..0d4991e29 100644 --- a/frontend/src/constants/queryKey.ts +++ b/frontend/src/constants/queryKey.ts @@ -6,4 +6,5 @@ export const QUERY_KEY = { USER_INFO: 'user_info', PASSION_RANKING: 'passion_ranking', POPULAR_RANKING: 'popular_ranking', + VOTE_STATISTICS: 'vote_statistics', }; diff --git a/frontend/src/hooks/query/post/usePostDetail.ts b/frontend/src/hooks/query/post/usePostDetail.ts index 643d1256d..2824693ea 100644 --- a/frontend/src/hooks/query/post/usePostDetail.ts +++ b/frontend/src/hooks/query/post/usePostDetail.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { PostInfo } from '@type/post'; @@ -6,14 +6,30 @@ import { getPost, getPostForGuest } from '@api/post'; import { QUERY_KEY } from '@constants/queryKey'; +import { checkClosedPost } from '@utils/time'; + export const usePostDetail = (isLoggedIn: boolean, postId: number) => { const fetchApi = isLoggedIn ? getPost : getPostForGuest; + const queryClient = useQueryClient(); + const POST_DETAIL_QUERY_KEY = [QUERY_KEY.POST_DETAIL, postId, isLoggedIn]; + const { data, isError, isLoading, error } = useQuery( - [QUERY_KEY.POST_DETAIL, postId, isLoggedIn], + POST_DETAIL_QUERY_KEY, () => fetchApi(postId), { suspense: true, + + onSuccess: data => { + if (checkClosedPost(data.deadline)) { + queryClient.setQueryDefaults(POST_DETAIL_QUERY_KEY, { + cacheTime: 60 * 60 * 1000, + staleTime: 60 * 60 * 1000, + }); + } + + return data; + }, } ); diff --git a/frontend/src/hooks/query/useVoteStatistics.tsx b/frontend/src/hooks/query/useVoteStatistics.tsx new file mode 100644 index 000000000..61eba626b --- /dev/null +++ b/frontend/src/hooks/query/useVoteStatistics.tsx @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getOptionStatistics, getPostStatistics } from '@api/voteResult'; + +import { VoteResult } from '@components/VoteStatistics/type'; + +import { QUERY_KEY } from '@constants/queryKey'; + +export const useVoteStatistics = (postId: number, optionId?: number) => { + const { data } = useQuery( + optionId ? [QUERY_KEY.VOTE_STATISTICS, postId, optionId] : [QUERY_KEY.VOTE_STATISTICS, postId], + () => (optionId ? getOptionStatistics({ postId, optionId }) : getPostStatistics(postId)), + { + cacheTime: 60 * 60 * 1000, + staleTime: 60 * 60 * 1000, + suspense: true, + } + ); + + return { data }; +}; diff --git a/frontend/src/hooks/useFetch.ts b/frontend/src/hooks/useFetch.ts deleted file mode 100644 index fd1d4b872..000000000 --- a/frontend/src/hooks/useFetch.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; - -export const useFetch = (fetchFn: () => Promise) => { - const [data, setData] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - const refetch = useCallback(() => { - setIsLoading(true); - setData(null); - setErrorMessage(null); - - fetchFn() - .then(res => { - setData(res); - }) - .catch(error => { - setErrorMessage(error.message); - }) - .finally(() => { - setIsLoading(false); - }); - }, [fetchFn]); - - useEffect(() => { - refetch(); - }, []); - - return { data, errorMessage, isLoading, refetch }; -}; diff --git a/frontend/src/pages/Ranking/PassionUser/index.tsx b/frontend/src/pages/Ranking/PassionUser/index.tsx index 5545696db..4900bfde6 100644 --- a/frontend/src/pages/Ranking/PassionUser/index.tsx +++ b/frontend/src/pages/Ranking/PassionUser/index.tsx @@ -1,11 +1,5 @@ -import { Suspense } from 'react'; - import { usePassionUserRanking } from '@hooks/query/ranking/usePassionUserRanking'; -import ErrorBoundary from '@pages/ErrorBoundary'; - -import LoadingSpinner from '@components/common/LoadingSpinner'; - import firstRankIcon from '@assets/first-rank.svg'; import secondRankIcon from '@assets/second-rank.svg'; import thirdRankIcon from '@assets/third-rank.svg'; @@ -25,47 +19,43 @@ export default function PassionUserRanking() { const { data: rankerList } = usePassionUserRanking(); return ( - - - {columnNameList.map(text => ( - {text} - ))} - - {rankerList && - new Array(10).fill(0).map((_, index) => { - const ranker = rankerList[index] ?? { - ranking: '', - nickname: '', - postCount: '', - voteCount: '', - score: '', - }; - - const rankIcon = rankIconUrl[ranker.ranking] && ( - {ranker.ranking.toString()} - ); - - return ( - - {rankIcon ?? ranker.ranking} - {ranker.nickname} - {ranker.postCount} - {ranker.voteCount} - {ranker.score} - - ); - })} - - - - - } - > + <> + + + + {columnNameList.map(text => ( + {text} + ))} + + + + {rankerList && + new Array(10).fill(0).map((_, index) => { + const ranker = rankerList[index] ?? { + ranking: '', + nickname: '', + postCount: '', + voteCount: '', + score: '', + }; + + const rankIcon = rankIconUrl[ranker.ranking] && ( + {ranker.ranking.toString()} + ); + + return ( + + {rankIcon ?? ranker.ranking} + {ranker.nickname} + {ranker.postCount} + {ranker.voteCount} + {ranker.score} + + ); + })} - - - + + + ); } diff --git a/frontend/src/pages/Ranking/PassionUser/style.ts b/frontend/src/pages/Ranking/PassionUser/style.ts index 2058d7b3b..b3487356a 100644 --- a/frontend/src/pages/Ranking/PassionUser/style.ts +++ b/frontend/src/pages/Ranking/PassionUser/style.ts @@ -17,6 +17,18 @@ export const Table = styled.table` } `; +export const Tbody = styled.tbody` + & > :nth-child(11) { + margin-top: 20px; + padding: 3px 0; + border-radius: 4px; + + background-color: var(--white); + + font-weight: 500; + } +`; + export const Tr = styled.tr` display: grid; grid-template-columns: 0.5fr 1.5fr 1fr 1fr 1fr; diff --git a/frontend/src/pages/Ranking/PopularPost/index.tsx b/frontend/src/pages/Ranking/PopularPost/index.tsx index 76c95f383..6921ca261 100644 --- a/frontend/src/pages/Ranking/PopularPost/index.tsx +++ b/frontend/src/pages/Ranking/PopularPost/index.tsx @@ -23,28 +23,32 @@ export default function PopularPost() { return ( - - {columnNameList.map(text => ( - {text} - ))} - - {rankingPostList && - rankingPostList.map(rankingPost => { - const rankIcon = rankIconUrl[rankingPost.ranking] && ( - {rankingPost.ranking.toString()} - ); - - return ( - - {rankIcon ?? rankingPost.ranking} - {rankingPost.post.writer} - - {rankingPost.post.title} - - {rankingPost.post.voteCount} - - ); - })} + + + {columnNameList.map(text => ( + {text} + ))} + + + + {rankingPostList && + rankingPostList.map((rankingPost, index) => { + const rankIcon = rankIconUrl[rankingPost.ranking] && ( + {rankingPost.ranking.toString()} + ); + + return ( + + {rankIcon ?? rankingPost.ranking} + {rankingPost.post.writer} + + {rankingPost.post.title} + + {rankingPost.post.voteCount} + + ); + })} + ); } diff --git a/frontend/src/pages/VoteStatisticsPage/OptionStatistics/index.tsx b/frontend/src/pages/VoteStatisticsPage/OptionStatistics/index.tsx index 109d720eb..e47c030fe 100644 --- a/frontend/src/pages/VoteStatisticsPage/OptionStatistics/index.tsx +++ b/frontend/src/pages/VoteStatisticsPage/OptionStatistics/index.tsx @@ -1,17 +1,14 @@ -import { useState } from 'react'; +import { Suspense, useState } from 'react'; import { WrittenVoteOptionType } from '@type/post'; import { Size } from '@type/style'; -import { useFetch } from '@hooks/useFetch'; -import { useToast } from '@hooks/useToast'; - -import { getOptionStatistics } from '@api/voteResult'; +import ErrorBoundary from '@pages/ErrorBoundary'; import LoadingSpinner from '@components/common/LoadingSpinner'; -import Toast from '@components/common/Toast'; import WrittenVoteOption from '@components/optionList/WrittenVoteOptionList/WrittenVoteOption'; -import VoteStatistics from '@components/VoteStatistics'; + +import StatisticsWrapper from '../StatisticsWrapper'; import * as S from './style'; @@ -29,17 +26,8 @@ export default function OptionStatistics({ size, }: OptionStatisticsProps) { const [isStatisticsOpen, setIsStatisticsOpen] = useState(false); - const { isToastOpen, openToast, toastMessage } = useToast(); - - const { - data: voteResult, - errorMessage, - isLoading, - } = useFetch(() => getOptionStatistics({ postId, optionId: voteOption.id })); const toggleOptionStatistics = () => { - if (!voteResult) return openToast('투표 통계 불러오기를 실패했습니다.'); - setIsStatisticsOpen(!isStatisticsOpen); }; @@ -60,26 +48,25 @@ export default function OptionStatistics({ 투표 선택지를 클릭하여 투표 통계를 열어 확인할 수 있습니다. )} - {isStatisticsOpen && voteResult && ( + {isStatisticsOpen && ( <> 투표 선택지를 클릭하여 투표 통계를 닫을 수 있습니다. - + + + + + } + > + + + )} - {isStatisticsOpen && isLoading && ( - - - - )} - {isStatisticsOpen && errorMessage} - {isToastOpen && ( - - {toastMessage} - - )} ); } diff --git a/frontend/src/pages/VoteStatisticsPage/OptionWrapper/index.tsx b/frontend/src/pages/VoteStatisticsPage/OptionWrapper/index.tsx new file mode 100644 index 000000000..2939a4452 --- /dev/null +++ b/frontend/src/pages/VoteStatisticsPage/OptionWrapper/index.tsx @@ -0,0 +1,35 @@ +import { useContext } from 'react'; +import { Navigate } from 'react-router-dom'; + +import { AuthContext } from '@hooks/context/auth'; +import { usePostDetail } from '@hooks/query/post/usePostDetail'; + +import { PATH } from '@constants/path'; + +import { checkWriter } from '@utils/post/checkWriter'; + +import OptionStatistics from '../OptionStatistics'; + +export default function OptionWrapper({ postId }: { postId: number }) { + const { isLoggedIn } = useContext(AuthContext).loggedInfo; + const { data: postDetail } = usePostDetail(isLoggedIn, postId); + + if (!isLoggedIn && postDetail && !checkWriter(postDetail.writer.id)) + return ; + + return ( + postDetail && + postDetail.voteInfo.options.map(option => { + const { postId, voteInfo } = postDetail; + return ( + + ); + }) + ); +} diff --git a/frontend/src/pages/VoteStatisticsPage/StatisticsWrapper/index.tsx b/frontend/src/pages/VoteStatisticsPage/StatisticsWrapper/index.tsx new file mode 100644 index 000000000..b249b28fa --- /dev/null +++ b/frontend/src/pages/VoteStatisticsPage/StatisticsWrapper/index.tsx @@ -0,0 +1,19 @@ +import { Size } from '@type/style'; + +import { useVoteStatistics } from '@hooks/query/useVoteStatistics'; + +import VoteStatistics from '@components/VoteStatistics'; + +export default function StatisticsWrapper({ + size, + postId, + optionId, +}: { + size: Size; + postId: number; + optionId?: number; +}) { + const { data: voteResult } = useVoteStatistics(postId, optionId); + + return voteResult && ; +} diff --git a/frontend/src/pages/VoteStatisticsPage/index.tsx b/frontend/src/pages/VoteStatisticsPage/index.tsx index a541b684a..a4328adf1 100644 --- a/frontend/src/pages/VoteStatisticsPage/index.tsx +++ b/frontend/src/pages/VoteStatisticsPage/index.tsx @@ -1,44 +1,22 @@ -import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import { Suspense } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; -import { useFetch } from '@hooks/useFetch'; +import ErrorBoundary from '@pages/ErrorBoundary'; -import { getPost } from '@api/post'; -import { getPostStatistics } from '@api/voteResult'; - -import ErrorMessage from '@components/common/ErrorMessage'; import IconButton from '@components/common/IconButton'; import Layout from '@components/common/Layout'; +import LoadingSpinner from '@components/common/LoadingSpinner'; import NarrowTemplateHeader from '@components/common/NarrowTemplateHeader'; -import Skeleton from '@components/common/Skeleton'; -import VoteStatistics from '@components/VoteStatistics'; - -import { PATH } from '@constants/path'; -import { checkWriter } from '@utils/post/checkWriter'; - -import OptionStatistics from './OptionStatistics'; +import OptionWrapper from './OptionWrapper'; +import StatisticsWrapper from './StatisticsWrapper'; import * as S from './style'; export default function VoteStatisticsPage() { const params = useParams() as { postId: string }; const postId = Number(params.postId); - const navigate = useNavigate(); - const { - data: postDetail, - errorMessage: postError, - isLoading: isPostLoading, - } = useFetch(() => getPost(postId)); - - const { - data: voteResultResponse, - errorMessage: voteResultError, - isLoading: isVoteResultLoading, - } = useFetch(() => getPostStatistics(postId)); - - if (postDetail && !checkWriter(postDetail.writer.id)) return ; - return ( @@ -48,37 +26,20 @@ export default function VoteStatisticsPage() { 투표 통계 - {postError && } - {isPostLoading && ( - - - - )} - {postDetail && ( - - {voteResultError && } - {isVoteResultLoading && ( - - - - )} - {voteResultResponse && ( - - )} - {postDetail.voteInfo.options.map(option => { - const { postId, voteInfo } = postDetail; - return ( - - ); - })} - - )} + + + + + + } + > + + + + + ); diff --git a/frontend/src/pages/VoteStatisticsPage/style.ts b/frontend/src/pages/VoteStatisticsPage/style.ts index 329cf2fe7..574732f0c 100644 --- a/frontend/src/pages/VoteStatisticsPage/style.ts +++ b/frontend/src/pages/VoteStatisticsPage/style.ts @@ -35,7 +35,7 @@ export const PageHeader = styled.div` font: var(--text-title); `; -export const OptionContainer = styled.div` +export const ContentContainer = styled.div` display: flex; flex-direction: column; align-items: center; From f846ecc0e0651c8dfac8f57ab3e4049746447230 Mon Sep 17 00:00:00 2001 From: Gilpop8663 Date: Sun, 17 Sep 2023 19:35:57 +0900 Subject: [PATCH 40/48] =?UTF-8?q?chore:=20(#631)=20https=EB=A1=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/localhost+1-key.pem | 28 ++++++++++++++++++++++++++++ frontend/localhost+1.pem | 27 +++++++++++++++++++++++++++ frontend/package.json | 1 + frontend/webpack.https.js | 20 ++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 frontend/localhost+1-key.pem create mode 100644 frontend/localhost+1.pem create mode 100644 frontend/webpack.https.js diff --git a/frontend/localhost+1-key.pem b/frontend/localhost+1-key.pem new file mode 100644 index 000000000..85c30afce --- /dev/null +++ b/frontend/localhost+1-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD5CA4PucgLpkJv +X3Ax6/PqsorqpXyTRX4vSCNKDLt4ldtV9MXh3tqxej3JumXXsCo6TyX9T7u38oYn +cUMlBhBKTUfRJ1rmMuVyjnH8mwC7tu3wEAWwcHGYEL8llJtL1LUueN2CtwGPcj3S +CEriIDR2TIeSGv1YQGJsibfMTrlwtZkkc4amEqpty4PhtslYlnKueUk5Jb+MWLR8 +Tyg62h5ThBCjo+RmrOw0KbgZ8fVeyi2Hv/ab7zW5MM1W/3uIP56fT/m0AyKzkPVD +78uTNBa7SyUWnZEZFqvAd9ySW7oyfm9IjjgHKmex62BGFEJJnxEx+lTt+8S6AmsH +j7nNb/dhAgMBAAECggEAB55kd7yyTxDuFWQxFYXnd3Ww5E7dyc+ldaLaWYicL6s+ +A5oyd5+Ox0E7JLHMLdYY5KHJQr+uuYj0673+VOKjmvJUUSTIFiTWejO7bwP6+MM2 +8lI7KlO2VfWlM2wheCdwx5QxCq+4n3/aPlO8nsBnensK5sBmoc2OdU2dTuXVrIoC +SzRGqdwhR4nQeE1RGstS1MjvXgJzrAycDQRKmSKqJPExuY1oM3LMwLk/Ssq71Ta6 +RPa6qi2HHRCCGaC3ML7mn5iH8Z1FFedEY17F8YdKeQiRwPkg7DG/GxqTdfIVp6zb +6+zA5hMpoma/Yq1DZAk3E1uD87317OXNnScEHbevpQKBgQD6QDpR5qdug5LlJtS4 +9HyfMjJFnISdwrfOSst5uAaNw+t4e/2PZrp/R6+JdN7JF/G9fGMP8+dINzhxB6i1 +0CDvNcM/F/1Pu+GnnUeu22O8wonzaTqhQPR/l0oxi5IdIJCGK0phFbNDGDhJ4RIz +J9ILXFM2yXGNvBufUdgxCHg7cwKBgQD+wKfLZMq4s4/TMSSIpvNxKEsUmbdC667u +aaZ1xCpaF2e8MXbdw655PfAHkcqyuzgCRz7Q1bdS8DT86j1CZ4qn8YKSwSaMdzFf +cedm3K8Gz2F16tDiJCVTgXJJy3lac4odnmRLBuhu1q6lV8YrDlDnHjpLTxGZUvi7 +wWIU3od02wKBgEN/wXtH2I9xg9S1RZlhsJ+L/Y1TK14WIFfPStTY80OXvdKwtvLQ +BavBHTLZSCI7iCPRAjNSWWbcJigupa3spoV/HdvNz7CK/9GjFauvV1aZrnXuQzct +ewVTPFrix5V9rB3UXWqUXNbTGJOs1qjfq8MOsw9LBIrzWwv9GnAXVb7LAoGBALOR +MvUwR6CnmSKBSkI0g/ZoM3wC2eWG4VhWCiqKqlu0kb9aknHAw9PcL7v2LQ2M3+7j +L0jezAjfHkqEBcgv2dGFb8dn9HcgcSHY1QZxWcIQHZ+Tc8djORhCbz/K9/Ak7t7v +zbwA04eFNbeARkQFJEu0x9xbkIklrgQSTMNvE8XvAoGAFWQBWypwlyTbcRPmHqJD +SA8wrfuQTc2AdHiJGfssxpQTwlNJgbyq8K4KADZS8gCzPhVOdb+v8inqXtpZ0ke4 +uMj6jbc46/EtW9PuFCP2PIwd+re4P+mqW4rbY4Wo0JxSQSyTFR9wHK1U4+HRbM1o +FY1VVi9c7mjn4Dv7UjaLHU8= +-----END PRIVATE KEY----- diff --git a/frontend/localhost+1.pem b/frontend/localhost+1.pem new file mode 100644 index 000000000..be342d3e6 --- /dev/null +++ b/frontend/localhost+1.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEkTCCAvmgAwIBAgIQZfkIRR7MLlUJFxiuOAIdcDANBgkqhkiG9w0BAQsFADCB +szEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMUQwQgYDVQQLDDtnaW0t +eWVvbmctZ2lsQGdpbS15ZW9uZy1naWwtdWktTWFjQm9va0Fpci5sb2NhbCAo6rmA +7JiB6ri4KTFLMEkGA1UEAwxCbWtjZXJ0IGdpbS15ZW9uZy1naWxAZ2ltLXllb25n +LWdpbC11aS1NYWNCb29rQWlyLmxvY2FsICjquYDsmIHquLgpMB4XDTIzMDkxNzEw +MjI1NloXDTI1MTIxNzEwMjI1NlowbzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3Bt +ZW50IGNlcnRpZmljYXRlMUQwQgYDVQQLDDtnaW0teWVvbmctZ2lsQGdpbS15ZW9u +Zy1naWwtdWktTWFjQm9va0Fpci5sb2NhbCAo6rmA7JiB6ri4KTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAPkIDg+5yAumQm9fcDHr8+qyiuqlfJNFfi9I +I0oMu3iV21X0xeHe2rF6Pcm6ZdewKjpPJf1Pu7fyhidxQyUGEEpNR9EnWuYy5XKO +cfybALu27fAQBbBwcZgQvyWUm0vUtS543YK3AY9yPdIISuIgNHZMh5Ia/VhAYmyJ +t8xOuXC1mSRzhqYSqm3Lg+G2yViWcq55STklv4xYtHxPKDraHlOEEKOj5Gas7DQp +uBnx9V7KLYe/9pvvNbkwzVb/e4g/np9P+bQDIrOQ9UPvy5M0FrtLJRadkRkWq8B3 +3JJbujJ+b0iOOAcqZ7HrYEYUQkmfETH6VO37xLoCawePuc1v92ECAwEAAaNkMGIw +DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaA +FIqR293KF7eY1Y6Lr9m0lFJ/H4ydMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAA +ATANBgkqhkiG9w0BAQsFAAOCAYEAs8vZ79vnZ0PGTPibslQgNdyC7Hp2K0QcQABq +OBY25D6my4jMPEouP+eIF7ByXJ7YqsQCi5NNPAVMg0Q42eCGz0pVELOxWLGUwhLC +u/Xl8PgzcGrb+qakqwVD2YZHcsOLq+O67p1zc29MvSR7itMxWh9+FzmvBcYfWqfv +LJrtiOt9FS5QBTDGOXvQmsnt7AGrwN28IRrKNr00rVodTavmuonrBW80z5IAzVDt +n73Ljb6Tysc69eXUwQ+odMzM9N+vdVsNQBmJSKljFdaXC4Q+Z0ZZyr9NIuC1rhVT +z4sVAaYcbNm2Ue5jV9G3F0HmpxuHfBoJQ/SuSoHe3FMmCYJxjDFU0vvMHDUkEz0Y +DqYJq2BhZn1pfSQPmoz0vy2fVxjNL9PZP5X2wkscg+U6zgf8xJINpH2rqvpk/488 +YMRjV4dPW6QM9PD1nj2B7lvL5sDhKR84W4ikDZdR14clg2OIcfQWMtKLG05EBYMC +/P2rwbx1KWEgss24Pm17lHl7RXsv +-----END CERTIFICATE----- diff --git a/frontend/package.json b/frontend/package.json index 1a197661b..21dd0b606 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "license": "ISC", "scripts": { "dev": "webpack-dev-server --config webpack.dev.js --open --hot", + "https-dev": "webpack-dev-server --config webpack.https.js --open --hot", "prebuild": "cd src/utils && node generateSiteMap.js", "build": "webpack --config webpack.prod.js", "start": "webpack --config webpack.dev.js", diff --git a/frontend/webpack.https.js b/frontend/webpack.https.js new file mode 100644 index 000000000..2b69bb7b6 --- /dev/null +++ b/frontend/webpack.https.js @@ -0,0 +1,20 @@ +const { merge } = require('webpack-merge'); + +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'development', // 현재 개발 모드 + devtool: 'eval', // 최대성능, 개발환경에 추천 + devServer: { + historyApiFallback: true, + port: 3000, + hot: true, + server: { + type: 'https', + options: { + key: './localhost+1-key.pem', + cert: './localhost+1.pem', + }, + }, + }, +}); From c0ea5c4071a802e239d16419e82c8ab5997637e9 Mon Sep 17 00:00:00 2001 From: Gilpop8663 Date: Sun, 17 Sep 2023 23:04:46 +0900 Subject: [PATCH 41/48] =?UTF-8?q?chore:=20(#631)=20pem=ED=82=A4=20gitignor?= =?UTF-8?q?e=EC=97=90=20=EC=B6=94=EA=B0=80=20=EB=B0=9C=EA=B8=89=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EA=B8=B0=EA=B8=B0=EC=97=90=EC=84=9C=EB=A7=8C=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=ED=95=9C=20pem=ED=82=A4=EC=9D=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=95=8C=EB=AC=B8=EC=97=90=20git=EC=97=90=20=EC=98=AC=EB=A6=B4?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=EA=B0=80=20=EC=97=86=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 2 ++ frontend/localhost+1-key.pem | 28 ---------------------------- frontend/localhost+1.pem | 27 --------------------------- frontend/webpack.https.js | 4 ++-- 4 files changed, 4 insertions(+), 57 deletions(-) delete mode 100644 frontend/localhost+1-key.pem delete mode 100644 frontend/localhost+1.pem diff --git a/frontend/.gitignore b/frontend/.gitignore index 1a4f00cb9..2a5ab3165 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,3 +1,5 @@ /node_modules .env /dist +localhost.pem +localhost-key.pem diff --git a/frontend/localhost+1-key.pem b/frontend/localhost+1-key.pem deleted file mode 100644 index 85c30afce..000000000 --- a/frontend/localhost+1-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD5CA4PucgLpkJv -X3Ax6/PqsorqpXyTRX4vSCNKDLt4ldtV9MXh3tqxej3JumXXsCo6TyX9T7u38oYn -cUMlBhBKTUfRJ1rmMuVyjnH8mwC7tu3wEAWwcHGYEL8llJtL1LUueN2CtwGPcj3S -CEriIDR2TIeSGv1YQGJsibfMTrlwtZkkc4amEqpty4PhtslYlnKueUk5Jb+MWLR8 -Tyg62h5ThBCjo+RmrOw0KbgZ8fVeyi2Hv/ab7zW5MM1W/3uIP56fT/m0AyKzkPVD -78uTNBa7SyUWnZEZFqvAd9ySW7oyfm9IjjgHKmex62BGFEJJnxEx+lTt+8S6AmsH -j7nNb/dhAgMBAAECggEAB55kd7yyTxDuFWQxFYXnd3Ww5E7dyc+ldaLaWYicL6s+ -A5oyd5+Ox0E7JLHMLdYY5KHJQr+uuYj0673+VOKjmvJUUSTIFiTWejO7bwP6+MM2 -8lI7KlO2VfWlM2wheCdwx5QxCq+4n3/aPlO8nsBnensK5sBmoc2OdU2dTuXVrIoC -SzRGqdwhR4nQeE1RGstS1MjvXgJzrAycDQRKmSKqJPExuY1oM3LMwLk/Ssq71Ta6 -RPa6qi2HHRCCGaC3ML7mn5iH8Z1FFedEY17F8YdKeQiRwPkg7DG/GxqTdfIVp6zb -6+zA5hMpoma/Yq1DZAk3E1uD87317OXNnScEHbevpQKBgQD6QDpR5qdug5LlJtS4 -9HyfMjJFnISdwrfOSst5uAaNw+t4e/2PZrp/R6+JdN7JF/G9fGMP8+dINzhxB6i1 -0CDvNcM/F/1Pu+GnnUeu22O8wonzaTqhQPR/l0oxi5IdIJCGK0phFbNDGDhJ4RIz -J9ILXFM2yXGNvBufUdgxCHg7cwKBgQD+wKfLZMq4s4/TMSSIpvNxKEsUmbdC667u -aaZ1xCpaF2e8MXbdw655PfAHkcqyuzgCRz7Q1bdS8DT86j1CZ4qn8YKSwSaMdzFf -cedm3K8Gz2F16tDiJCVTgXJJy3lac4odnmRLBuhu1q6lV8YrDlDnHjpLTxGZUvi7 -wWIU3od02wKBgEN/wXtH2I9xg9S1RZlhsJ+L/Y1TK14WIFfPStTY80OXvdKwtvLQ -BavBHTLZSCI7iCPRAjNSWWbcJigupa3spoV/HdvNz7CK/9GjFauvV1aZrnXuQzct -ewVTPFrix5V9rB3UXWqUXNbTGJOs1qjfq8MOsw9LBIrzWwv9GnAXVb7LAoGBALOR -MvUwR6CnmSKBSkI0g/ZoM3wC2eWG4VhWCiqKqlu0kb9aknHAw9PcL7v2LQ2M3+7j -L0jezAjfHkqEBcgv2dGFb8dn9HcgcSHY1QZxWcIQHZ+Tc8djORhCbz/K9/Ak7t7v -zbwA04eFNbeARkQFJEu0x9xbkIklrgQSTMNvE8XvAoGAFWQBWypwlyTbcRPmHqJD -SA8wrfuQTc2AdHiJGfssxpQTwlNJgbyq8K4KADZS8gCzPhVOdb+v8inqXtpZ0ke4 -uMj6jbc46/EtW9PuFCP2PIwd+re4P+mqW4rbY4Wo0JxSQSyTFR9wHK1U4+HRbM1o -FY1VVi9c7mjn4Dv7UjaLHU8= ------END PRIVATE KEY----- diff --git a/frontend/localhost+1.pem b/frontend/localhost+1.pem deleted file mode 100644 index be342d3e6..000000000 --- a/frontend/localhost+1.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEkTCCAvmgAwIBAgIQZfkIRR7MLlUJFxiuOAIdcDANBgkqhkiG9w0BAQsFADCB -szEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMUQwQgYDVQQLDDtnaW0t -eWVvbmctZ2lsQGdpbS15ZW9uZy1naWwtdWktTWFjQm9va0Fpci5sb2NhbCAo6rmA -7JiB6ri4KTFLMEkGA1UEAwxCbWtjZXJ0IGdpbS15ZW9uZy1naWxAZ2ltLXllb25n -LWdpbC11aS1NYWNCb29rQWlyLmxvY2FsICjquYDsmIHquLgpMB4XDTIzMDkxNzEw -MjI1NloXDTI1MTIxNzEwMjI1NlowbzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3Bt -ZW50IGNlcnRpZmljYXRlMUQwQgYDVQQLDDtnaW0teWVvbmctZ2lsQGdpbS15ZW9u -Zy1naWwtdWktTWFjQm9va0Fpci5sb2NhbCAo6rmA7JiB6ri4KTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAPkIDg+5yAumQm9fcDHr8+qyiuqlfJNFfi9I -I0oMu3iV21X0xeHe2rF6Pcm6ZdewKjpPJf1Pu7fyhidxQyUGEEpNR9EnWuYy5XKO -cfybALu27fAQBbBwcZgQvyWUm0vUtS543YK3AY9yPdIISuIgNHZMh5Ia/VhAYmyJ -t8xOuXC1mSRzhqYSqm3Lg+G2yViWcq55STklv4xYtHxPKDraHlOEEKOj5Gas7DQp -uBnx9V7KLYe/9pvvNbkwzVb/e4g/np9P+bQDIrOQ9UPvy5M0FrtLJRadkRkWq8B3 -3JJbujJ+b0iOOAcqZ7HrYEYUQkmfETH6VO37xLoCawePuc1v92ECAwEAAaNkMGIw -DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaA -FIqR293KF7eY1Y6Lr9m0lFJ/H4ydMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAA -ATANBgkqhkiG9w0BAQsFAAOCAYEAs8vZ79vnZ0PGTPibslQgNdyC7Hp2K0QcQABq -OBY25D6my4jMPEouP+eIF7ByXJ7YqsQCi5NNPAVMg0Q42eCGz0pVELOxWLGUwhLC -u/Xl8PgzcGrb+qakqwVD2YZHcsOLq+O67p1zc29MvSR7itMxWh9+FzmvBcYfWqfv -LJrtiOt9FS5QBTDGOXvQmsnt7AGrwN28IRrKNr00rVodTavmuonrBW80z5IAzVDt -n73Ljb6Tysc69eXUwQ+odMzM9N+vdVsNQBmJSKljFdaXC4Q+Z0ZZyr9NIuC1rhVT -z4sVAaYcbNm2Ue5jV9G3F0HmpxuHfBoJQ/SuSoHe3FMmCYJxjDFU0vvMHDUkEz0Y -DqYJq2BhZn1pfSQPmoz0vy2fVxjNL9PZP5X2wkscg+U6zgf8xJINpH2rqvpk/488 -YMRjV4dPW6QM9PD1nj2B7lvL5sDhKR84W4ikDZdR14clg2OIcfQWMtKLG05EBYMC -/P2rwbx1KWEgss24Pm17lHl7RXsv ------END CERTIFICATE----- diff --git a/frontend/webpack.https.js b/frontend/webpack.https.js index 2b69bb7b6..bca1f97b0 100644 --- a/frontend/webpack.https.js +++ b/frontend/webpack.https.js @@ -12,8 +12,8 @@ module.exports = merge(common, { server: { type: 'https', options: { - key: './localhost+1-key.pem', - cert: './localhost+1.pem', + key: './localhost-key.pem', + cert: './localhost.pem', }, }, }, From 6a4cb613aa9a71355b43bd00677d6b1f46112cae Mon Sep 17 00:00:00 2001 From: jero_kang <81199414+inyeong-kang@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:52:40 +0900 Subject: [PATCH 42/48] =?UTF-8?q?feat:=20(#563)=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#6?= =?UTF-8?q?33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../category/useCategoryFavoriteToggle.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/query/category/useCategoryFavoriteToggle.ts b/frontend/src/hooks/query/category/useCategoryFavoriteToggle.ts index 13fa830ca..5c238285f 100644 --- a/frontend/src/hooks/query/category/useCategoryFavoriteToggle.ts +++ b/frontend/src/hooks/query/category/useCategoryFavoriteToggle.ts @@ -8,16 +8,36 @@ import { QUERY_KEY } from '@constants/queryKey'; export const useCategoryFavoriteToggle = () => { const queryClient = useQueryClient(); + const LOGGED_IN = true; + const queryKey = [QUERY_KEY.CATEGORIES, LOGGED_IN]; + const { mutate, isLoading, isError, error } = useMutation( ({ id, isFavorite }: Omit) => isFavorite ? removeFavoriteCategory(id) : addFavoriteCategory(id), { - onSuccess: () => { - queryClient.invalidateQueries([QUERY_KEY.CATEGORIES]); + onMutate: async ({ id }: Omit) => { + const oldCategoryList: Category[] | undefined = queryClient.getQueryData(queryKey); + + if (oldCategoryList) { + await queryClient.cancelQueries(queryKey); + const updatedCategoryList = oldCategoryList.map(item => + item.id === id ? { ...item, isFavorite: !item.isFavorite } : item + ); + queryClient.setQueryData(queryKey, updatedCategoryList); + + return () => queryClient.setQueryData(queryKey, oldCategoryList); + } }, - onError: error => { + onError: (error, _, rollback) => { + if (rollback) { + rollback(); + return; + } window.console.log('Category favorite toggle error', error); }, + onSettled: () => { + queryClient.invalidateQueries(queryKey); + }, } ); From f1d8219b3775c4fac54562539c820355e7a6a6e5 Mon Sep 17 00:00:00 2001 From: chsua Date: Mon, 11 Sep 2023 12:48:44 +0900 Subject: [PATCH 43/48] =?UTF-8?q?feat:=20(#541)=20imageUrl=20=EC=95=9E=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존: 받은 imageUrl에 http-를 붙여야 image가 보여짐. 반대로 http-를 떼서 수정 api를 보내야 함 - 수정: 서버에서 보내주는 url 그대로 받아서 image를 보이고 수정 api를 보냄 --- frontend/src/components/PostForm/index.tsx | 1 - frontend/src/components/common/Post/index.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/components/PostForm/index.tsx b/frontend/src/components/PostForm/index.tsx index d337e724e..66eb3af10 100644 --- a/frontend/src/components/PostForm/index.tsx +++ b/frontend/src/components/PostForm/index.tsx @@ -65,7 +65,6 @@ export default function PostForm({ data, mutate }: PostFormProps) { const navigate = useNavigate(); const contentImageHook = useContentImage(serverImageUrl); const { handlePasteImage } = contentImageHook; - const writingOptionHook = useWritingOption( serverVoteInfo?.options.map(option => ({ ...option, diff --git a/frontend/src/components/common/Post/index.tsx b/frontend/src/components/common/Post/index.tsx index e1df53df6..52cc2260e 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'; From 0989a3823bac52f87e3ec209f17f32b6970e8489 Mon Sep 17 00:00:00 2001 From: chsua Date: Tue, 19 Sep 2023 13:53:25 +0900 Subject: [PATCH 44/48] =?UTF-8?q?refactor:=20(#541)=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C(=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=98=20=ED=81=B4=EB=A6=AD=EC=9D=B4=EB=B2=A4=ED=8A=B8)=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostForm/ContentImageSection/index.tsx | 14 ++------------ .../OptionUploadImageButton/index.tsx | 9 ++------- 2 files changed, 4 insertions(+), 19 deletions(-) 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 )} { - + 본문에 사진 넣기 ) => { - e.preventDefault(); - contentInputRefList.current[index].click(); - }; - return ( -