From 9e1cb8f8bafa65cce8dfc3ef8124c3fee2d3ddbc Mon Sep 17 00:00:00 2001 From: dohpark Date: Sat, 10 Dec 2022 20:45:24 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20loadingDelay=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Forum/index.tsx | 3 +-- client/src/pages/Manage/index.tsx | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/pages/Forum/index.tsx b/client/src/pages/Forum/index.tsx index ca9ffbf..768d8de 100644 --- a/client/src/pages/Forum/index.tsx +++ b/client/src/pages/Forum/index.tsx @@ -35,7 +35,6 @@ function Forum() { const [category, setCategory] = useState("전체"); const [orderBy, setOrderBy] = useState("latestAsc"); const [page, setPage] = useState(1); - // const [loadingDelay, setLoadingDelay] = useState(false); const fetchFormList = (): Promise => boardApi.getFormList({ title: keyword, category, orderBy, page }); const { data, isLoading, isSuccess, isError } = useQuery({ @@ -176,7 +175,7 @@ function Forum() { )) : null} - {isSuccess && !data.form.length ? : null} + {!loadingDelay && isSuccess && !data.form.length ? : null} ); diff --git a/client/src/pages/Manage/index.tsx b/client/src/pages/Manage/index.tsx index 7e23c95..bcbf3fc 100644 --- a/client/src/pages/Manage/index.tsx +++ b/client/src/pages/Manage/index.tsx @@ -157,7 +157,9 @@ function Manage() { ) : null}
- {isSuccess && !data.pages[0].form.length ? : null} + {!loadingDelay && isSuccess && !data.pages[0].form.length ? ( + + ) : null} {isLoading || loadingDelay || isError ? Array.from({ length: 3 }, (_, index) => index).map((value) => ( From e5fb49f6815156ed4ecfb6e695b4d7ca120a2fad Mon Sep 17 00:00:00 2001 From: dohpark Date: Sun, 11 Dec 2022 17:47:55 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=EC=84=A4=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1/=EC=9D=91=EB=8B=B5/=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=20=EC=8A=A4=EC=BC=88?= =?UTF-8?q?=EB=A0=88=ED=86=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Skeleton/index.tsx | 3 +- .../src/components/common/Skeleton/style.ts | 42 ++- client/src/components/common/Skeleton/type.ts | 10 + client/src/pages/Edit/index.tsx | 272 ++++++++++-------- client/src/pages/Edit/style.ts | 6 + client/src/pages/Forum/index.tsx | 7 +- client/src/pages/Manage/index.tsx | 7 +- client/src/pages/Result/index.tsx | 83 ++++-- client/src/pages/Result/style.ts | 19 +- client/src/pages/View/index.tsx | 120 +++++--- client/src/pages/View/style.ts | 7 + 11 files changed, 393 insertions(+), 183 deletions(-) create mode 100644 client/src/components/common/Skeleton/type.ts diff --git a/client/src/components/common/Skeleton/index.tsx b/client/src/components/common/Skeleton/index.tsx index b2703ce..6a1657d 100644 --- a/client/src/components/common/Skeleton/index.tsx +++ b/client/src/components/common/Skeleton/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import * as S from "./style"; +import SkeletonType from "./type"; function SkeletonContainer({ children, custom = "" }: { children: React.ReactNode; custom?: string }) { return {children}; @@ -8,7 +9,7 @@ SkeletonContainer.defaultProps = { custom: "", }; -function Element({ type }: { type: string }) { +function Element({ type }: { type: SkeletonType }) { return ; } diff --git a/client/src/components/common/Skeleton/style.ts b/client/src/components/common/Skeleton/style.ts index 6f6e730..c61ed13 100644 --- a/client/src/components/common/Skeleton/style.ts +++ b/client/src/components/common/Skeleton/style.ts @@ -1,11 +1,13 @@ import styled, { css } from "styled-components"; +import SkeletonType from "./type"; -const getSkeletonTypeCss = (type: string) => { +const getSkeletonTypeCss = (type: SkeletonType) => { switch (type) { case "text": return css` width: 100%; height: 12px; + margin: 10px 0; `; case "title": @@ -13,6 +15,41 @@ const getSkeletonTypeCss = (type: string) => { width: 50%; height: 20px; margin-bottom: 15px; + margin-top: 10px; + `; + + case "formTitle": + return css` + width: 33%; + height: 27px; + padding: 5px 0; + margin: 10px 0 20px; + `; + + case "formCategoryBox": + return css` + width: 150px; + height: 38px; + `; + + case "formQuestionTitleEdit": + return css` + width: 33%; + height: 16px; + margin: 30px 0 20px; + `; + + case "formQuestionTitle": + return css` + width: 33%; + height: 16px; + margin-bottom: 20px; + `; + + case "button": + return css` + width: 55px; + height: 30px; `; default: @@ -31,9 +68,8 @@ const Container = styled.div<{ custom: string }>` ${({ custom }) => custom} `; -const Element = styled.div<{ type: string }>` +const Element = styled.div<{ type: SkeletonType }>` background-color: #ddd; - margin: 10px 0; border-radius: 3px; ${({ type }) => getSkeletonTypeCss(type)} diff --git a/client/src/components/common/Skeleton/type.ts b/client/src/components/common/Skeleton/type.ts new file mode 100644 index 0000000..5658eed --- /dev/null +++ b/client/src/components/common/Skeleton/type.ts @@ -0,0 +1,10 @@ +type SkeletonType = + | "text" + | "title" + | "formTitle" + | "formCategoryBox" + | "formQuestionTitle" + | "button" + | "formQuestionTitleEdit"; + +export default SkeletonType; diff --git a/client/src/pages/Edit/index.tsx b/client/src/pages/Edit/index.tsx index fb874e6..692f88a 100644 --- a/client/src/pages/Edit/index.tsx +++ b/client/src/pages/Edit/index.tsx @@ -12,7 +12,7 @@ import Icon from "components/common/Icon"; import ToggleButton from "components/common/ToggleButton"; import QuestionRead from "components/Edit/QuestionRead"; import TextDropdown from "components/common/Dropdown/TextDropdown"; - +import Skeleton from "components/common/Skeleton"; import ShareFormModal from "components/Modal/ShareFormModal"; import Button from "components/common/Button"; import IconButton from "components/common/IconButton"; @@ -23,6 +23,7 @@ import formApi from "api/formApi"; import { fromApiToForm, fromFormToApi } from "utils/form"; import useModal from "hooks/useModal"; import { CATEGORY_LIST, QUESTION_TYPE_LIST } from "store/form"; +import useLoadingDelay from "hooks/useLoadingDelay"; import * as S from "./style"; const initialState: FormState = { @@ -45,7 +46,7 @@ function Edit() { const { id } = useParams(); const fetchForm = (): Promise => formApi.getForm(id); - const { data, isSuccess } = useQuery({ queryKey: [id], queryFn: fetchForm }); + const { data, isSuccess, isLoading, isError } = useQuery({ queryKey: [id], queryFn: fetchForm }); const [state, dispatch] = useReducer(writeReducer, initialState); const { form, question } = state; @@ -54,6 +55,7 @@ function Edit() { const [drag, setDrag] = useState(""); const { openModal, closeModal, ModalPortal } = useModal(); + const delayLoading = useLoadingDelay(isLoading); useEffect(() => { if (!id) return; @@ -139,7 +141,7 @@ function Edit() { }; const onClickCopyLink = () => { - navigator.clipboard.writeText(`${process.env.REACT_APP_CLIENT_ORIGIN_URL}/forms/${id}/view`); + window.navigator.clipboard.writeText(`${process.env.REACT_APP_CLIENT_ORIGIN_URL}/forms/${id}/view`); }; const onClickSaveForm = () => { @@ -171,22 +173,20 @@ function Edit() { return false; }; + const checkApiSuccess = () => { + if (!delayLoading && isSuccess) return true; + return false; + }; + const checkApiLoadingOrError = () => { + if (isLoading || delayLoading || isError) return true; + return false; + }; + return ( onClickTitle()}> - {focus !== "title" && ( - <> - {form.title} - - {form.description ? form.description : "설문지 설명"} - - - {form.category || "카테고리"} - - - )} - {focus === "title" && ( + {checkApiSuccess() && ( <> @@ -200,124 +200,154 @@ function Edit() { )} + {checkApiLoadingOrError() ? ( + <> + + + + + + ) : null} {(droppable) => (
- {question.map(({ questionId, title, type, essential }, questionIndex) => ( - - {(draggable) => { - let transform = draggable.draggableProps.style?.transform; - - if (transform) { - transform = transform.replace(/([0-9]+px)/, "0px"); - draggable.draggableProps.style = { - ...draggable.draggableProps.style, - transform, - }; - } - - return ( - onClickQuestion(questionIndex)} - onMouseOver={() => onMouseOverQuestion(questionIndex)} - onMouseOut={() => onMouseOutQuestion()} - {...draggable.draggableProps} - ref={draggable.innerRef} - > - - {showDragIndicator(questionIndex) ? : null} - - {focus === `q${questionIndex}` && ( - <> - - onInputQuestionTitle(e.currentTarget.value, questionIndex)} - value={question[questionIndex].title} - placeholder="질문" - /> - { - const isQuestionType = (str: string): str is QuestionType => - str === "checkbox" || str === "multiple" || str === "paragraph"; - - if (isQuestionType(questionType)) - onClickSetQuestionType(questionType, questionIndex); - }} - items={QUESTION_TYPE_LIST} - defaultValue="선택해주세요" - /> - - - - - - - onClickAddQuestion(questionIndex)} - icon="add" - size="21px" - custom="margin-right: 12px;" - /> - onClickCopyQuestion(questionIndex)} - icon="copy" - size="18px" - custom="margin-right: 12px;" - /> - onClickDeleteQuestion(questionIndex)} - icon="trashcan" - size="18px" - custom="margin-right: 12px;" - /> - - 필수 - onClickChangeQuestionEssential(questionIndex)} + {checkApiSuccess() && + question.map(({ questionId, title, type, essential }, questionIndex) => ( + + {(draggable) => { + let transform = draggable.draggableProps.style?.transform; + + if (transform) { + transform = transform.replace(/([0-9]+px)/, "0px"); + draggable.draggableProps.style = { + ...draggable.draggableProps.style, + transform, + }; + } + + return ( + onClickQuestion(questionIndex)} + onMouseOver={() => onMouseOverQuestion(questionIndex)} + onMouseOut={() => onMouseOutQuestion()} + {...draggable.draggableProps} + ref={draggable.innerRef} + > + + {showDragIndicator(questionIndex) ? : null} + + {focus === `q${questionIndex}` && ( + <> + + onInputQuestionTitle(e.currentTarget.value, questionIndex)} + value={question[questionIndex].title} + placeholder="질문" /> - - - - )} - {focus !== `q${questionIndex}` && ( - <> -
{title}
- - - )} -
- ); - }} -
- ))} + { + const isQuestionType = (str: string): str is QuestionType => + str === "checkbox" || str === "multiple" || str === "paragraph"; + + if (isQuestionType(questionType)) + onClickSetQuestionType(questionType, questionIndex); + }} + items={QUESTION_TYPE_LIST} + defaultValue="선택해주세요" + /> + + + + + + + onClickAddQuestion(questionIndex)} + icon="add" + size="21px" + custom="margin-right: 12px;" + /> + onClickCopyQuestion(questionIndex)} + icon="copy" + size="18px" + custom="margin-right: 12px;" + /> + onClickDeleteQuestion(questionIndex)} + icon="trashcan" + size="18px" + custom="margin-right: 12px;" + /> + + 필수 + onClickChangeQuestionEssential(questionIndex)} + /> + + + + )} + {focus !== `q${questionIndex}` && ( + <> +
{title}
+ + + )} + + ); + }} + + ))} {droppable.placeholder}
)}
+ + {checkApiLoadingOrError() + ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( + + + + + + + + + )) + : null} - + {checkApiSuccess() && ( + + )} + {checkApiLoadingOrError() ? ( + <> + + + + ) : null}
diff --git a/client/src/pages/Edit/style.ts b/client/src/pages/Edit/style.ts index 1e2bbb8..8778542 100644 --- a/client/src/pages/Edit/style.ts +++ b/client/src/pages/Edit/style.ts @@ -9,6 +9,8 @@ const TitleContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 20px; + position: relative; + overflow: hidden; `; const TitleInput = styled.input` @@ -75,6 +77,8 @@ const QuestionContainer = styled.div` border-radius: 3px; padding: 0 20px 20px; border: solid 1px ${({ theme }) => theme.colors.grey3}; + position: relative; + overflow: hidden; `; const QuestionHead = styled.div` @@ -158,6 +162,8 @@ const BottomContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 20px; + position: relative; + overflow: hidden; `; const DragIndicator = styled.div` diff --git a/client/src/pages/Forum/index.tsx b/client/src/pages/Forum/index.tsx index 768d8de..2c7538e 100644 --- a/client/src/pages/Forum/index.tsx +++ b/client/src/pages/Forum/index.tsx @@ -44,6 +44,11 @@ function Forum() { const loadingDelay = useLoadingDelay(isLoading); + const checkApiLoadingOrError = () => { + if (isLoading || loadingDelay || isError) return true; + return false; + }; + return ( @@ -163,7 +168,7 @@ function Forum() { ) : null} - {isLoading || isError || loadingDelay + {checkApiLoadingOrError() ? Array.from({ length: 3 }, (_, index) => index).map((value) => ( diff --git a/client/src/pages/Manage/index.tsx b/client/src/pages/Manage/index.tsx index bcbf3fc..23c301f 100644 --- a/client/src/pages/Manage/index.tsx +++ b/client/src/pages/Manage/index.tsx @@ -68,6 +68,11 @@ function Manage() { openModal(); }; + const checkApiLoadingOrError = () => { + if (isLoading || loadingDelay || isError) return true; + return false; + }; + return ( @@ -160,7 +165,7 @@ function Manage() { {!loadingDelay && isSuccess && !data.pages[0].form.length ? ( ) : null} - {isLoading || loadingDelay || isError + {checkApiLoadingOrError() ? Array.from({ length: 3 }, (_, index) => index).map((value) => ( diff --git a/client/src/pages/Result/index.tsx b/client/src/pages/Result/index.tsx index fda7287..ce8c560 100644 --- a/client/src/pages/Result/index.tsx +++ b/client/src/pages/Result/index.tsx @@ -5,16 +5,19 @@ import { useQuery } from "@tanstack/react-query"; import resultApi from "api/resultApi"; import { ResultApi, QuestionSummary } from "types/result"; import QuestionResult from "components/Result/QuestionResult"; +import Skeleton from "components/common/Skeleton"; +import useLoadingDelay from "hooks/useLoadingDelay"; import * as S from "./style"; function Result() { const { id } = useParams(); const fetchForm = (): Promise => resultApi.getResult(id); - const { data, isSuccess } = useQuery({ queryKey: [id, "result"], queryFn: fetchForm }); + const { data, isSuccess, isLoading, isError } = useQuery({ queryKey: [id, "result"], queryFn: fetchForm }); const [formResult, setFormResult] = useState(); const [questionResult, setQuestionResult] = useState([]); + const delayLoading = useLoadingDelay(isLoading); useEffect(() => { if (!id) return; @@ -24,30 +27,70 @@ function Result() { } }, [id, isSuccess, data]); + const checkApiSuccess = () => { + if (!delayLoading && isSuccess) return true; + return false; + }; + const checkApiLoadingOrError = () => { + if (isLoading || delayLoading || isError) return true; + return false; + }; + return ( - {formResult?.formTitle} - 응답 {formResult?.totalResponseCount}개 + {checkApiSuccess() && ( + <> + {formResult?.formTitle} + 응답 {formResult?.totalResponseCount}개 + + )} + {checkApiLoadingOrError() ? ( + <> + + + + + + ) : null} - {questionResult.map(({ type, questionTitle, responseCount, answerTotal }) => ( - -
- {questionTitle} -
- {responseCount ? ( - - 응답 {responseCount}개 - - ) : null} - {responseCount ? ( - - ) : ( - 질문에 대한 응답이 없습니다. - )} -
- ))} + {checkApiSuccess() && + (questionResult.length ? ( + questionResult.map(({ type, questionTitle, responseCount, answerTotal }) => ( + +
+ {questionTitle} +
+ {responseCount ? ( + + 응답 {responseCount}개 + + ) : null} + {responseCount ? ( + + ) : ( + 질문에 대한 응답이 없습니다. + )} +
+ )) + ) : ( + + 설문지에 대한 응답이 없습니다. + + ))} + {checkApiLoadingOrError() + ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( + + + + + + + + + )) + : null}
); diff --git a/client/src/pages/Result/style.ts b/client/src/pages/Result/style.ts index 67ece0c..76d2b4d 100644 --- a/client/src/pages/Result/style.ts +++ b/client/src/pages/Result/style.ts @@ -9,12 +9,14 @@ const HeadContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 10px 20px; + position: relative; + overflow: hidden; `; const HeadTitle = styled.div` width: 100%; display: block; - font-size: 32px; + font-size: 28px; padding: 5px 0; border: none; font-family: Arial, Helvetica, sans-serif; @@ -33,7 +35,7 @@ const ToggleText = styled.span` const OverallResponseCount = styled.div` margin-top: 8px; margin-bottom: 8px; - font-size: 20px; + font-size: 16px; `; const QuestionContainer = styled.div` @@ -42,6 +44,9 @@ const QuestionContainer = styled.div` border-radius: 3px; padding: 20px; + position: relative; + overflow: hidden; + &:last-child { margin-bottom: 24px; } @@ -54,7 +59,12 @@ const QuestionResponseCount = styled.div` font-weight: 400; `; -const NoResponse = styled.div` +const NoResponseForm = styled.div` + font-size: 14px; + font-weight: 400; +`; + +const NoResponseQuestion = styled.div` margin-top: 24px; font-size: 14px; font-weight: 400; @@ -69,5 +79,6 @@ export { OverallResponseCount, QuestionContainer, QuestionResponseCount, - NoResponse, + NoResponseForm, + NoResponseQuestion, }; diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index a439bc4..0185e37 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -9,8 +9,10 @@ import { checkPrevResponseUpdateValidateCheckList, fromApiToValidateCheckList, v import FormLayout from "components/template/Layout"; import QuestionView from "components/View/QuestionView"; import Button from "components/common/Button"; +import Skeleton from "components/common/Skeleton"; import theme from "styles/theme"; import responseApi from "api/responseApi"; +import useLoadingDelay from "hooks/useLoadingDelay"; import { ResponseElement, Validation } from "types/response"; import * as S from "./style"; @@ -36,14 +38,26 @@ function View() { const { state: prevResponseId } = useLocation(); const fetchForm = (): Promise => formApi.getForm(id); - const { data: formData, isSuccess: formIsSuccess } = useQuery({ queryKey: [id, "form"], queryFn: fetchForm }); + const { + data: formData, + isSuccess: formIsSuccess, + isLoading: formIsLoading, + isError: formIsError, + } = useQuery({ queryKey: [id, "form"], queryFn: fetchForm }); const fetchResponse = (): Promise => responseApi.getResponse(id, prevResponseId); - const { data: responseData, isSuccess: responseIsSuccess } = useQuery({ + const { + data: responseData, + isSuccess: responseIsSuccess, + isLoading: responseIsLoading, + isError: resposneIsError, + } = useQuery({ queryKey: [prevResponseId, "response"], queryFn: fetchResponse, }); + const loadingDelay = useLoadingDelay(formIsLoading || responseIsLoading); + const [state, setState] = useState(initialState); const { form, question } = state; const [responseState, dispatch] = useReducer(formViewReducer, []); @@ -63,7 +77,6 @@ function View() { useEffect(() => { if (!id) return; if (formIsSuccess) { - console.log(formData); setState(fromApiToForm(formData)); const checkList = fromApiToValidateCheckList(formData); setValidation(checkList); @@ -87,41 +100,84 @@ function View() { } }; + const checkApiSuccess = () => { + if (!loadingDelay && formIsSuccess && responseIsSuccess) return true; + return false; + }; + const checkApiLoadingOrError = () => { + if (formIsLoading || responseIsLoading || loadingDelay || formIsError || resposneIsError) return true; + return false; + }; + return ( - {form.title} - {form.description ? {form.description} : null} + {checkApiSuccess() && ( + <> + {form.title} + {form.description ? {form.description} : null} + + )} + {formIsLoading || responseIsLoading || loadingDelay || formIsError || resposneIsError ? ( + <> + + + + + + + ) : null} - {question.map(({ questionId, title, essential }, questionIndex) => ( - -
- {title} - {essential ? * : null} -
- -
- ))} + {checkApiSuccess() && + question.map(({ questionId, title, essential }, questionIndex) => ( + +
+ {title} + {essential ? * : null} +
+ +
+ ))} + {checkApiLoadingOrError() + ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( + + + + + + + + + )) + : null} - + {checkApiSuccess() && ( + + )} + {checkApiLoadingOrError() ? ( + <> + + + + ) : null}
diff --git a/client/src/pages/View/style.ts b/client/src/pages/View/style.ts index 50c56d9..3a13e37 100644 --- a/client/src/pages/View/style.ts +++ b/client/src/pages/View/style.ts @@ -9,6 +9,8 @@ const HeadContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 10px 20px; + position: relative; + overflow: hidden; `; const HeadTitle = styled.div` @@ -41,6 +43,9 @@ const QuestionContainer = styled.div<{ isEssential: boolean }>` css` border: 1px solid ${({ theme }) => theme.colors.red1}; `} + + position: relative; + overflow: hidden; `; const BottomContainer = styled.div` @@ -50,6 +55,8 @@ const BottomContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 20px; + position: relative; + overflow: hidden; `; const Essential = styled.span` From e3683b07010b0d7daf2f9ed87c66fbc627210648 Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 11:41:02 +0900 Subject: [PATCH 03/15] =?UTF-8?q?fix:=20=EC=BF=A0=ED=82=A4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=95=88=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/User/User.Controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/User/User.Controller.ts b/server/src/User/User.Controller.ts index b0e2b28..9914a3c 100644 --- a/server/src/User/User.Controller.ts +++ b/server/src/User/User.Controller.ts @@ -61,7 +61,7 @@ class UserController { userService .logout(userID) .then(() => { - res.status(204).clearCookie("accessToken").clearCookie("refreshToken"); + res.status(204).clearCookie("accessToken").clearCookie("refreshToken").end(); }) .catch((err) => { next(err); From f662f9b9c68e21365101b453ad8cc8a88ad4660a Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 11:41:41 +0900 Subject: [PATCH 04/15] =?UTF-8?q?chore:=20react-toastify=20library=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 34 ++++++++++++++++++++++++++++++++++ client/package.json | 1 + 2 files changed, 35 insertions(+) diff --git a/client/package-lock.json b/client/package-lock.json index d4b2103..54fe172 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -28,6 +28,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", "react-scripts": "5.0.1", + "react-toastify": "^9.1.1", "styled-components": "^5.3.6", "styled-reset": "^4.4.2", "typescript": "^4.8.4", @@ -5659,6 +5660,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14621,6 +14630,18 @@ } } }, + "node_modules/react-toastify": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", + "integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -21478,6 +21499,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -27766,6 +27792,14 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-toastify": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", + "integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==", + "requires": { + "clsx": "^1.1.1" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index 7fd21cb..9839305 100644 --- a/client/package.json +++ b/client/package.json @@ -23,6 +23,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", "react-scripts": "5.0.1", + "react-toastify": "^9.1.1", "styled-components": "^5.3.6", "styled-reset": "^4.4.2", "typescript": "^4.8.4", From 7748d52a84ddf289e5ff0675fe855f45bcd88e3e Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 11:48:18 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20=EC=84=A4=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=9E=91=EC=84=B1=20=EC=A0=9C=EC=B6=9C?= =?UTF-8?q?=EC=8B=9C=20validation=20=EC=97=90=EB=9F=AC=20=EA=B1=B8?= =?UTF-8?q?=EB=A6=B0=20=EA=B2=BD=EC=9A=B0=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=82=A0=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/View/index.tsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index 0185e37..44cfc58 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -14,6 +14,8 @@ import theme from "styles/theme"; import responseApi from "api/responseApi"; import useLoadingDelay from "hooks/useLoadingDelay"; import { ResponseElement, Validation } from "types/response"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import * as S from "./style"; const initialState: FormState = { @@ -92,6 +94,17 @@ function View() { const onClickSubmitForm = async () => { setValidationMode(true); const checkResult = validationCheck(validation); + if (!checkResult) + toast.error("필수 질문을 작성해주세요!", { + position: "bottom-center", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); if (checkResult) { let responseId; if (!prevResponseId) responseId = await responseApi.sendResponse(id, responseState); @@ -179,6 +192,18 @@ function View() { ) : null} +
); From b1efbc0fa41037444b3fb4520f10591f02349f1b Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 12:03:08 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=EC=84=A4=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=20?= =?UTF-8?q?=EC=84=A4=EB=AC=B8=EC=A7=80=20=EB=AC=B8=ED=95=AD=EC=9D=B4=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=8C=80=EB=B9=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Edit/index.tsx | 2 +- client/src/pages/View/index.tsx | 54 +++++++++++++++++++-------------- client/src/pages/View/style.ts | 16 +++++++++- client/src/utils/form.ts | 4 +-- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/client/src/pages/Edit/index.tsx b/client/src/pages/Edit/index.tsx index 692f88a..f4306c9 100644 --- a/client/src/pages/Edit/index.tsx +++ b/client/src/pages/Edit/index.tsx @@ -59,7 +59,7 @@ function Edit() { useEffect(() => { if (!id) return; - if (isSuccess) dispatch({ type: "FETCH_DATA", init: fromApiToForm(data) }); + if (isSuccess) dispatch({ type: "FETCH_DATA", init: fromApiToForm(data, "edit") }); }, [data, id, isSuccess]); const onClickTitle = () => { diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index 44cfc58..086ef72 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -79,7 +79,7 @@ function View() { useEffect(() => { if (!id) return; if (formIsSuccess) { - setState(fromApiToForm(formData)); + setState(fromApiToForm(formData, "view")); const checkList = fromApiToValidateCheckList(formData); setValidation(checkList); } @@ -132,7 +132,7 @@ function View() { {form.description ? {form.description} : null} )} - {formIsLoading || responseIsLoading || loadingDelay || formIsError || resposneIsError ? ( + {checkApiLoadingOrError() ? ( <> @@ -142,7 +142,7 @@ function View() { ) : null} - {checkApiSuccess() && + {checkApiSuccess() && question.length ? ( question.map(({ questionId, title, essential }, questionIndex) => (
@@ -160,7 +160,12 @@ function View() { setValidation={setValidation} /> - ))} + )) + ) : ( + + 설문지 문항이 존재하지 않습니다. + + )} {checkApiLoadingOrError() ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( @@ -173,25 +178,28 @@ function View() { )) : null} - - {checkApiSuccess() && ( - - )} - {checkApiLoadingOrError() ? ( - <> - - - - ) : null} - + {question.length ? ( + + {checkApiSuccess() && ( + + )} + {checkApiLoadingOrError() ? ( + <> + + + + ) : null} + + ) : null} + { +const fromApiToForm = (api: FormDataApi, pageType: string): FormState => { const { id, userID, @@ -17,7 +17,7 @@ const fromApiToForm = (api: FormDataApi): FormState => { let formQuestionList: QuestionState[]; let currentQuestionId = 1; - if (!questionList.length) + if (pageType === "edit" && !questionList.length) formQuestionList = [ { questionId: 1, From 07d9bab6b1b59fbd71897f6feb8c74229abf8fa9 Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 13:01:47 +0900 Subject: [PATCH 07/15] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=B9=88=EC=B9=B8=20?= =?UTF-8?q?=EB=AF=B8=EC=A0=95=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 --- client/src/pages/Forum/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/Forum/index.tsx b/client/src/pages/Forum/index.tsx index 2c7538e..ec4560f 100644 --- a/client/src/pages/Forum/index.tsx +++ b/client/src/pages/Forum/index.tsx @@ -136,7 +136,7 @@ function Forum() { {data?.form.map(({ formId, title, category: formCategory, responseCount }) => (
- 카테고리: {formCategory} + 카테고리: {formCategory || "미정"}
응답 수: {responseCount} From 3876249469c7952c4eceda02171080c8cb1b8c73 Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 14:17:13 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=EC=84=A4=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20validation?= =?UTF-8?q?=20=EC=B2=B4=ED=81=AC=20=EB=B0=8F=20toast=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Edit/index.tsx | 90 ++++++++++++++++++++++++++-- client/src/pages/Edit/style.ts | 2 - client/src/reducer/formEdit/index.ts | 7 ++- client/src/reducer/formEdit/type.ts | 2 +- 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/client/src/pages/Edit/index.tsx b/client/src/pages/Edit/index.tsx index f4306c9..8a6c99b 100644 --- a/client/src/pages/Edit/index.tsx +++ b/client/src/pages/Edit/index.tsx @@ -24,6 +24,8 @@ import { fromApiToForm, fromFormToApi } from "utils/form"; import useModal from "hooks/useModal"; import { CATEGORY_LIST, QUESTION_TYPE_LIST } from "store/form"; import useLoadingDelay from "hooks/useLoadingDelay"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import * as S from "./style"; const initialState: FormState = { @@ -46,7 +48,11 @@ function Edit() { const { id } = useParams(); const fetchForm = (): Promise => formApi.getForm(id); - const { data, isSuccess, isLoading, isError } = useQuery({ queryKey: [id], queryFn: fetchForm }); + const { data, isSuccess, isLoading, isError } = useQuery({ + queryKey: [id], + queryFn: fetchForm, + refetchOnWindowFocus: false, + }); const [state, dispatch] = useReducer(writeReducer, initialState); const { form, question } = state; @@ -113,7 +119,19 @@ function Edit() { }; const onClickDeleteQuestion = (questionIndex: number) => { - dispatch({ type: "DELETE_QUESTION", questionIndex }); + const toastCallback = () => { + toast.error("삭제가 불가능합니다!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); + }; + dispatch({ type: "DELETE_QUESTION", questionIndex, callback: toastCallback }); }; const onClickChangeQuestionEssential = (questionIndex: number) => { @@ -142,12 +160,58 @@ function Edit() { const onClickCopyLink = () => { window.navigator.clipboard.writeText(`${process.env.REACT_APP_CLIENT_ORIGIN_URL}/forms/${id}/view`); + toast.success("링크가 복사되었습니다!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); }; const onClickSaveForm = () => { if (!id) return; + if (!form.title) { + toast.error("제목을 작성해주세요!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); + return; + } + if (!form.category) { + toast.error("카테고리를 정해주세요!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); + return; + } const apiData = fromFormToApi(state); formApi.saveForm(id, apiData); + toast.success("저장이 완료되었습니다.!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); }; const onDragStart = (initial: DragStart) => { @@ -188,8 +252,12 @@ function Edit() { onClickTitle()}> {checkApiSuccess() && ( <> - - + + @@ -205,7 +273,6 @@ function Edit() { - ) : null} @@ -363,6 +430,19 @@ function Edit() { copyLink={onClickCopyLink} /> + + ); } diff --git a/client/src/pages/Edit/style.ts b/client/src/pages/Edit/style.ts index 8778542..5869e6b 100644 --- a/client/src/pages/Edit/style.ts +++ b/client/src/pages/Edit/style.ts @@ -9,8 +9,6 @@ const TitleContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 20px; - position: relative; - overflow: hidden; `; const TitleInput = styled.input` diff --git a/client/src/reducer/formEdit/index.ts b/client/src/reducer/formEdit/index.ts index d5919c8..c5b1281 100644 --- a/client/src/reducer/formEdit/index.ts +++ b/client/src/reducer/formEdit/index.ts @@ -139,7 +139,12 @@ function formEditReducer(state: FormState, action: FormEditAction) { }; } if (type === "DELETE_QUESTION") { - const { questionIndex } = action; + const { questionIndex, callback } = action; + const { length } = state.question; + if (length === 1) { + callback(); + return { ...state }; + } const leftQuestion = state.question.slice(0, questionIndex); const rightQuestion = state.question.slice(questionIndex + 1); diff --git a/client/src/reducer/formEdit/type.ts b/client/src/reducer/formEdit/type.ts index 7879c8c..5d54ac5 100644 --- a/client/src/reducer/formEdit/type.ts +++ b/client/src/reducer/formEdit/type.ts @@ -8,7 +8,7 @@ type FormEditAction = | { type: "ADD_QUESTION_CHOICE"; questionIndex: number } | { type: "MODIFY_QUESTION_CHOICE"; questionIndex: number; choiceIndex: number; value: string } | { type: "DELETE_QUESTION_CHOICE"; questionIndex: number; choiceIndex: number } - | { type: "DELETE_QUESTION"; questionIndex: number } + | { type: "DELETE_QUESTION"; questionIndex: number; callback: () => void } | { type: "COPY_QUESTION"; questionIndex: number } | { type: "ADD_QUESTION"; questionIndex: number } | { type: "CHANGE_QUESTION_ESSENTIAL"; questionIndex: number } From feb56c28a62eace650c20caa37064d07c39df8c3 Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 14:23:24 +0900 Subject: [PATCH 09/15] =?UTF-8?q?fix:=20=EB=A1=9C=EB=94=A9=EC=8B=9C=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/View/index.tsx | 50 ++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index 086ef72..6330bb4 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -142,30 +142,34 @@ function View() { ) : null} - {checkApiSuccess() && question.length ? ( - question.map(({ questionId, title, essential }, questionIndex) => ( - -
- {title} - {essential ? * : null} -
- + {checkApiSuccess() && + (question.length ? ( + question.map(({ questionId, title, essential }, questionIndex) => ( + +
+ {title} + {essential ? * : null} +
+ +
+ )) + ) : ( + + 설문지 문항이 존재하지 않습니다. - )) - ) : ( - - 설문지 문항이 존재하지 않습니다. - - )} + ))} {checkApiLoadingOrError() ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( From 3186c39a91b65414141927c4c29c5913f21a8b47 Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 16:29:48 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EC=8B=9C=20history=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/common/Button/index.tsx | 4 +- .../components/common/Pagination/index.tsx | 10 +-- client/src/pages/Forum/index.tsx | 70 +++++++++++++------ client/src/pages/Forum/style.ts | 4 +- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/client/src/components/common/Button/index.tsx b/client/src/components/common/Button/index.tsx index f440289..8092bf0 100644 --- a/client/src/components/common/Button/index.tsx +++ b/client/src/components/common/Button/index.tsx @@ -2,9 +2,8 @@ import React from "react"; import theme from "styles/theme"; import ButtonComponent from "./style"; -interface ButtonProps { +interface ButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; - type: "button" | "submit" | "reset"; color?: string; backgroundColor?: string; hover?: string; @@ -12,7 +11,6 @@ interface ButtonProps { active?: boolean; border?: string; custom?: string; - onClick: () => void; } function Button({ diff --git a/client/src/components/common/Pagination/index.tsx b/client/src/components/common/Pagination/index.tsx index 1ca90c3..5c359df 100644 --- a/client/src/components/common/Pagination/index.tsx +++ b/client/src/components/common/Pagination/index.tsx @@ -6,11 +6,11 @@ import * as S from "./style"; function Pagination({ currentPage, lastPage, - setPage, + callback, }: { currentPage: number; lastPage: number; - setPage: React.Dispatch>; + callback: (pageNumber: number) => void; }) { const [pageNumbers, setPageNumbers] = useState([]); @@ -30,7 +30,7 @@ function Pagination({ setPage((prev) => prev - 1)} + onClick={() => callback(currentPage - 1)} disabled={currentPage === 1} icon="left" fill={theme.colors.grey5} @@ -38,7 +38,7 @@ function Pagination({ /> {pageNumbers.map((number) => ( - setPage(number)}> + callback(number)}> {number} ))} @@ -46,7 +46,7 @@ function Pagination({ setPage((prev) => prev + 1)} + onClick={() => callback(currentPage + 1)} disabled={currentPage === lastPage} icon="right" fill={theme.colors.grey5} diff --git a/client/src/pages/Forum/index.tsx b/client/src/pages/Forum/index.tsx index ec4560f..3384447 100644 --- a/client/src/pages/Forum/index.tsx +++ b/client/src/pages/Forum/index.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; import Layout from "components/template/BannerLayout"; import Button from "components/common/Button"; import theme from "styles/theme"; @@ -29,12 +29,34 @@ interface ForumApi { function Forum() { const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const initPage = Number(searchParams.get("page")) || 1; + const initCategory = searchParams.get("category") || ""; + const initKeyword = searchParams.get("keyword") || ""; + const initOrderBy = searchParams.get("orderBy") || ""; - const [inputSearch, setInputSearch] = useState(""); - const [keyword, setKeyword] = useState(""); - const [category, setCategory] = useState("전체"); - const [orderBy, setOrderBy] = useState("latestAsc"); - const [page, setPage] = useState(1); + function isTypeOfCategory(categoryParam: string): categoryParam is ForumCategory { + const Category = CATEGORY_FORUM_LIST as string[]; + return Category.includes(categoryParam); + } + function isTypeOfOrderBy(orderByParam: string): orderByParam is OrderBy { + const ORDER_BY = ["latestAsc", "responseAsc", "responseDesc"]; + return ORDER_BY.includes(orderByParam); + } + + const [inputSearch, setInputSearch] = useState(initKeyword); + const [keyword, setKeyword] = useState(initKeyword); + const [category, setCategory] = useState(isTypeOfCategory(initCategory) ? initCategory : "전체"); + const [orderBy, setOrderBy] = useState(isTypeOfOrderBy(initOrderBy) ? initOrderBy : "latestAsc"); + const [page, setPage] = useState(Number(initPage)); + + useEffect(() => { + setInputSearch(initKeyword); + setKeyword(initKeyword); + setCategory(isTypeOfCategory(initCategory) ? initCategory : "전체"); + setOrderBy(isTypeOfOrderBy(initOrderBy) ? initOrderBy : "latestAsc"); + setPage(Number(initPage)); + }, [initCategory, initKeyword, initOrderBy, initPage]); const fetchFormList = (): Promise => boardApi.getFormList({ title: keyword, category, orderBy, page }); const { data, isLoading, isSuccess, isError } = useQuery({ @@ -49,10 +71,15 @@ function Forum() { return false; }; + const onSubmitSearchKeyword: React.FormEventHandler = (e) => { + e.preventDefault(); + setSearchParams({ page: "1", category, keyword: inputSearch, orderBy }); + }; + return ( - + + + + ); +} + +export default LoginModal; diff --git a/client/src/components/Modal/LoginModal/style.ts b/client/src/components/Modal/LoginModal/style.ts new file mode 100644 index 0000000..a5eca16 --- /dev/null +++ b/client/src/components/Modal/LoginModal/style.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +const Container = styled.div` + position: absolute; + top: 35%; + left: 50%; + transform: translate(-50%, -50%); + + width: 400px; + border-radius: 3px; + padding: 24px; + + z-index: 2; + background-color: ${({ theme }) => theme.colors.white}; +`; + +const Title = styled.h2` + margin-bottom: 20px; + font-size: ${({ theme }) => theme.fontSize.sz20}; + font-weight: 400; +`; + +const Text = styled.p` + margin-bottom: 16px; + font-size: 14px; +`; + +const Input = styled.input` + width: 100%; + padding: 5px 10px; + border: 1px solid ${({ theme }) => theme.colors.grey3}; + border-radius: 3px; + margin-bottom: 24px; +`; + +const ButtonContainer = styled.div` + display: flex; + justify-content: right; +`; + +export { Container, ButtonContainer, Input, Title, Text }; diff --git a/client/src/pages/Response/index.tsx b/client/src/pages/Response/index.tsx index 222a0f3..b409c06 100644 --- a/client/src/pages/Response/index.tsx +++ b/client/src/pages/Response/index.tsx @@ -8,7 +8,9 @@ import * as S from "./style"; function Result() { const { id } = useParams(); - const { state } = useLocation(); + const { + state: { responseId, type }, + } = useLocation(); const navigate = useNavigate(); const fetchForm = (): Promise => formApi.getForm(id); @@ -24,7 +26,7 @@ function Result() { }, [isSuccess, data, id]); const onClickModifyPreviousResponse = () => { - navigate(`/forms/${id}/view`, { state }); + navigate(`/forms/${id}/view`, { state: responseId }); }; const onClickNavigateOtherResponse = () => { @@ -36,7 +38,7 @@ function Result() { {form?.title} - 응답이 기록되었습니다. + {type === "submitResponse" ? "응답이 기록되었습니다." : "이미 응답했습니다."} {form?.responseModifiable ? 응답 수정 : null} {!form?.loginRequired ? 다른 응답 제출 : null} diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index 6330bb4..1c476b4 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -1,6 +1,8 @@ -import React, { useEffect, useReducer, useState } from "react"; +import React, { useContext, useEffect, useReducer, useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import { FormState, FormDataApi } from "types/form"; import formViewReducer from "reducer/formView"; import formApi from "api/formApi"; @@ -10,12 +12,14 @@ import FormLayout from "components/template/Layout"; import QuestionView from "components/View/QuestionView"; import Button from "components/common/Button"; import Skeleton from "components/common/Skeleton"; +import LoginModal from "components/Modal/LoginModal"; import theme from "styles/theme"; import responseApi from "api/responseApi"; import useLoadingDelay from "hooks/useLoadingDelay"; +import useModal from "hooks/useModal"; import { ResponseElement, Validation } from "types/response"; -import { ToastContainer, toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; + +import { AuthContext } from "contexts/authProvider"; import * as S from "./style"; const initialState: FormState = { @@ -36,8 +40,10 @@ const initialState: FormState = { function View() { const { id } = useParams(); + const { auth } = useContext(AuthContext); const navigate = useNavigate(); const { state: prevResponseId } = useLocation(); + const { openModal, closeModal, ModalPortal } = useModal({ setBackgroundClickClose: true }); const fetchForm = (): Promise => formApi.getForm(id); const { @@ -58,6 +64,12 @@ function View() { queryFn: fetchResponse, }); + const checkDuplicateResponse = (): Promise<{ responseId: string | null }> => responseApi.checkDuplicateResponse(id); + const { data: isDuplicateResponse } = useQuery({ + queryKey: [id, "duplicateResponse"], + queryFn: checkDuplicateResponse, + }); + const loadingDelay = useLoadingDelay(formIsLoading || responseIsLoading); const [state, setState] = useState(initialState); @@ -66,15 +78,33 @@ function View() { const [validationMode, setValidationMode] = useState(false); const [validation, setValidation] = useState({}); - const onClickAddResponse = (value: ResponseElement) => { - dispatch({ type: "ADD_RESPONSE", value }); - }; - const onClickDeleteResponse = (questionId: number) => { - dispatch({ type: "DELETE_RESPONSE", questionId }); - }; - const onClickEditResponse = (questionId: number, value: string[]) => { - dispatch({ type: "EDIT_RESPONSE", value, questionId }); - }; + useEffect(() => { + if (formIsSuccess && !formData.acceptResponse) { + navigate("/"); + return; + } + if ( + formIsSuccess && + formData.loginRequired && + isDuplicateResponse?.responseId && + formData.responseModifiable && + prevResponseId + ) { + // 중복 응답이 불가능(로그인 필수)하지만 재수정하는 경우 + return; + } + + if (formIsSuccess && formData.loginRequired && isDuplicateResponse?.responseId) { + // 중복 응답이 불가능(로그인 필수)하고 재수정이 아닌 경우 + navigate(`/forms/${id}/response`, { + state: { responseId: isDuplicateResponse.responseId, type: "duplicateResponse" }, + }); + return; + } + if (formIsSuccess && formData.loginRequired && !auth?.userID) { + openModal(); + } + }, [auth?.userID, formData, formIsSuccess, navigate, openModal, isDuplicateResponse, prevResponseId, id]); useEffect(() => { if (!id) return; @@ -91,6 +121,16 @@ function View() { } }, [formData, id, formIsSuccess, responseData, responseIsSuccess]); + const onClickAddResponse = (value: ResponseElement) => { + dispatch({ type: "ADD_RESPONSE", value }); + }; + const onClickDeleteResponse = (questionId: number) => { + dispatch({ type: "DELETE_RESPONSE", questionId }); + }; + const onClickEditResponse = (questionId: number, value: string[]) => { + dispatch({ type: "EDIT_RESPONSE", value, questionId }); + }; + const onClickSubmitForm = async () => { setValidationMode(true); const checkResult = validationCheck(validation); @@ -109,7 +149,7 @@ function View() { let responseId; if (!prevResponseId) responseId = await responseApi.sendResponse(id, responseState); if (prevResponseId) responseId = await responseApi.patchResponse(id, prevResponseId, responseState); - navigate(`/forms/${id}/response`, { state: responseId }); + navigate(`/forms/${id}/response`, { state: { responseId, type: "submitResponse" } }); } }; @@ -216,6 +256,9 @@ function View() { pauseOnHover={false} theme="light" /> + + + ); From e115b1a45f3f33a43183f062133179a0756edea1 Mon Sep 17 00:00:00 2001 From: dohpark Date: Mon, 12 Dec 2022 23:36:56 +0900 Subject: [PATCH 15/15] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Response/index.tsx | 32 +++++++++++++++++++---------- client/src/pages/Response/style.ts | 7 +++---- client/src/pages/View/index.tsx | 4 +++- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/client/src/pages/Response/index.tsx b/client/src/pages/Response/index.tsx index b409c06..1c8b9ae 100644 --- a/client/src/pages/Response/index.tsx +++ b/client/src/pages/Response/index.tsx @@ -8,9 +8,9 @@ import * as S from "./style"; function Result() { const { id } = useParams(); - const { - state: { responseId, type }, - } = useLocation(); + const { state } = useLocation() as { + state: { responseId: string; type: "submitResponse" | "duplicateResponse" | "endResponse" }; + }; const navigate = useNavigate(); const fetchForm = (): Promise => formApi.getForm(id); @@ -26,24 +26,34 @@ function Result() { }, [isSuccess, data, id]); const onClickModifyPreviousResponse = () => { - navigate(`/forms/${id}/view`, { state: responseId }); + navigate(`/forms/${id}/view`, { state: state.responseId }); }; const onClickNavigateOtherResponse = () => { navigate(`/forms/${id}/view`); }; + const getTitle = () => { + if (state && state.type === "submitResponse") return "응답이 기록되었습니다."; + if (state && state.type === "duplicateResponse") return "이미 응답했습니다."; + if (state && state.type === "endResponse") return "더 이상 응답을 받지 않습니다."; + navigate("/"); + return ""; + }; + return ( - + {form?.title} - {type === "submitResponse" ? "응답이 기록되었습니다." : "이미 응답했습니다."} - - {form?.responseModifiable ? 응답 수정 : null} - {!form?.loginRequired ? 다른 응답 제출 : null} - - + {getTitle()} + {form?.acceptResponse ? ( + + {form?.responseModifiable ? 응답 수정 : null} + {!form?.loginRequired ? 다른 응답 제출 : null} + + ) : null} + ); diff --git a/client/src/pages/Response/style.ts b/client/src/pages/Response/style.ts index b7ea1c9..1e823d1 100644 --- a/client/src/pages/Response/style.ts +++ b/client/src/pages/Response/style.ts @@ -4,11 +4,11 @@ const Container = styled.div` width: 760px; `; -const HeadContainer = styled.div` +const ResponseWrapper = styled.div` margin-top: 36px; background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; - padding: 10px 20px; + padding: 10px 20px 30px; `; const Title = styled.div` @@ -29,7 +29,6 @@ const Description = styled.p` const LinkWrapper = styled.div` margin-top: 24px; - margin-bottom: 18px; display: flex; flex-direction: column; `; @@ -43,4 +42,4 @@ const Link = styled.a` cursor: pointer; `; -export { Container, HeadContainer, Title, Description, LinkWrapper, Link }; +export { Container, ResponseWrapper, Title, Description, LinkWrapper, Link }; diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index 1c476b4..ce83c2f 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -80,7 +80,9 @@ function View() { useEffect(() => { if (formIsSuccess && !formData.acceptResponse) { - navigate("/"); + navigate(`/forms/${id}/response`, { + state: { responseId: "", type: "endResponse" }, + }); return; } if (