diff --git a/client/src/components/Edit/QuestionEdit/Objective/index.tsx b/client/src/components/Edit/QuestionEdit/Objective/index.tsx index aca4542..ce13776 100644 --- a/client/src/components/Edit/QuestionEdit/Objective/index.tsx +++ b/client/src/components/Edit/QuestionEdit/Objective/index.tsx @@ -30,7 +30,7 @@ function Objective({ index, questionState, addQuestionChoice, modifyChoice, dele color={theme.colors.grey5} fontSize={theme.fontSize.sz14} onClick={() => addQuestionChoice(index)} - custom="padding-left: 2px;" + style={{ paddingLeft: "2px" }} > 옵션 추가 diff --git a/client/src/components/Modal/DeleteFormModal/index.tsx b/client/src/components/Modal/DeleteFormModal/index.tsx index 818ff7d..1193dc2 100644 --- a/client/src/components/Modal/DeleteFormModal/index.tsx +++ b/client/src/components/Modal/DeleteFormModal/index.tsx @@ -25,7 +25,7 @@ function DeleteFormModal({ closeModal, refetchData, selectedFormId }: DeleteForm border={theme.colors.red1} color={theme.colors.red1} fontSize={theme.fontSize.sz12} - custom="margin-right: 12px;" + style={{ marginRight: "12px" }} hover={theme.colors.red0} active > diff --git a/client/src/components/Modal/EditFormNameModal/index.tsx b/client/src/components/Modal/EditFormNameModal/index.tsx index 5475dc8..d4ee277 100644 --- a/client/src/components/Modal/EditFormNameModal/index.tsx +++ b/client/src/components/Modal/EditFormNameModal/index.tsx @@ -32,7 +32,7 @@ function EditFormNameModal({ closeModal, selectedFormId, refetchData }: EditForm border={theme.colors.blue2} color={theme.colors.blue2} fontSize={theme.fontSize.sz12} - custom="margin-right: 12px;" + style={{ marginRight: "12px" }} hover={theme.colors.blue0} active > diff --git a/client/src/components/Modal/ShareFormModal/index.tsx b/client/src/components/Modal/ShareFormModal/index.tsx index 8fe9d6b..eed52ee 100644 --- a/client/src/components/Modal/ShareFormModal/index.tsx +++ b/client/src/components/Modal/ShareFormModal/index.tsx @@ -64,7 +64,7 @@ function ShareFormModal({ backgroundColor={theme.colors.blue5} border={theme.colors.grey2} color={theme.colors.white} - custom="margin-right: 12px;" + style={{ marginRight: "12px" }} > 저장 diff --git a/client/src/components/common/Button/index.tsx b/client/src/components/common/Button/index.tsx index 8092bf0..6b2d81f 100644 --- a/client/src/components/common/Button/index.tsx +++ b/client/src/components/common/Button/index.tsx @@ -1,17 +1,7 @@ import React from "react"; import theme from "styles/theme"; import ButtonComponent from "./style"; - -interface ButtonProps extends React.ButtonHTMLAttributes { - children: React.ReactNode; - color?: string; - backgroundColor?: string; - hover?: string; - fontSize?: string; - active?: boolean; - border?: string; - custom?: string; -} +import { ButtonProps } from "./type"; function Button({ children, @@ -22,8 +12,8 @@ function Button({ color, hover, active, - custom, onClick, + style, }: ButtonProps) { return ( {children} @@ -49,7 +39,6 @@ Button.defaultProps = { color: theme.colors.black, hover: "", active: false, - custom: "", }; export default Button; diff --git a/client/src/components/common/Button/style.ts b/client/src/components/common/Button/style.ts index d282572..3cc575f 100644 --- a/client/src/components/common/Button/style.ts +++ b/client/src/components/common/Button/style.ts @@ -1,14 +1,5 @@ import styled, { css } from "styled-components"; - -interface StyledButtonProps { - color?: string; - backgroundColor?: string; - hover?: string; - fontSize?: string; - active?: boolean; - border?: string; - custom?: string; -} +import { StyledButtonProps } from "./type"; const Button = styled.button` display: flex; @@ -37,8 +28,6 @@ const Button = styled.button` transform: translateY(1px); } `} - - ${({ custom }) => custom} `; export default Button; diff --git a/client/src/components/common/Button/type.ts b/client/src/components/common/Button/type.ts new file mode 100644 index 0000000..c9e5e55 --- /dev/null +++ b/client/src/components/common/Button/type.ts @@ -0,0 +1,20 @@ +interface ButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; + color?: string; + backgroundColor?: string; + hover?: string; + fontSize?: string; + active?: boolean; + border?: string; +} + +interface StyledButtonProps { + color?: string; + backgroundColor?: string; + hover?: string; + fontSize?: string; + active?: boolean; + border?: string; +} + +export type { ButtonProps, StyledButtonProps }; diff --git a/client/src/components/common/Dropdown/IconDropdown/type.ts b/client/src/components/common/Dropdown/IconDropdown/type.ts index cc61aa5..76f0f72 100644 --- a/client/src/components/common/Dropdown/IconDropdown/type.ts +++ b/client/src/components/common/Dropdown/IconDropdown/type.ts @@ -1,5 +1,4 @@ import { IconType } from "components/common/Icon/type"; -import { QuestionType } from "types/form"; interface IconItem { value: string; diff --git a/client/src/components/common/Dropdown/TextDropdown/index.tsx b/client/src/components/common/Dropdown/TextDropdown/index.tsx index c1844ba..33a517b 100644 --- a/client/src/components/common/Dropdown/TextDropdown/index.tsx +++ b/client/src/components/common/Dropdown/TextDropdown/index.tsx @@ -55,20 +55,16 @@ Head.defaultProps = { bold: false, }; -function ItemList({ children, custom = "" }: ItemListProps) { +function ItemList({ children, style }: ItemListProps) { const { open, setOpen } = useContext(TextDropdownContext); return open ? ( setOpen && setOpen(false)}> - {children} + {children} ) : null; } -ItemList.defaultProps = { - custom: "", -}; - function Item({ value, onClick }: ItemProps) { const { setSelected, setOpen, fontSize } = useContext(TextDropdownContext); diff --git a/client/src/components/common/Dropdown/TextDropdown/style.ts b/client/src/components/common/Dropdown/TextDropdown/style.ts index d6892a4..eafa430 100644 --- a/client/src/components/common/Dropdown/TextDropdown/style.ts +++ b/client/src/components/common/Dropdown/TextDropdown/style.ts @@ -19,7 +19,7 @@ const Button = styled.button<{ fontSize: string; border: string; padding: string cursor: pointer; `; -const Content = styled.ul<{ custom: string }>` +const Content = styled.ul` width: 100%; position: absolute; z-index: 1; @@ -28,8 +28,6 @@ const Content = styled.ul<{ custom: string }>` border-radius: 3px; border: 1px solid ${({ theme }) => theme.colors.grey3}; - ${({ custom }) => custom} - li { text-align: left; diff --git a/client/src/components/common/Dropdown/TextDropdown/type.ts b/client/src/components/common/Dropdown/TextDropdown/type.ts index 547774d..77bf529 100644 --- a/client/src/components/common/Dropdown/TextDropdown/type.ts +++ b/client/src/components/common/Dropdown/TextDropdown/type.ts @@ -1,3 +1,5 @@ +import { HTMLAttributes } from "react"; + interface DropdownProps { state: string; defaultState: string; @@ -17,9 +19,8 @@ interface ItemProps { onClick: () => void; } -interface ItemListProps { +interface ItemListProps extends HTMLAttributes { children: React.ReactNode; - custom?: string; } export type { DropdownProps, HeadProps, ItemProps, ItemListProps }; diff --git a/client/src/components/common/ErrorBoundary/index.tsx b/client/src/components/common/ErrorBoundary/index.tsx new file mode 100644 index 0000000..ae9fc32 --- /dev/null +++ b/client/src/components/common/ErrorBoundary/index.tsx @@ -0,0 +1,38 @@ +/* eslint-disable react/prefer-stateless-function */ +import React from "react"; + +interface ErrorBoundaryProps { + children: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + + this.state = { + hasError: false, + error: null, + }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + render() { + const { hasError, error } = this.state; + const { children } = this.props; + + if (hasError) { + return
sth went wrong {error?.message}
; + } + return children; + } +} + +export default ErrorBoundary; diff --git a/client/src/components/common/IconButton/index.tsx b/client/src/components/common/IconButton/index.tsx index 5ffae3b..5191643 100644 --- a/client/src/components/common/IconButton/index.tsx +++ b/client/src/components/common/IconButton/index.tsx @@ -1,22 +1,11 @@ import React from "react"; import Icon from "components/common/Icon"; -import { IconType } from "components/common/Icon/type"; import IconButtonComponent from "./style"; +import { IconButtonProps } from "./type"; -interface IconButtonProps { - type: "button" | "submit" | "reset"; - icon: IconType; - fill?: string; - size: string; - active?: boolean; - onClick: () => void; - disabled?: boolean; - custom?: string; -} - -function IconButton({ size, type, active, fill, onClick, disabled, custom, icon }: IconButtonProps) { +function IconButton({ size, type, active, fill, onClick, disabled, icon, style }: IconButtonProps) { return ( - + ); @@ -26,7 +15,6 @@ IconButton.defaultProps = { fill: "black", disabled: false, active: false, - custom: "", }; export default IconButton; diff --git a/client/src/components/common/IconButton/style.ts b/client/src/components/common/IconButton/style.ts index ca3bf24..46a75b4 100644 --- a/client/src/components/common/IconButton/style.ts +++ b/client/src/components/common/IconButton/style.ts @@ -1,9 +1,5 @@ import styled, { css } from "styled-components"; - -interface StyledIconButtonProps { - active?: boolean; - custom?: string; -} +import { StyledIconButtonProps } from "./type"; const IconButton = styled.button` padding: 0; @@ -32,8 +28,6 @@ const IconButton = styled.button` transform: translateY(1px); } `} - - ${({ custom }) => custom} `; export default IconButton; diff --git a/client/src/components/common/IconButton/type.ts b/client/src/components/common/IconButton/type.ts new file mode 100644 index 0000000..694d09b --- /dev/null +++ b/client/src/components/common/IconButton/type.ts @@ -0,0 +1,16 @@ +import { IconType } from "components/common/Icon/type"; + +interface IconButtonProps extends React.ButtonHTMLAttributes { + type: "button" | "submit" | "reset"; + icon: IconType; + fill?: string; + size: string; + active?: boolean; + disabled?: boolean; +} + +interface StyledIconButtonProps { + active?: boolean; +} + +export type { IconButtonProps, StyledIconButtonProps }; diff --git a/client/src/components/common/Pagination/index.tsx b/client/src/components/common/Pagination/index.tsx index 5c359df..8da1a31 100644 --- a/client/src/components/common/Pagination/index.tsx +++ b/client/src/components/common/Pagination/index.tsx @@ -3,15 +3,13 @@ import IconButton from "components/common/IconButton"; import theme from "styles/theme"; import * as S from "./style"; -function Pagination({ - currentPage, - lastPage, - callback, -}: { +interface PaginationProps { currentPage: number; lastPage: number; callback: (pageNumber: number) => void; -}) { +} + +function Pagination({ currentPage, lastPage, callback }: PaginationProps) { const [pageNumbers, setPageNumbers] = useState([]); useEffect(() => { @@ -34,7 +32,7 @@ function Pagination({ disabled={currentPage === 1} icon="left" fill={theme.colors.grey5} - custom="height: 24px;" + style={{ height: "24px" }} /> {pageNumbers.map((number) => ( @@ -50,7 +48,7 @@ function Pagination({ disabled={currentPage === lastPage} icon="right" fill={theme.colors.grey5} - custom="height: 24px;" + style={{ height: "24px" }} /> ); diff --git a/client/src/components/common/Skeleton/index.tsx b/client/src/components/common/Skeleton/index.tsx index 6a1657d..65fc8d2 100644 --- a/client/src/components/common/Skeleton/index.tsx +++ b/client/src/components/common/Skeleton/index.tsx @@ -1,13 +1,10 @@ import React from "react"; import * as S from "./style"; -import SkeletonType from "./type"; +import { SkeletonType, SkeletonContainerProps } from "./type"; -function SkeletonContainer({ children, custom = "" }: { children: React.ReactNode; custom?: string }) { - return {children}; +function SkeletonContainer({ children, style }: SkeletonContainerProps) { + return {children}; } -SkeletonContainer.defaultProps = { - custom: "", -}; 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 c61ed13..9ae257b 100644 --- a/client/src/components/common/Skeleton/style.ts +++ b/client/src/components/common/Skeleton/style.ts @@ -1,5 +1,5 @@ import styled, { css } from "styled-components"; -import SkeletonType from "./type"; +import { SkeletonType } from "./type"; const getSkeletonTypeCss = (type: SkeletonType) => { switch (type) { @@ -57,15 +57,13 @@ const getSkeletonTypeCss = (type: SkeletonType) => { } }; -const Container = styled.div<{ custom: string }>` +const Container = styled.div` margin: 20px auto; padding: 10px 15px; background-color: #f2f2f2; border-radius: 3px; position: relative; overflow: hidden; - - ${({ custom }) => custom} `; const Element = styled.div<{ type: SkeletonType }>` diff --git a/client/src/components/common/Skeleton/type.ts b/client/src/components/common/Skeleton/type.ts index 5658eed..df80b4e 100644 --- a/client/src/components/common/Skeleton/type.ts +++ b/client/src/components/common/Skeleton/type.ts @@ -7,4 +7,8 @@ type SkeletonType = | "button" | "formQuestionTitleEdit"; -export default SkeletonType; +interface SkeletonContainerProps extends React.HTMLAttributes { + children: React.ReactNode; +} + +export type { SkeletonType, SkeletonContainerProps }; diff --git a/client/src/components/common/TextButton/index.tsx b/client/src/components/common/TextButton/index.tsx index 8353bc5..cdaffe5 100644 --- a/client/src/components/common/TextButton/index.tsx +++ b/client/src/components/common/TextButton/index.tsx @@ -1,16 +1,10 @@ import React from "react"; import TextButtonComponent from "./style"; +import { TextButtonProps } from "./type"; -interface TextButtonProps extends React.ButtonHTMLAttributes { - children: React.ReactNode; - color?: string; - fontSize?: string; - custom?: string; -} - -function TextButton({ children, color, fontSize, custom, onClick }: TextButtonProps) { +function TextButton({ children, color, fontSize, style, onClick }: TextButtonProps) { return ( - + {children} ); @@ -19,7 +13,6 @@ function TextButton({ children, color, fontSize, custom, onClick }: TextButtonPr TextButton.defaultProps = { color: "black", fontSize: "16px", - custom: "", }; export default TextButton; diff --git a/client/src/components/common/TextButton/style.ts b/client/src/components/common/TextButton/style.ts index 4726c59..9d34a77 100644 --- a/client/src/components/common/TextButton/style.ts +++ b/client/src/components/common/TextButton/style.ts @@ -1,10 +1,5 @@ import styled from "styled-components"; - -interface StyledTextButtonProps { - color?: string; - fontSize?: string; - custom?: string; -} +import { StyledTextButtonProps } from "./type"; const TextButtonComponent = styled.button` border: 0px; @@ -21,8 +16,6 @@ const TextButtonComponent = styled.button` text-decoration: underline; filter: brightness(85%); } - - ${({ custom }) => custom} `; export default TextButtonComponent; diff --git a/client/src/components/common/TextButton/type.ts b/client/src/components/common/TextButton/type.ts new file mode 100644 index 0000000..bf77ef5 --- /dev/null +++ b/client/src/components/common/TextButton/type.ts @@ -0,0 +1,12 @@ +interface TextButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; + color?: string; + fontSize?: string; +} + +interface StyledTextButtonProps { + color?: string; + fontSize?: string; +} + +export type { TextButtonProps, StyledTextButtonProps }; diff --git a/client/src/pages/Edit/index.tsx b/client/src/pages/Edit/index.tsx index 255d625..50aec08 100644 --- a/client/src/pages/Edit/index.tsx +++ b/client/src/pages/Edit/index.tsx @@ -4,7 +4,10 @@ import React, { useEffect, useReducer, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { DragDropContext, Droppable, Draggable, DropResult, DragStart } from "react-beautiful-dnd"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import formApi from "api/formApi"; import FormLayout from "components/template/Layout"; import IconDropdown from "components/common/Dropdown/IconDropdown"; import Question from "components/Edit/QuestionEdit"; @@ -16,34 +19,16 @@ import Skeleton from "components/common/Skeleton"; import ShareFormModal from "components/Modal/ShareFormModal"; import Button from "components/common/Button"; import IconButton from "components/common/IconButton"; -import theme from "styles/theme"; -import writeReducer from "reducer/formEdit"; -import { FormState, FormDataApi, QuestionType } from "types/form"; -import formApi from "api/formApi"; -import { fromApiToForm, fromFormToApi } from "utils/form"; +import ErrorBoundary from "components/common/ErrorBoundary"; 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 writeReducer from "reducer/formEdit"; +import { CATEGORY_LIST, INITIAL_FORM, QUESTION_TYPE_LIST } from "store/form"; +import theme from "styles/theme"; +import { FormDataApi, QuestionType } from "types/form"; +import { fromApiToForm, fromFormToApi } from "utils/form"; import * as S from "./style"; -const initialState: FormState = { - form: { - id: "example", - userId: 3, - title: "", - description: "", - category: "기타", - acceptResponse: false, - onBoard: false, - loginRequired: false, - responseModifiable: false, - currentQuestionId: 1, - }, - question: [], -}; - function Edit() { const { id } = useParams(); const navigate = useNavigate(); @@ -53,14 +38,16 @@ function Edit() { queryKey: [id], queryFn: fetchForm, refetchOnWindowFocus: false, - onError: (error: { response: { status: number } }) => { - const { status } = error.response; - if (status === 400 || status === 404 || status === 404 || status === 500) navigate("/error", { state: status }); - if (status === 401) navigate("/login"); - }, + retry: 2, + useErrorBoundary: true, + // onError: (error: { response: { status: number } }) => { + // const { status } = error.response; + // if (status === 400 || status === 404 || status === 404 || status === 500) throw new Response("error", { status }); + // if (status === 401) navigate("/login"); + // }, }); - const [state, dispatch] = useReducer(writeReducer, initialState); + const [state, dispatch] = useReducer(writeReducer, INITIAL_FORM); const { form, question } = state; const [focus, setFocus] = useState(""); const [hover, setHover] = useState(""); @@ -253,203 +240,199 @@ function Edit() { }; return ( - - - onClickTitle()}> - {checkApiSuccess() && ( - <> - - - - - - {CATEGORY_LIST.map((value) => ( - onClickSelectCategory(value)} /> - ))} - - - - )} + + + {checkApiLoadingOrError() ? ( <> - - - - - ) : null} - - - - {(droppable) => ( -
- {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="질문" - /> - { - 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) => ( - - - - + + + + {Array.from({ length: 2 }, (_, index) => index).map((value) => ( + + + + + + + + + ))} + + - - )) - : null} - - {checkApiSuccess() && ( - - )} - {checkApiLoadingOrError() ? ( + + + ) : null} + {checkApiSuccess() ? ( <> - - + onClickTitle()}> + + + + + + {CATEGORY_LIST.map((value) => ( + onClickSelectCategory(value)} /> + ))} + + + + + + {(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" + style={{ marginRight: "12px" }} + /> + onClickCopyQuestion(questionIndex)} + icon="copy" + size="18px" + style={{ marginRight: "12px" }} + /> + onClickDeleteQuestion(questionIndex)} + icon="trashcan" + size="18px" + style={{ marginRight: "12px" }} + /> + + 필수 + onClickChangeQuestionEssential(questionIndex)} + /> + + + + )} + {focus !== `q${questionIndex}` && ( + <> +
{title}
+ + + )} +
+ ); + }} +
+ ))} + {droppable.placeholder} +
+ )} +
+
+ + + ) : null} - -
- - - + + + + + + - - - -
+ + ); } diff --git a/client/src/pages/Error/index.tsx b/client/src/pages/Error/index.tsx index c6452b6..f01afc2 100644 --- a/client/src/pages/Error/index.tsx +++ b/client/src/pages/Error/index.tsx @@ -1,20 +1,26 @@ import React, { useEffect, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate, useRouteError } from "react-router-dom"; import FormLayout from "components/template/Layout"; import Button from "components/common/Button"; import theme from "styles/theme"; import * as S from "./style"; +interface ErrorType { + response: { status: number }; +} + function Error() { + const error = useRouteError() as ErrorType; + + const { status: statusCode } = error.response; const navigate = useNavigate(); - const { state } = useLocation(); const [status, setStatus] = useState({ code: 404, message: "죄송합니다. 원하시는 페이지를 찾을 수가 없습니다." }); useEffect(() => { - if (state === 400) setStatus({ code: 400, message: "죄송합니다. 원하시는 페이지를 찾을 수가 없습니다." }); - if (state === 404) setStatus({ code: 404, message: "죄송합니다. 원하시는 페이지를 찾을 수가 없습니다." }); - if (state === 500) setStatus({ code: 500, message: "죄송합니다. 원하시는 페이지를 찾을 수가 없습니다." }); - }, [state]); + if (statusCode === 400) setStatus({ code: 400, message: "죄송합니다. 페이지를 표시할 수 없습니다." }); + if (statusCode === 404) setStatus({ code: 404, message: "죄송합니다. 원하시는 페이지를 찾을 수가 없습니다." }); + if (statusCode === 500) setStatus({ code: 500, message: "죄송합니다. 페이지를 표시할 수 없습니다." }); + }, [statusCode]); return ( diff --git a/client/src/pages/Error/style.ts b/client/src/pages/Error/style.ts index 577d000..e81da56 100644 --- a/client/src/pages/Error/style.ts +++ b/client/src/pages/Error/style.ts @@ -3,19 +3,25 @@ import styled from "styled-components"; const Container = styled.section` margin-top: 64px; min-width: 1024px; + display: flex; + flex-direction: column; + align-items: center; `; const H2 = styled.p` text-align: center; display: block; + margin-bottom: 32px; + color: ${({ theme }) => theme.colors.grey7}; `; const H1 = styled.h1` text-align: center; font-size: 60px; - margin: auto; max-width: 600px; line-height: 72px; + margin-top: 64px; + color: ${({ theme }) => theme.colors.blue3}; `; export { Container, H2, H1 }; diff --git a/client/src/pages/Forum/index.tsx b/client/src/pages/Forum/index.tsx index fe47de7..753066a 100644 --- a/client/src/pages/Forum/index.tsx +++ b/client/src/pages/Forum/index.tsx @@ -1,18 +1,19 @@ import React, { useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; + +import boardApi from "api/forumApi"; import Layout from "components/template/BannerLayout"; import Button from "components/common/Button"; -import theme from "styles/theme"; -import boardApi from "api/forumApi"; -import { useQuery } from "@tanstack/react-query"; import TextDropdown from "components/common/Dropdown/TextDropdown"; import Card from "components/common/Card"; import Pagination from "components/common/Pagination"; import Notice from "components/common/Notice"; import Skeleton from "components/common/Skeleton"; import useLoadingDelay from "hooks/useLoadingDelay"; -import { ForumCategory, OrderBy } from "types/forum"; import { CATEGORY_FORUM_LIST } from "store/form"; +import theme from "styles/theme"; +import { ForumCategory, OrderBy } from "types/forum"; import * as S from "./style"; interface FormList { @@ -97,7 +98,7 @@ function Forum() { fontSize={theme.fontSize.sz12} backgroundColor={theme.colors.blue3} color={theme.colors.white} - custom="margin-left: 2px;" + style={{ marginLeft: "2px" }} > 검색 @@ -141,7 +142,7 @@ function Forum() { 카테고리 - + {CATEGORY_FORUM_LIST.map((value) => ( navigate(`/forms/${formId}/view`)} backgroundColor={theme.colors.blue3} color={theme.colors.white} - custom="margin-right: 8px;" + style={{ marginRight: "8px" }} > 설문조사 참여하기 diff --git a/client/src/pages/Forum/style.ts b/client/src/pages/Forum/style.ts index 519ce78..da21150 100644 --- a/client/src/pages/Forum/style.ts +++ b/client/src/pages/Forum/style.ts @@ -14,7 +14,7 @@ const divSearchBox = styled.form` `; const inputSearch = styled.input` - width: calc(100% - 55px); + width: calc(100% - 60px); height: 37px; padding: 0 10px; border: 1px solid ${({ theme }) => theme.colors.grey3}; diff --git a/client/src/pages/Main/index.tsx b/client/src/pages/Main/index.tsx index abafa4a..f97d1cd 100644 --- a/client/src/pages/Main/index.tsx +++ b/client/src/pages/Main/index.tsx @@ -1,10 +1,10 @@ import React, { useContext } from "react"; import { useNavigate } from "react-router-dom"; +import Example from "assets/Images/Example.png"; +import { AuthContext } from "contexts/authProvider"; import FormLayout from "components/template/Layout"; import Button from "components/common/Button"; import theme from "styles/theme"; -import { AuthContext } from "contexts/authProvider"; -import Example from "assets/Images/Example.png"; import * as S from "./style"; function Main() { diff --git a/client/src/pages/MyForms/index.tsx b/client/src/pages/MyForms/index.tsx index 56801bd..894369d 100644 --- a/client/src/pages/MyForms/index.tsx +++ b/client/src/pages/MyForms/index.tsx @@ -1,20 +1,21 @@ import React, { useCallback, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; -import formApi from "api/formApi"; import { useInfiniteQuery } from "@tanstack/react-query"; + +import formApi from "api/formApi"; import BannerLayout from "components/template/BannerLayout"; import EditNameModal from "components/Modal/EditFormNameModal"; import DeleteSurveyModal from "components/Modal/DeleteFormModal"; -import useModal from "hooks/useModal"; -import useIntersectionObserver from "hooks/useIntersectionObserver"; -import { FormList } from "types/myForms"; import Card from "components/common/Card"; import Button from "components/common/Button"; import Icon from "components/common/Icon"; import Notice from "components/common/Notice"; import Skeleton from "components/common/Skeleton"; import useLoadingDelay from "hooks/useLoadingDelay"; +import useModal from "hooks/useModal"; +import useIntersectionObserver from "hooks/useIntersectionObserver"; import theme from "styles/theme"; +import { FormList } from "types/myForms"; import * as S from "./style"; function MyForms() { @@ -126,7 +127,7 @@ function MyForms() { onClick={() => onClickNavigateEditForm(_id)} backgroundColor={theme.colors.blue3} color={theme.colors.white} - custom="margin-right: 8px;" + style={{ marginRight: "8px" }} > 설문조사 수정하기 @@ -136,7 +137,7 @@ function MyForms() { border={theme.colors.blue3} backgroundColor={theme.colors.white} color={theme.colors.blue3} - custom="margin-right: 8px;" + style={{ marginRight: "8px" }} > 설문조사 결과보기 @@ -145,7 +146,7 @@ function MyForms() { onClick={() => onClickOpenNameChangeModal(_id)} backgroundColor={theme.colors.blue3} color={theme.colors.white} - custom="margin-right: 8px;" + style={{ marginRight: "8px" }} > 제목 수정하기 @@ -172,7 +173,7 @@ function MyForms() { ) : null} {checkApiLoadingOrError() ? Array.from({ length: 3 }, (_, index) => index).map((value) => ( - + diff --git a/client/src/pages/Response/index.tsx b/client/src/pages/Response/index.tsx index 7664918..577072a 100644 --- a/client/src/pages/Response/index.tsx +++ b/client/src/pages/Response/index.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react"; import { useParams, useLocation, useNavigate } from "react-router-dom"; -import FormLayout from "components/template/Layout"; -import { FormDataApi } from "types/form"; import { useQuery } from "@tanstack/react-query"; + import formApi from "api/formApi"; +import FormLayout from "components/template/Layout"; +import { FormDataApi } from "types/form"; import * as S from "./style"; function Result() { @@ -17,6 +18,7 @@ function Result() { const { data, isSuccess } = useQuery({ queryKey: [id, "form"], queryFn: fetchForm, + retry: false, onError: (error: { response: { status: number } }) => { const { status } = error.response; if (status === 400 || status === 404 || status === 404 || status === 500) navigate("/error", { state: status }); diff --git a/client/src/pages/Result/index.tsx b/client/src/pages/Result/index.tsx index 36705f6..8bdde47 100644 --- a/client/src/pages/Result/index.tsx +++ b/client/src/pages/Result/index.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import FormLayout from "components/template/Layout"; import { useQuery } from "@tanstack/react-query"; + import resultApi from "api/resultApi"; -import { ResultApi, QuestionSummary } from "types/result"; +import FormLayout from "components/template/Layout"; import QuestionResult from "components/Result/QuestionResult"; import Skeleton from "components/common/Skeleton"; import useLoadingDelay from "hooks/useLoadingDelay"; +import { ResultApi, QuestionSummary } from "types/result"; import * as S from "./style"; function Result() { @@ -17,6 +18,7 @@ function Result() { const { data, isSuccess, isLoading, isError } = useQuery({ queryKey: [id, "result"], queryFn: fetchForm, + retry: 2, onError: (error: { response: { status: number } }) => { const { status } = error.response; if (status === 400 || status === 404 || status === 404 || status === 500) navigate("/error", { state: status }); @@ -48,48 +50,15 @@ function Result() { return ( - - {checkApiSuccess() && ( - <> - {formResult?.formTitle} - 응답 {formResult?.totalResponseCount}개 - - )} - {checkApiLoadingOrError() ? ( - <> + {checkApiLoadingOrError() ? ( + <> + - - ) : null} - - {checkApiSuccess() && - (questionResult.length ? ( - questionResult.map(({ type, questionTitle, responseCount, answerTotal }) => ( - -
- {questionTitle} -
- {responseCount ? ( - - 응답 {responseCount}개 - - ) : null} - {responseCount ? ( - - ) : ( - 질문에 대한 응답이 없습니다. - )} -
- )) - ) : ( - - 설문지에 대한 응답이 없습니다. - - ))} - {checkApiLoadingOrError() - ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( +
+ {Array.from({ length: 2 }, (_, index) => index).map((value) => ( @@ -98,8 +67,40 @@ function Result() { - )) - : null} + ))} + + ) : null} + {checkApiSuccess() ? ( + <> + + {formResult?.formTitle} + 응답 {formResult?.totalResponseCount}개 + + {questionResult.length ? ( + questionResult.map(({ type, questionTitle, responseCount, answerTotal }) => ( + +
+ {questionTitle} +
+ {responseCount ? ( + + 응답 {responseCount}개 + + ) : null} + {responseCount ? ( + + ) : ( + 질문에 대한 응답이 없습니다. + )} +
+ )) + ) : ( + + 설문지에 대한 응답이 없습니다. + + )} + + ) : null}
); diff --git a/client/src/pages/View/index.tsx b/client/src/pages/View/index.tsx index 68b1cbf..8833c28 100644 --- a/client/src/pages/View/index.tsx +++ b/client/src/pages/View/index.tsx @@ -3,41 +3,26 @@ 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"; -import { fromApiToForm } from "utils/form"; -import { checkPrevResponseUpdateValidateCheckList, fromApiToValidateCheckList, validationCheck } from "utils/response"; +import responseApi from "api/responseApi"; +import { AuthContext } from "contexts/authProvider"; 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 formViewReducer from "reducer/formView"; +import theme from "styles/theme"; +import { INITIAL_FORM } from "store/form"; import { ResponseElement, Validation } from "types/response"; - -import { AuthContext } from "contexts/authProvider"; +import { FormDataApi } from "types/form"; +import { fromApiToForm } from "utils/form"; +import { checkPrevResponseUpdateValidateCheckList, fromApiToValidateCheckList, validationCheck } from "utils/response"; import * as S from "./style"; -const initialState: FormState = { - form: { - id: "example", - userId: 3, - title: "", - description: "", - category: "기타", - acceptResponse: false, - onBoard: false, - loginRequired: false, - responseModifiable: false, - currentQuestionId: 1, - }, - question: [], -}; - function View() { const { id } = useParams(); const { auth } = useContext(AuthContext); @@ -54,6 +39,7 @@ function View() { } = useQuery({ queryKey: [id, "form"], queryFn: fetchForm, + retry: 2, onError: (error: { response: { status: number } }) => { const { status } = error.response; if (status === 400 || status === 404 || status === 404 || status === 500) navigate("/error", { state: status }); @@ -91,7 +77,7 @@ function View() { const loadingDelay = useLoadingDelay(formIsLoading || responseIsLoading); - const [state, setState] = useState(initialState); + const [state, setState] = useState(INITIAL_FORM); const { form, question } = state; const [responseState, dispatch] = useReducer(formViewReducer, []); const [validationMode, setValidationMode] = useState(false); @@ -186,53 +172,16 @@ function View() { return ( - - {checkApiSuccess() && ( - <> - {form.title} - {form.description ? {form.description} : null} - - )} - {checkApiLoadingOrError() ? ( - <> + {checkApiLoadingOrError() ? ( + <> + - - ) : null} - - {checkApiSuccess() && - (question.length ? ( - question.map(({ questionId, title, essential }, questionIndex) => ( - -
- {title} - {essential ? * : null} -
- -
- )) - ) : ( - - 설문지 문항이 존재하지 않습니다. - - ))} - {checkApiLoadingOrError() - ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( +
+ {Array.from({ length: 2 }, (_, index) => index).map((value) => ( @@ -241,28 +190,60 @@ function View() { - )) - : null} - {question.length ? ( - - {checkApiSuccess() && ( - + ))} + + + + + + ) : null} + {checkApiSuccess() ? ( + <> + + {form.title} + {form.description ? {form.description} : null} + + {question.length ? ( + question.map(({ questionId, title, essential }, questionIndex) => ( + +
+ {title} + {essential ? * : null} +
+ +
+ )) + ) : ( + + 설문지 문항이 존재하지 않습니다. + )} - {checkApiLoadingOrError() ? ( - <> - - - + {question.length ? ( + + + ) : null} -
+ ) : null} , + errorElement: , }, { path: "/forms/:id/view", diff --git a/client/src/store/form.ts b/client/src/store/form.ts index 3b0b26b..ebdde47 100644 --- a/client/src/store/form.ts +++ b/client/src/store/form.ts @@ -1,5 +1,6 @@ import { IconItem } from "components/common/Dropdown/IconDropdown/type"; import { ForumCategory } from "types/forum"; +import { FormState } from "types/form"; const CATEGORY_LIST = ["개발 및 학습", "취업 및 채용", "취미 및 여가", "기타"]; @@ -11,4 +12,20 @@ const QUESTION_TYPE_LIST: IconItem[] = [ const CATEGORY_FORUM_LIST: ForumCategory[] = ["전체", "개발 및 학습", "취업 및 채용", "취미 및 여가", "기타"]; -export { CATEGORY_LIST, QUESTION_TYPE_LIST, CATEGORY_FORUM_LIST }; +const INITIAL_FORM: FormState = { + form: { + id: "example", + userId: 3, + title: "", + description: "", + category: "기타", + acceptResponse: false, + onBoard: false, + loginRequired: false, + responseModifiable: false, + currentQuestionId: 1, + }, + question: [], +}; + +export { CATEGORY_LIST, QUESTION_TYPE_LIST, CATEGORY_FORUM_LIST, INITIAL_FORM };