@@ -40,31 +48,20 @@ const GoalRoomList = () => { } 개

- - {(selectedOption) => { - setSortedOption(selectedOption); - return ( - - - - {goalRoomFilter['1']} - - - {goalRoomFilter['2']} - - - - ); - }} - +
- - {goalRoomList - .filter((goalRoomInfo) => goalRoomInfo.status === 'RECRUITING') - .map((goalRoomInfo) => ( + {RecruitingGoalRoomList.length ? ( + + {RecruitingGoalRoomList.map((goalRoomInfo) => ( ))} - + + ) : ( + +
현재 모집중인 모임이 존재하지 않아요
+
모임을 생성해서 목표 달성을 함께 할 동료들을 모집 해 보세요!
+
+ )} {hasNext && } diff --git a/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts b/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts index 87d93d3ff..68e1e7d6f 100644 --- a/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts +++ b/client/src/components/goalRoomListPage/goalRoomList/goalRoomList.styles.ts @@ -144,3 +144,20 @@ export const FilterOption = styled.li` background-color: ${({ theme }) => theme.colors.main_dark}; `; + +export const NoContent = styled.div` + ${({ theme }) => theme.fonts.h1} + display: flex; + flex-direction: column; + align-items: center; + + margin-top: 8rem; + + line-height: 3rem; + + opacity: 0.6; + + & > div:first-child { + margin-bottom: 2rem; + } +`; diff --git a/client/src/components/icons/svgIcons.tsx b/client/src/components/icons/svgIcons.tsx index 1c766aa6a..6926831c6 100644 --- a/client/src/components/icons/svgIcons.tsx +++ b/client/src/components/icons/svgIcons.tsx @@ -2503,3 +2503,48 @@ export const NoImageIcon = ({ width, ...props }: SVGProps) => ( /> ); + +export const SuccessIcon = ({ width, ...props }: SVGProps) => ( + + + +); + +export const ErrorIcon = ({ width, ...props }: SVGProps) => ( + + + +); + +export const WarningIcon = ({ width, ...props }: SVGProps) => ( + + + +); diff --git a/client/src/components/loginPage/loginForm/LoginForm.tsx b/client/src/components/loginPage/loginForm/LoginForm.tsx index f985ed794..6885f14a0 100644 --- a/client/src/components/loginPage/loginForm/LoginForm.tsx +++ b/client/src/components/loginPage/loginForm/LoginForm.tsx @@ -1,23 +1,15 @@ import SVGIcon from '@components/icons/SVGIcon'; import { UserLoginRequest } from '@myTypes/user/remote'; import { useLogin } from '@hooks/queries/user'; -import useFormInput from '@hooks/_common/useFormInput'; import * as S from './LoginForm.styles'; +import { useForm } from 'react-lightweight-form'; const LoginForm = () => { - const { - formState: loginData, - handleInputChange, - handleSubmit, - } = useFormInput({ - identifier: '', - password: '', - }); - + const { register, handleSubmit } = useForm(); const { login } = useLogin(); - const onSubmit = () => { - login(loginData); + const onSubmit = (formData: UserLoginRequest) => { + login(formData); }; return ( @@ -25,13 +17,12 @@ const LoginForm = () => { - + diff --git a/client/src/components/mainPage/MainPage.tsx b/client/src/components/mainPage/MainPage.tsx deleted file mode 100644 index 090b88d27..000000000 --- a/client/src/components/mainPage/MainPage.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - DialogBackdrop, - DialogBox, - DialogContent, - DialogTrigger, -} from '../_common/dialog/dialog'; -import styled from 'styled-components'; - -const BackDrop = styled.div` - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - - background-color: black; -`; - -const Trigger = styled.div` - width: 1rem; - height: 1rem; - background-color: black; -`; - -const MainPage = () => { - return ( - // 모달을 사용하고싶은 곳에서 최상위로 꼭 DialogBox를 감싸줘야합니다 - - {/* 눌렀을 때 모달이 열고 닫히는 trigger버튼이에요. 골룸에서는 전체보기 or 크게보기 버틴이 되겠죠? asChild 속성을 준 후에, 스타일링하고싶은 컴포넌트 꼭 1개만 자식으로 줘야해요 */} - - {/* 이렇게 커스텀 된 trigger버튼 1개만!! */} - - - {/* 모달 뜨면 뒤에 생기는 배경입니다! 이것도 asChild 속성을 준 후에 스타일링하고싶은 컴포넌트 꼭 1개만 자식으로 줘야합니다. */} - - {/* 이렇게 커스텀 된 backdreop 1개만!! */} - - - {/* 모달의 내용물이 들어가는 부분입니다. 이부분은 asChild 속성을 주면 안되고, children을 줘서 마음대로 모달 내용물을 넣어주면 됩니다~ */} - -
모달 내용물~
-
-
- ); -}; - -export default MainPage; diff --git a/client/src/components/roadmapCreatePage/category/Category.tsx b/client/src/components/roadmapCreatePage/category/Category.tsx index 7b8efca2e..9ea30272c 100644 --- a/client/src/components/roadmapCreatePage/category/Category.tsx +++ b/client/src/components/roadmapCreatePage/category/Category.tsx @@ -2,49 +2,46 @@ import { CategoriesInfo } from '@/constants/roadmap/category'; import { useSelect } from '@/hooks/_common/useSelect'; import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType'; import { useEffect } from 'react'; -import { Select, SelectBox } from '../selector/SelectBox'; +import { Select } from 'ck-util-components'; import { S } from './category.styles'; -// 임시 더미데이터 -export type DummyCategoryType = { - [key: number]: string; -}; - type CategoryProps = { - getSelectedCategoryId: (category: keyof DummyCategoryType | null) => void; + getSelectedCategoryId: (category: keyof typeof CategoriesInfo) => void; }; const Category = ({ getSelectedCategoryId }: CategoryProps) => { - const { selectOption, selectedOption } = useSelect(); + const { selectOption, selectedOption } = useSelect(); useEffect(() => { - getSelectedCategoryId(selectedOption); + if (selectedOption !== null) { + getSelectedCategoryId(selectedOption); + } }, [selectedOption]); return ( - - - - 카테고리

*

-
-
- - - 컨텐츠에 어울리는 카테고리를 선택해주세요. - - + ); }; diff --git a/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx b/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx index c5b07ce0a..4e61e00c4 100644 --- a/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx +++ b/client/src/components/roadmapCreatePage/difficulty/Difficulty.tsx @@ -1,9 +1,13 @@ /* eslint-disable react/no-unused-prop-types */ import { useSelect } from '@/hooks/_common/useSelect'; import { DifficultiesType, DifficultyKeyType } from '@/myTypes/roadmap/internal'; -import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType'; +import { + getInvariantObjectKeys, + getInvariantObjectValues, + invariantOf, +} from '@/utils/_common/invariantType'; import { useEffect } from 'react'; -import { Select, SelectBox } from '../selector/SelectBox'; +import { Select } from 'ck-util-components'; import * as S from './difficulty.styles'; const Difficulties: DifficultiesType = { @@ -19,54 +23,42 @@ type DifficultyProps = { }; const Difficulty = ({ getSelectedDifficulty }: DifficultyProps) => { - const { selectOption, selectedOption } = useSelect(); + const { selectOption, selectedOption } = useSelect(); useEffect(() => { if (selectedOption === null) return; - getSelectedDifficulty( - getInvariantObjectKeys(invariantOf(Difficulties))[selectedOption] - ); + getSelectedDifficulty(selectedOption); }, [selectedOption]); return ( - - - - 난이도

*

-
-
- - - 컨텐츠의 달성 난이도를 선택해주세요 - - + ); }; diff --git a/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts b/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts index 7d6684b0b..7089b6992 100644 --- a/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts +++ b/client/src/components/roadmapCreatePage/difficulty/difficulty.styles.ts @@ -34,6 +34,12 @@ export const Wrapper = styled.ul` `; export const TriggerButton = styled.button` + cursor: pointer; + + display: flex; + align-items: center; + justify-content: center; + width: 15.4rem; height: 4rem; diff --git a/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx b/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx index 2c11169b6..ccfc66a3a 100644 --- a/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx +++ b/client/src/components/roadmapCreatePage/roadmapCreateForm/RoadmapCreateForm.tsx @@ -1,5 +1,4 @@ import { useCollectRoadmapData } from '@/hooks/roadmap/useCollectRoadmapData'; -import React, { createContext, PropsWithChildren, useRef } from 'react'; import Category from '../category/Category'; import Description from '../description/Description'; import Difficulty from '../difficulty/Difficulty'; @@ -11,18 +10,6 @@ import Tag from '../tag/Tag'; import Title from '../title/Title'; import * as S from './roadmapCreateForm.styles'; -// ref공유를 위한 context - 다음 브랜치에서 파일 옮길 예정 -const FormRefContext = createContext<{ ref: React.MutableRefObject | null }>({ - ref: null, -}); - -const RefProvider = ({ children }: PropsWithChildren) => { - const ref = useRef(); - - return {children}; -}; -// - const RoadmapCreateForm = () => { const { roadmapValue, @@ -36,7 +23,7 @@ const RoadmapCreateForm = () => { } = useCollectRoadmapData(); return ( - + <>

로드맵

을 생성해주세요
@@ -75,7 +62,7 @@ const RoadmapCreateForm = () => { 로드맵 생성완료 -
+ ); }; diff --git a/client/src/components/roadmapCreatePage/selector/SelectBox.tsx b/client/src/components/roadmapCreatePage/selector/SelectBox.tsx deleted file mode 100644 index d204ff88d..000000000 --- a/client/src/components/roadmapCreatePage/selector/SelectBox.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { cloneElement, PropsWithChildren, ReactElement, useEffect } from 'react'; -import { useContextScope } from '@/hooks/_common/useContextScope'; -import { combineStates, getCustomElement } from '@/hooks/_common/compound'; -import { - DescriptionProps, - externalStateType, - IndicatorProps, - LabelProps, - OptionGroupProps, - OptionProps, - SelectBoxProps, - SelectContextType, - TriggerProps, - ValueProps, -} from '@/myTypes/_common/select'; -import { useSelect } from '@/hooks/_common/useSelect'; -import { SelectContext } from '@/context/selectContext'; -import { S } from './selectBox.styles'; - -// select컴포넌트가 context를 공유할 수 있게 하는 provider컴포넌트 -export const SelectBox = ( - props: PropsWithChildren> -) => { - const { children, defaultOpen, externalSelectState } = props; - const { - selectedOption: selectedId, - selectOption: innerSelectState, - isSelecBoxOpen, - toggleBoxOpen, - } = useSelect(defaultOpen); - const selectOption = combineStates(externalSelectState, innerSelectState); - - return ( - - {children} - - ); -}; - -// select컴포넌트의 라벨 -export const Label = (props: PropsWithChildren) => { - const { asChild = false, children, ...restProps } = props; - - if (asChild) { - return getCustomElement(children as ReactElement, { ...restProps }); - } - return {children}; -}; - -// select컴포넌트에 대한 설명 -export const Description = (props: PropsWithChildren) => { - const { asChild = false, children, ...restProps } = props; - - if (asChild) { - return getCustomElement(children as ReactElement, { ...restProps }); - } - return {children}; -}; - -// 클릭하면 selectBox를 보여줄 수 있는 trigger 버튼 -export const Trigger = (props: PropsWithChildren) => { - const { asChild = false, children, ...restProps } = props; - const { toggleBoxOpen } = useContextScope(SelectContext); - - if (asChild) { - return getCustomElement(children as ReactElement, { - ...restProps, - onClick: (e: React.MouseEvent) => { - e.preventDefault(); - toggleBoxOpen(); - }, - }); - } - return ( - ) => { - e.preventDefault(); - toggleBoxOpen(); - }} - > - {children} - - ); -}; - -export const Value = (props: ValueProps) => { - const { asChild = false, children, ...restProps } = props; - console.log(restProps); - const { selectedId } = useContextScope(SelectContext); - return cloneElement(children({ selectedId })); -}; - -// Option들을 담는 컨테이너 컴포넌트 -export const OptionGroup = (props: PropsWithChildren) => { - const { asChild = false, children, ...restProps } = props; - const { isSelecBoxOpen } = useContextScope(SelectContext); - - if (asChild) { - return isSelecBoxOpen - ? getCustomElement(children as ReactElement, { ...restProps }) - : null; - } - return isSelecBoxOpen ? {children} : null; -}; - -// Option이 선택되었는지 나타내는 indicator -export const Indicator = (props: PropsWithChildren) => { - const { asChild = false, children, ...restProps } = props; - const { selectedId } = useContextScope(SelectContext); - const isSelected = restProps.id === selectedId; - - if (asChild) { - return getCustomElement(children as ReactElement, { ...restProps, isSelected }); - } - return {children}; -}; - -// select의 각 Option -export const Option = (props: PropsWithChildren) => { - const { asChild = false, children, ...restProps } = props; - const { selectOption, selectedId, toggleBoxOpen } = - useContextScope(SelectContext); - const isSelected = restProps.id === selectedId; - - useEffect(() => { - if (restProps.defaultSelected) { - selectOption(restProps.id); - } - }, []); - - if (asChild) { - return getCustomElement(children as ReactElement, { - ...restProps, - isSelected, - onClick: (e: React.MouseEvent) => { - e.preventDefault(); - selectOption(restProps.id); - if (!restProps.defaultOpen) { - toggleBoxOpen(); - } - }, - }); - } - return ( - ) => { - e.preventDefault(); - selectOption(restProps.id); - }} - > - {children} - - ); -}; - -export const Select = Object.assign(SelectBox, { - Label, - Description, - Trigger, - Value, - OptionGroup, - Indicator, - Option, -}); diff --git a/client/src/components/roadmapCreatePage/selector/selectBox.styles.ts b/client/src/components/roadmapCreatePage/selector/selectBox.styles.ts deleted file mode 100644 index 6fbc512d3..000000000 --- a/client/src/components/roadmapCreatePage/selector/selectBox.styles.ts +++ /dev/null @@ -1,37 +0,0 @@ -import styled from 'styled-components'; - -const DefaultLabel = styled.div` - ${({ theme }) => theme.fonts.title_large} -`; - -const DefaultDescription = styled.div` - ${({ theme }) => theme.fonts.description5} -`; - -const DefaultTrigger = styled.div` - width: 2rem; - height: 2rem; -`; - -const DefaultIndicator = styled.div<{ isSelected: boolean }>` - width: 0.2rem; - height: 0.2rem; -`; - -const DefaultOptionGroup = styled.div` - width: 2rem; -`; - -const DefaultOption = styled.div<{ isSelected: boolean }>` - width: 8rem; - height: 2rem; -`; - -export const S = { - DefaultLabel, - DefaultDescription, - DefaultTrigger, - DefaultIndicator, - DefaultOptionGroup, - DefaultOption, -}; diff --git a/client/src/components/roadmapListPage/categories/Categories.tsx b/client/src/components/roadmapListPage/categories/Categories.tsx index 666bae3da..156cff2f0 100644 --- a/client/src/components/roadmapListPage/categories/Categories.tsx +++ b/client/src/components/roadmapListPage/categories/Categories.tsx @@ -1,4 +1,3 @@ -import { MouseEvent } from 'react'; import type { CategoryType, SelectedCategoryId } from '@myTypes/roadmap/internal'; import { CategoriesInfo } from '@constants/roadmap/category'; import SVGIcon from '@components/icons/SVGIcon'; @@ -7,16 +6,14 @@ import { useNavigate } from 'react-router-dom'; type CategoriesProps = { selectedCategoryId: SelectedCategoryId; - selectCategory: ({ currentTarget }: MouseEvent) => void; }; -const Categories = ({ selectedCategoryId, selectCategory }: CategoriesProps) => { +const Categories = ({ selectedCategoryId }: CategoriesProps) => { const categories = Object.values(CategoriesInfo); const upCategories = categories.slice(0, 5); const downCategories = categories.slice(5); const navigate = useNavigate(); - console.log(selectCategory); const handleClickCategory = (categoryId: CategoryType['id']) => { const queryParams = new URLSearchParams(); diff --git a/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx b/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx index 25c677bdb..ff9b7ca46 100644 --- a/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx +++ b/client/src/components/roadmapListPage/roadmapList/RoadmapList.tsx @@ -1,17 +1,12 @@ import { useRoadmapList } from '@hooks/queries/roadmap'; import RoadmapItem from '@components/_common/roadmapItem/RoadmapItem'; import * as S from './RoadmapList.styles'; -import { SelectedCategoryId } from '@myTypes/roadmap/internal'; import { useLocation, useNavigate } from 'react-router-dom'; import { useInfiniteScroll } from '@hooks/_common/useInfiniteScroll'; import WavyLoading from '@/components/_common/wavyLoading/WavyLoading'; import NoResult from '@components/roadmapListPage/roadmapSearch/NoResult'; -type RoadmapListProps = { - selectedCategoryId: SelectedCategoryId; -}; - -const RoadmapList = ({ selectedCategoryId }: RoadmapListProps) => { +const RoadmapList = () => { const location = useLocation(); const queryParams = new URLSearchParams(location.search); const categoryId = queryParams.get('category'); @@ -30,7 +25,7 @@ const RoadmapList = ({ selectedCategoryId }: RoadmapListProps) => { const moveRoadmapCreatePage = () => { navigate('/roadmap-create'); }; - console.log(selectedCategoryId); + return ( {!roadmapListResponse.responses.length && } diff --git a/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx b/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx index 874e32db5..c8d3b2672 100644 --- a/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx +++ b/client/src/components/roadmapListPage/roadmapListView/RoadmapListView.tsx @@ -1,18 +1,15 @@ -import { Suspense } from 'react'; import Categories from '../categories/Categories'; - import * as S from './RoadmapListView.styles'; import { useSelectCategory } from '@/hooks/roadmap/useSelectCategory'; import RoadmapList from '../roadmapList/RoadmapList'; -import Spinner from '@components/_common/spinner/Spinner'; import RoadmapSearch from '../roadmapSearch/RoadmapSearch'; -// import { Select } from '@/components/roadmapCreatePage/selector/SelectBox'; import { Link, Outlet } from 'react-router-dom'; import useValidParams from '@/hooks/_common/useValidParams'; import SVGIcon from '@/components/icons/SVGIcon'; +import AsyncBoundary from '@/components/_common/errorBoundary/AsyncBoundary'; const RoadmapListView = () => { - const [selectedCategoryId, selectCategory] = useSelectCategory(); + const [selectedCategoryId] = useSelectCategory(); const { search } = useValidParams(); return ( @@ -23,21 +20,12 @@ const RoadmapListView = () => { - + - }> + - {!search && ( - - )} - + {!search && } + ); }; diff --git a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx index 1bded6fbb..b0cceeb2b 100644 --- a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx +++ b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearch.tsx @@ -1,15 +1,10 @@ import { SearchIcon } from '@/components/icons/svgIcons'; -import { Select } from '@/components/roadmapCreatePage/selector/SelectBox'; +import { getInvariantObjectKeys, invariantOf } from '@/utils/_common/invariantType'; +import { Select } from 'ck-util-components'; import { FormEvent, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import * as S from './roadmapSearch.styles'; -const searchCategoryKeyword = { - 1: 'tagName', - 2: 'roadmapTitle', - 3: 'creatorName', -} as const; - const searchCategorySelection = { tagName: '태그', roadmapTitle: '로드맵 제목', @@ -22,12 +17,15 @@ const RoadmapSearch = () => { const [searchCategory, setSearchCategory] = useState< 'tagName' | 'roadmapTitle' | 'creatorName' >('roadmapTitle'); + const [categoryOpen, setCategoryOpen] = useState(false); + + const selectSearchCategory = (option: keyof typeof searchCategorySelection) => { + setSearchCategory(option); + }; - const selectSearchCategory = (id: number) => { - // eslint-disable-next-line no-prototype-builtins - if (searchCategory.hasOwnProperty(id)) { - setSearchCategory(searchCategoryKeyword[id as keyof typeof searchCategoryKeyword]); - } + const toggleSearchCategory = () => { + // eslint-disable-next-line no-unused-expressions + categoryOpen ? setCategoryOpen(false) : setCategoryOpen(true); }; const searchRoadmap = (e: FormEvent) => { @@ -42,7 +40,12 @@ const RoadmapSearch = () => {
) => searchRoadmap(e)}> - @@ -52,15 +55,21 @@ const RoadmapSearch = () => { - - 태그 - - - 로드맵 제목 - - - 크리에이터 - + {getInvariantObjectKeys(invariantOf(searchCategorySelection)).map( + (categ) => { + return ( + toggleSearchCategory()} + asChild + > + + {searchCategorySelection[categ]} + + + ); + } + )} diff --git a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx index 2f81bdc0a..8b7c9b1ba 100644 --- a/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx +++ b/client/src/components/roadmapListPage/roadmapSearch/RoadmapSearchResult.tsx @@ -1,3 +1,4 @@ +import AsyncBoundary from '@/components/_common/errorBoundary/AsyncBoundary'; import RoadmapItem from '@/components/_common/roadmapItem/RoadmapItem'; import WavyLoading from '@/components/_common/wavyLoading/WavyLoading'; import { useSearchRoadmapList } from '@/hooks/queries/roadmap'; @@ -26,14 +27,16 @@ const RoadmapSearchResult = () => { return ( <> - {roadmapList.length === 0 && } - - {roadmapList.map((item) => ( - - ))} - {hasNext && } - + - + + {roadmapList.length === 0 && } + + {roadmapList.map((item) => ( + + ))} + {hasNext && } + + + + ); }; diff --git a/client/src/components/signUpPage/signUpForm/SignUpForm.tsx b/client/src/components/signUpPage/signUpForm/SignUpForm.tsx index d739eeb08..10b209648 100644 --- a/client/src/components/signUpPage/signUpForm/SignUpForm.tsx +++ b/client/src/components/signUpPage/signUpForm/SignUpForm.tsx @@ -4,34 +4,15 @@ import SVGIcon from '@components/icons/SVGIcon'; import logo from '@assets/images/logo.png'; import logoAV from '@assets/images/logo.avif'; import { SingleCardWrapper } from '@components/_common/SingleCard/SingleCard.styles'; -import useFormInput from '@hooks/_common/useFormInput'; import * as S from './SignUpForm.styles'; -import { staticValidations } from './signUpValidations'; +import { useForm } from 'react-lightweight-form'; const SignUpForm = () => { - const { - formState: signUpFormData, - handleInputChange, - handleSubmit, - error, - } = useFormInput( - { - identifier: '', - password: '', - email: '', - nickname: '', - genderType: '', - }, - staticValidations - ); - + const { register, handleSubmit, errors } = useForm(); const { signUp } = useSignUp(); - const onSubmit = () => { - signUp({ - ...signUpFormData, - genderType: signUpFormData.genderType.toUpperCase(), - }); + const onSubmit = (formData: MemberJoinRequest) => { + signUp(formData); }; return ( @@ -44,13 +25,27 @@ const SignUpForm = () => { - + @@ -58,22 +53,43 @@ const SignUpForm = () => { - + - inputValue.toUpperCase(), + })} + > @@ -83,7 +99,7 @@ const SignUpForm = () => { - {Object.values(error).map((message: string) => ( + {Object.values(errors).map((message: string) => (

{message}

))}
diff --git a/client/src/components/signUpPage/signUpForm/signUpValidations.ts b/client/src/components/signUpPage/signUpForm/signUpValidations.ts deleted file mode 100644 index a9c30f8eb..000000000 --- a/client/src/components/signUpPage/signUpForm/signUpValidations.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ValidationsType } from '@hooks/_common/useFormInput'; - -import { EMAIL, GENDER, IDENTIFIER, NICKNAME, PASSWORD } from '@constants/user/regex'; - -export const staticValidations: ValidationsType = { - identifier: (inputValue) => { - if (!IDENTIFIER.rule.test(inputValue)) { - return { - ok: false, - message: IDENTIFIER.message, - updateOnFail: true, - }; - } - - return { ok: true }; - }, - - password: (inputValue) => { - if (!PASSWORD.rule.test(inputValue)) { - return { - ok: false, - message: PASSWORD.message, - updateOnFail: true, - }; - } - - return { ok: true }; - }, - - email: (inputValue) => { - if (!EMAIL.rule.test(inputValue)) { - return { - ok: false, - message: EMAIL.message, - updateOnFail: true, - }; - } - - return { ok: true }; - }, - - nickname: (inputValue) => { - if (!NICKNAME.rule.test(inputValue)) { - return { - ok: false, - message: NICKNAME.message, - updateOnFail: true, - }; - } - - return { ok: true }; - }, - - genderType: (inputValue) => { - if (!GENDER.rule.test(inputValue)) { - return { - ok: false, - message: GENDER.message, - updateOnFail: true, - }; - } - - return { ok: true }; - }, -}; diff --git a/client/src/constants/_common/api.ts b/client/src/constants/_common/api.ts new file mode 100644 index 000000000..a70068489 --- /dev/null +++ b/client/src/constants/_common/api.ts @@ -0,0 +1,32 @@ +import { GoalRoomRecruitmentStatus } from '@/myTypes/goalRoom/internal'; + +export const API_PATH = { + GOALROOMS: (roadmapId: number) => `/roadmaps/${roadmapId}/goal-rooms`, + MY_GOALROOMS: (statusCond: GoalRoomRecruitmentStatus) => + `/goal-rooms/me?statusCond=${statusCond}`, + GOALROOM_DETAIL: (goalRoomId: number) => `/goal-rooms/${goalRoomId}`, + GOALROOM_DASHBOARD: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/me`, + CREATE_GOALROOM: `/goal-rooms`, + GOALROOM_TODOS: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/todos`, + CHANGE_TODO_CHECKS: (goalRoomId: string, todoId: string) => + `/goal-rooms/${goalRoomId}/todos/${todoId}`, + CREATE_TODO: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/todos`, + CREATE_FEED: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/checkFeeds`, + JOIN_GOALROOM: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/join`, + GOALROOM_PARTICIPANTS: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/members`, + GOALROOM_FEEDS: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/checkFeeds`, + START_GOALROOM: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/start`, + GOALROOM_NODE_LIST: (goalRoomId: string) => `/goal-rooms/${goalRoomId}/nodes`, + + ROADMAPS: '/roadmaps', + ROADMAP_SEARCH: '/roadmaps/search', + ROADMAP_DETAIL: (roadmapId: number) => `/roadmaps/${roadmapId}`, + CREATE_ROADMAP: '/roadmaps', + MY_ROADMAPS: '/roadmaps/me', + + SIGN_UP: '/members/join', + NAVER_LOGIN_REDIRECT: '/auth/oauth/naver', + NAVER_TOKEN: '/auth/login/oauth', + LOGIN: '/auth/login', + USER_INFO: '/members/me', +} as const; diff --git a/client/src/constants/_common/toast.tsx b/client/src/constants/_common/toast.tsx new file mode 100644 index 000000000..81c8aea3b --- /dev/null +++ b/client/src/constants/_common/toast.tsx @@ -0,0 +1,119 @@ +import SVGIcon from '@/components/icons/SVGIcon'; + +export const TOAST_CONTENTS = { + CREATE_GOALROOM: { + success: { + message: '모임을 생성했습니다!', + indicator: , + }, + error: { + message: '모임을 생성하지 못했습니다 😭', + indicator: , + }, + }, + CREATE_TODO: { + success: { + message: '새로운 투두리스트가 등록되었습니다.', + indicator: , + }, + error: { + message: '투두리스트 등록에 실패했습니다 😭', + indicator: , + }, + }, + CHECK_TODO: { + success: { + message: '투두리스트 상태 변경 완료!', + indicator: , + }, + error: { + message: '다시한번 시도해주세요 😭', + indicator: , + }, + }, + CREATE_FEED: { + success: { + message: '인증 피드가 등록되었습니다.', + indicator: , + }, + error: { + message: '인증피드 등록에 실패했습니다 😭', + indicator: , + }, + }, + JOIN_GOALROOM: { + success: { + message: '모임에 참여하였습니다!', + indicator: , + }, + error: { + message: '모임 참여에 실패했습니다 😭', + indicator: , + }, + }, + START_GOALROOM: { + success: { + message: '모임이 시작되었습니다.', + indicator: , + }, + error: { + message: '모임 시작에 실패했습니다 😭', + indicator: , + }, + }, + CREATE_ROADMAP: { + success: { + message: '로드맵이 생성되었습니다!', + indicator: , + }, + error: { + message: '로드맵을 생성하지 못했습니다 😭', + indicator: , + }, + }, + SIGN_UP: { + success: { + message: '회원가입 성공!', + indicator: , + }, + error: { + message: '오류가 발생했습니다 😭', + indicator: , + }, + }, + LOGIN: { + success: { + message: '로그인 성공!', + indicator: , + }, + error: { + message: '존재하지 않는 계정입니다.', + indicator: , + }, + }, + LOGOUT: { + success: { + message: '로그아웃 성공!', + indicator: , + }, + error: { + message: '오류가 발생했습니다 😭', + indicator: , + }, + }, + PRIVATE_PAGE: { + success: { + message: '로그인이 필요한 서비스입니다.', + indicator: , + }, + error: { + message: '오류가 발생했습니다 😭', + indicator: , + }, + }, +}; + +export const NETWORK_ERROR = { + message: '네트워크 연결에 문제가 발생했습니다 🚨', + indicator: , +}; diff --git a/client/src/constants/roadmap/category.ts b/client/src/constants/roadmap/category.ts index cc93773e0..c60c2b875 100644 --- a/client/src/constants/roadmap/category.ts +++ b/client/src/constants/roadmap/category.ts @@ -1,9 +1,4 @@ export const CategoriesInfo = { - // 0: { - // id: 0, - // name: '전체', - // iconName: 'AllCategoryIcon', - // }, 1: { id: 1, name: '어학', diff --git a/client/src/constants/user/regex.ts b/client/src/constants/user/regex.ts deleted file mode 100644 index b977610b9..000000000 --- a/client/src/constants/user/regex.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const IDENTIFIER = { - rule: /^[a-z0-9]{4,20}$/, - message: '-아이디는 영어 소문자와 숫자만 포함할 수 있으며, 4~20자여야 합니다.', -}; - -export const PASSWORD = { - rule: /^[a-z0-9!@#$%^&*()~]{8,15}$/, - message: - '-비밀번호는 8~15자리여야 하며, 영어 소문자, 숫자, [!,@,#,$,%,^,&,*,(,),~] 특수문자만 포함해야 합니다.', -}; - -export const EMAIL = { - rule: /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i, - message: '-유효하지 않은 이메일 형식입니다.', -}; - -export const NICKNAME = { - rule: /^.{2,8}$/, - message: '-닉네임은 2~8자리여야 합니다.', -}; - -export const GENDER = { - rule: /^(male|female)$/, - message: "-성별은 '남자' 또는 '여자'만 선택 가능합니다.", -}; diff --git a/client/src/constants/user/signUpValidation.ts b/client/src/constants/user/signUpValidation.ts deleted file mode 100644 index 13975785c..000000000 --- a/client/src/constants/user/signUpValidation.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const ERROR_MESSAGE = { - IDENTIFIER_REQUIRED: '이메일은 필수 항목입니다', - - PASSWORD_REQUIRED: '비밀번호는 필수 항목입니다', - - NICKNAME_REQUIRED: '닉네임은 필수 항목입니다', - - GENDER_TYPE_REQUIRED: '성별은 필수 항목입니다', -}; diff --git a/client/src/context/dialogContext.ts b/client/src/context/dialogContext.ts deleted file mode 100644 index cc9eb0251..000000000 --- a/client/src/context/dialogContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DialogContextType } from '@/myTypes/_common/dialog'; -import { createContext } from 'react'; - -export const DialogContext = createContext({ - isOpen: false, - openDialog: () => {}, - closeDialog: () => {}, -}); diff --git a/client/src/context/errorboundaryContext.ts b/client/src/context/errorboundaryContext.ts deleted file mode 100644 index d06b44611..000000000 --- a/client/src/context/errorboundaryContext.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ErrorBoundaryContextType } from '@/myTypes/_common/errorBoundary'; -import { createContext } from 'react'; - -export const ErrorBoundaryContext = createContext(null); diff --git a/client/src/context/selectContext.ts b/client/src/context/selectContext.ts deleted file mode 100644 index f161b0157..000000000 --- a/client/src/context/selectContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from 'react'; -import { SelectContextType } from '@/myTypes/_common/select'; - -export const SelectContext = createContext({ - isSelecBoxOpen: false, - toggleBoxOpen: () => {}, - selectedId: null, - selectOption: (_id: number) => {}, -}); diff --git a/client/src/context/toastContext.ts b/client/src/context/toastContext.ts new file mode 100644 index 000000000..d9f3d853b --- /dev/null +++ b/client/src/context/toastContext.ts @@ -0,0 +1,6 @@ +import { ToastContextType } from '@/myTypes/_common/toast'; +import { createContext } from 'react'; + +export const ToastContext = createContext({ + triggerToast: () => {}, +}); diff --git a/client/src/hooks/_common/useToast.ts b/client/src/hooks/_common/useToast.ts index b3a721e29..45083198c 100644 --- a/client/src/hooks/_common/useToast.ts +++ b/client/src/hooks/_common/useToast.ts @@ -1,4 +1,4 @@ -import { ToastContext } from '@components/_common/toastProvider/ToastProvider'; +import { ToastContext } from '@/context/toastContext'; import { useContext } from 'react'; const useToast = () => { diff --git a/client/src/hooks/queries/goalRoom.ts b/client/src/hooks/queries/goalRoom.ts index 3a781465e..420040b12 100644 --- a/client/src/hooks/queries/goalRoom.ts +++ b/client/src/hooks/queries/goalRoom.ts @@ -23,11 +23,11 @@ import { getGoalRoomNodeList, } from '@apis/goalRoom'; import { useSuspendedQuery } from '@hooks/queries/useSuspendedQuery'; -import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import useToast from '@hooks/_common/useToast'; +import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { GoalRoomRecruitmentStatus } from '@myTypes/goalRoom/internal'; import { useNavigate } from 'react-router-dom'; import QUERY_KEYS from '@constants/@queryKeys/queryKeys'; +import { useMutationWithKey } from './useMutationWithKey'; export const useGoalRoomList = (params: GoalRoomListRequest) => { const { roadmapId } = params; @@ -66,31 +66,28 @@ export const useGoalRoomDetail = (goalRoomId: number) => { }; export const useFetchGoalRoom = (goalRoomId: string) => { - const { data: goalRoomResponse } = useSuspendedQuery( - [QUERY_KEYS.goalRoom.dashboard, goalRoomId], - () => getGoalRoomDashboard(goalRoomId) + const { data } = useSuspendedQuery([QUERY_KEYS.goalRoom.dashboard, goalRoomId], () => + getGoalRoomDashboard(goalRoomId) ); return { - goalRoom: goalRoomResponse, + goalRoom: data, }; }; export const useCreateGoalRoom = (roadmapId: number) => { const queryClient = useQueryClient(); - const navigate = useNavigate(); - const { triggerToast } = useToast(); - const { mutate } = useMutation( + + const { mutate } = useMutationWithKey( + 'CREATE_GOALROOM', (body: CreateGoalRoomRequest) => postCreateGoalRoom(body), { - async onSuccess() { + onSuccess: async () => { + navigate(`/roadmap/${roadmapId}/goalroom-list`); await queryClient.refetchQueries([QUERY_KEYS.goalRoom.list, roadmapId]); await queryClient.refetchQueries([QUERY_KEYS.goalRoom.my, roadmapId]); - navigate(`/roadmap/${roadmapId}/goalroom-list`); - triggerToast({ message: '모임을 생성했습니다!' }); }, - onError() {}, } ); @@ -101,16 +98,14 @@ export const useCreateGoalRoom = (roadmapId: number) => { export const useCreateTodo = (goalRoomId: string) => { const queryClient = useQueryClient(); - const { triggerToast } = useToast(); - const { mutate } = useMutation( + const { mutate } = useMutationWithKey( + 'CREATE_TODO', (body: newTodoPayload) => postCreateNewTodo(goalRoomId, body), { onSuccess() { queryClient.invalidateQueries([QUERY_KEYS.goalRoom.dashboard, goalRoomId]); queryClient.invalidateQueries([QUERY_KEYS.goalRoom.todos, goalRoomId]); - - triggerToast({ message: '새로운 투두리스트가 등록되었습니다.' }); }, } ); @@ -135,13 +130,12 @@ export const usePostChangeTodoCheckStatus = ({ todoId, }: GoalRoomTodoChangeStatusRequest) => { const queryClient = useQueryClient(); - const { triggerToast } = useToast(); - const { mutate } = useMutation( + const { mutate } = useMutationWithKey( + 'CHECK_TODO', () => postToChangeTodoCheckStatus({ goalRoomId, todoId }), { onSuccess() { - triggerToast({ message: '투두리스트 상태 변경 완료!' }); queryClient.invalidateQueries([QUERY_KEYS.goalRoom.dashboard, goalRoomId]); queryClient.invalidateQueries([QUERY_KEYS.goalRoom.todos, goalRoomId]); }, @@ -157,15 +151,14 @@ export const useCreateCertificationFeed = ( goalRoomId: string, onSuccessCallbackFunc: () => void ) => { - const { triggerToast } = useToast(); const queryClient = useQueryClient(); - const { mutate } = useMutation( + const { mutate } = useMutationWithKey( + 'CREATE_FEED', (formData: FormData) => postCreateNewCertificationFeed(goalRoomId, formData), { onSuccess() { queryClient.invalidateQueries([QUERY_KEYS.goalRoom.dashboard, goalRoomId]); - triggerToast({ message: '인증 피드가 등록되었습니다' }); queryClient.invalidateQueries([ QUERY_KEYS.goalRoom.certificationFeeds, goalRoomId, @@ -182,16 +175,18 @@ export const useCreateCertificationFeed = ( export const useJoinGoalRoom = ({ goalRoomId }: JoinGoalRoomRequest) => { const navigate = useNavigate(); - const { triggerToast } = useToast(); const queryClient = useQueryClient(); - const { mutate } = useMutation(() => postJoinGoalRoom(goalRoomId), { - onSuccess() { - navigate(`/goalroom-dashboard/${goalRoomId}`); - triggerToast({ message: '모임에 참여하였습니다!' }); - queryClient.invalidateQueries([QUERY_KEYS.goalRoom.detail, goalRoomId]); - }, - }); + const { mutate } = useMutationWithKey( + 'JOIN_GOALROOM', + () => postJoinGoalRoom(goalRoomId), + { + onSuccess() { + navigate(`/goalroom-dashboard/${goalRoomId}`); + queryClient.invalidateQueries([QUERY_KEYS.goalRoom.detail, goalRoomId]); + }, + } + ); return { joinGoalRoom: mutate, @@ -223,15 +218,17 @@ export const useCertificationFeeds = (goalRoomId: string) => { }; export const useStartGoalRoom = (goalRoomId: string) => { - const { triggerToast } = useToast(); const queryClient = useQueryClient(); - const { mutate } = useMutation(() => postStartGoalRoom(goalRoomId), { - onSuccess() { - triggerToast({ message: '모임이 시작되었습니다' }); - queryClient.invalidateQueries([QUERY_KEYS.goalRoom.dashboard, goalRoomId]); - }, - }); + const { mutate } = useMutationWithKey( + 'START_GOALROOM', + () => postStartGoalRoom(goalRoomId), + { + onSuccess() { + queryClient.invalidateQueries([QUERY_KEYS.goalRoom.dashboard, goalRoomId]); + }, + } + ); return { startGoalRoom: mutate, diff --git a/client/src/hooks/queries/roadmap.ts b/client/src/hooks/queries/roadmap.ts index 683fd4aa4..eaa092c56 100644 --- a/client/src/hooks/queries/roadmap.ts +++ b/client/src/hooks/queries/roadmap.ts @@ -8,8 +8,9 @@ import { } from '@apis/roadmap'; import QUERY_KEYS from '@constants/@queryKeys/queryKeys'; import { useSuspendedQuery } from '@hooks/queries/useSuspendedQuery'; -import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; +import { useMutationWithKey } from './useMutationWithKey'; export const useRoadmapList = ({ categoryId, @@ -79,12 +80,16 @@ export const useCreateRoadmap = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); - const { mutate } = useMutation((formData: FormData) => postCreateRoadmap(formData), { - async onSuccess() { - await queryClient.refetchQueries([QUERY_KEYS.roadmap.list]); - navigate('/roadmap-list'); - }, - }); + const { mutate } = useMutationWithKey( + 'CREATE_ROADMAP', + (formData: FormData) => postCreateRoadmap(formData), + { + async onSuccess() { + await queryClient.refetchQueries([QUERY_KEYS.roadmap.list]); + navigate('/roadmap-list'); + }, + } + ); return { createRoadmap: mutate, diff --git a/client/src/hooks/queries/useMutationWithKey.ts b/client/src/hooks/queries/useMutationWithKey.ts new file mode 100644 index 000000000..b3efb30ea --- /dev/null +++ b/client/src/hooks/queries/useMutationWithKey.ts @@ -0,0 +1,67 @@ +import { NETWORK_ERROR, TOAST_CONTENTS } from '@/constants/_common/toast'; +import { MutationFunction, useMutation, UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import useToast from '../_common/useToast'; + +export const useMutationWithKey = < + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown +>( + key: keyof typeof TOAST_CONTENTS, + mutationFn: MutationFunction, + options?: Omit, 'mutationFn'> +) => { + const { triggerToast } = useToast(); + const { mutate, ...restProps } = useMutation( + mutationFn, + { + ...options, + + onSuccess: (...args) => { + triggerToast({ + message: TOAST_CONTENTS[key].success.message, + indicator: TOAST_CONTENTS[key].success.indicator, + }); + options?.onSuccess?.(...args); + }, + + onMutate: () => { + if (!window.navigator.onLine) { + triggerToast({ + message: NETWORK_ERROR.message, + indicator: NETWORK_ERROR.indicator, + isError: true, + }); + } + + return undefined; + }, + + onError: (e, ...arg) => { + // TODO : custom error을 통해 에러 분류 + + if (e instanceof AxiosError && e.response?.status === 400) { + triggerToast({ + message: e.response?.data[0].message, + indicator: TOAST_CONTENTS[key].error.indicator, + isError: true, + }); + } else { + triggerToast({ + message: TOAST_CONTENTS[key].error.message, + indicator: TOAST_CONTENTS[key].error.indicator, + isError: true, + }); + } + options?.onError?.(e, ...arg); + }, + } + ); + + return { + mutate, + ...restProps, + }; +}; diff --git a/client/src/hooks/queries/user.ts b/client/src/hooks/queries/user.ts index b01274b9c..357b234ad 100644 --- a/client/src/hooks/queries/user.ts +++ b/client/src/hooks/queries/user.ts @@ -20,16 +20,17 @@ import { AxiosResponse } from 'axios'; import { useNavigate } from 'react-router-dom'; import { setCookie } from '@/utils/_common/cookies'; import logout from '@utils/user/logout'; +import { useMutationWithKey } from './useMutationWithKey'; +import { TOAST_CONTENTS } from '@/constants/_common/toast'; export const useSignUp = () => { - const { triggerToast } = useToast(); const navigate = useNavigate(); - const { mutate } = useMutation( + const { mutate } = useMutationWithKey( + 'SIGN_UP', (memberJoinPayload: MemberJoinRequest) => signUp(memberJoinPayload), { onSuccess() { - triggerToast({ message: '회원가입 성공!' }); navigate('/login'); }, } @@ -53,14 +54,12 @@ export const useNaverLogin = () => { export const useNaverOAuth = (code: string, state: string) => { const navigate = useNavigate(); - const { triggerToast } = useToast(); const { setUserInfo } = useUserInfoContext(); - const { mutate } = useMutation(() => naverOAuthToken(code, state), { + const { mutate } = useMutationWithKey('LOGIN', () => naverOAuthToken(code, state), { onSuccess: ({ accessToken, refreshToken }) => { setCookie('access_token', accessToken); setCookie('refresh_token', refreshToken); - triggerToast({ message: '로그인 성공!' }); getUserInfo().then((response: AxiosResponse) => { setUserInfo(response.data); @@ -75,17 +74,16 @@ export const useNaverOAuth = (code: string, state: string) => { export const useLogin = () => { const navigate = useNavigate(); - const { triggerToast } = useToast(); const { setUserInfo } = useUserInfoContext(); - const { mutate } = useMutation( + const { mutate } = useMutationWithKey( + 'LOGIN', (loginPayload: UserLoginRequest) => login(loginPayload), { onSuccess(response) { const { accessToken, refreshToken } = response.data; setCookie('access_token', accessToken); setCookie('refresh_token', refreshToken); - triggerToast({ message: '로그인 성공!' }); getUserInfo().then((response: AxiosResponse) => { setUserInfo(response.data); @@ -93,9 +91,6 @@ export const useLogin = () => { navigate('/roadmap-list'); }, - onError() { - triggerToast({ message: '존재하지 않는 계정입니다', isError: true }); - }, } ); @@ -111,7 +106,10 @@ export const useLogout = () => { const triggerLogout = () => { logout(); setUserInfo(defaultUserInfo); - triggerToast({ message: '로그아웃 성공!' }); + triggerToast({ + message: TOAST_CONTENTS.LOGOUT.success.message, + indicator: TOAST_CONTENTS.LOGOUT.success.indicator, + }); }; return { logout: triggerLogout }; diff --git a/client/src/hooks/roadmap/useCollectRoadmapData.ts b/client/src/hooks/roadmap/useCollectRoadmapData.ts index d5b1658f1..9505a6d4f 100644 --- a/client/src/hooks/roadmap/useCollectRoadmapData.ts +++ b/client/src/hooks/roadmap/useCollectRoadmapData.ts @@ -1,4 +1,4 @@ -import { DummyCategoryType } from '@/components/roadmapCreatePage/category/Category'; +import { CategoriesInfo } from '@/constants/roadmap/category'; import { DifficultyKeyType, NodeImagesType, @@ -23,10 +23,10 @@ export const useCollectRoadmapData = () => { const [isSumbited, setIsSubmited] = useState(false); const { createRoadmap } = useCreateRoadmap(); - const getSelectedCategoryId = (category: keyof DummyCategoryType | null) => { + const getSelectedCategoryId = (category: keyof typeof CategoriesInfo) => { setRoadmapValue((prev) => ({ ...prev, - categoryId: category, + categoryId: CategoriesInfo[category].id, })); }; diff --git a/client/src/index.tsx b/client/src/index.tsx index 745163503..261b06a7a 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -22,16 +22,6 @@ const startApp = async () => { staleTime: 1000 * 60 * 5, cacheTime: 1000 * 60 * 30, }, - - mutations: { - useErrorBoundary: false, - onError: (e) => { - const error = e as any; - if (error.response.status === 400) { - alert(error.response.data[0].message); - } - }, - }, }, }); diff --git a/client/src/myTypes/_common/errorBoundary.ts b/client/src/myTypes/_common/errorBoundary.ts index 5e3dc5abf..21b6bf33b 100644 --- a/client/src/myTypes/_common/errorBoundary.ts +++ b/client/src/myTypes/_common/errorBoundary.ts @@ -15,14 +15,22 @@ export type FallbackProps = { resetErrorBoundary: (...args: any[]) => void; }; -type ErrorBoundarySharedProps = PropsWithChildren<{ +export type ErrorBoundarySharedProps = PropsWithChildren<{ onError?: (error: Error, info: ErrorInfo) => void; onReset?: ( details: - | { reason: 'imperative-api'; args: any[] } - | { reason: 'keys'; prev: any[] | undefined; next: any[] | undefined } + | { + reason: 'keys'; + prev: unknown[] | undefined; + next: unknown[] | undefined; + } + | { + reason: 'keys'; + args: unknown[]; + } ) => void; - resetKeys?: any[]; + resetKeys?: unknown[]; + isCritical?: boolean; }>; export type ErrorBoundaryPropsWithComponent = ErrorBoundarySharedProps & { @@ -34,7 +42,7 @@ export type ErrorBoundaryPropsWithComponent = ErrorBoundarySharedProps & { export type ErrorBoundaryPropsWithRender = ErrorBoundarySharedProps & { fallback?: never; FallbackComponent?: never; - fallbackRender: typeof FallbackRender; + fallbackRender?: typeof FallbackRender; }; export type ErrorBoundaryPropsWithFallback = ErrorBoundarySharedProps & { @@ -54,7 +62,9 @@ export type ErrorBoundaryContextType = { resetErrorBoundary: (...args: any[]) => void; }; -export type ErrorBoundaryState = +export type ErrorBoundaryState = { + fallback?: ReactNode | null; +} & ( | { didCatch: true; error: any; @@ -62,4 +72,5 @@ export type ErrorBoundaryState = | { didCatch: false; error: null; - }; + } +); diff --git a/client/src/myTypes/_common/toast.ts b/client/src/myTypes/_common/toast.ts index a6b3a2038..612e20e0b 100644 --- a/client/src/myTypes/_common/toast.ts +++ b/client/src/myTypes/_common/toast.ts @@ -1,5 +1,6 @@ export type ToastContainerProps = { message: string; + indicator: JSX.Element | null; isError?: boolean; isShow?: boolean | null; onClickToast?: () => void; diff --git a/client/src/myTypes/roadmap/internal.ts b/client/src/myTypes/roadmap/internal.ts index 833f88e3c..c40030197 100644 --- a/client/src/myTypes/roadmap/internal.ts +++ b/client/src/myTypes/roadmap/internal.ts @@ -71,7 +71,7 @@ export type RoadmapNodes = { }; export type RoadmapValueType = { - categoryId: null | number; + categoryId: number | null; title: null | string; introduction: null | string; content: null | string; diff --git a/client/src/pages/goalRoomDashboardPage/GoalRoomDashboardPage.tsx b/client/src/pages/goalRoomDashboardPage/GoalRoomDashboardPage.tsx index 996818869..c78eb2bd9 100644 --- a/client/src/pages/goalRoomDashboardPage/GoalRoomDashboardPage.tsx +++ b/client/src/pages/goalRoomDashboardPage/GoalRoomDashboardPage.tsx @@ -1,15 +1,14 @@ import GoalRoomDashboardContent from '@components/goalRoomDahsboardPage/goalRoomDashboardContent/GoalRoomDashboardContent'; import GoalRoomDashboardProvider from '@components/_providers/GoalRoomDashboardProvider'; -import { Suspense } from 'react'; -import Spinner from '@components/_common/spinner/Spinner'; +import AsyncBoundary from '@/components/_common/errorBoundary/AsyncBoundary'; const GoalRoomDashboardPage = () => { return ( - }> + - + ); }; diff --git a/client/src/pages/goalRoomListPage/GoalRoomListPage.tsx b/client/src/pages/goalRoomListPage/GoalRoomListPage.tsx index 2b1acbe88..4dccefe69 100644 --- a/client/src/pages/goalRoomListPage/GoalRoomListPage.tsx +++ b/client/src/pages/goalRoomListPage/GoalRoomListPage.tsx @@ -1,14 +1,13 @@ import GoalRoomList from '@/components/goalRoomListPage/goalRoomList/GoalRoomList'; -import { Suspense } from 'react'; import * as S from './goalRoomListPage.styles'; -import Spinner from '@components/_common/spinner/Spinner'; +import AsyncBoundary from '@/components/_common/errorBoundary/AsyncBoundary'; const GoalRoomListPage = () => { return ( - }> + - + ); }; diff --git a/client/src/pages/roadmapDetailPage/RoadmapDetailPage.tsx b/client/src/pages/roadmapDetailPage/RoadmapDetailPage.tsx index c99117a6a..7336bba9f 100644 --- a/client/src/pages/roadmapDetailPage/RoadmapDetailPage.tsx +++ b/client/src/pages/roadmapDetailPage/RoadmapDetailPage.tsx @@ -1,12 +1,11 @@ -import { Suspense } from 'react'; import RoadmapDetail from '@components/roadmapDetailPage/roadmapDetail/RoadmapDetail'; -import Spinner from '@components/_common/spinner/Spinner'; +import AsyncBoundary from '@/components/_common/errorBoundary/AsyncBoundary'; const RoadmapDetailPage = () => { return ( - }> + - + ); }; diff --git a/client/src/utils/_common/invariantType.ts b/client/src/utils/_common/invariantType.ts index 7a59183fc..3238c1e3f 100644 --- a/client/src/utils/_common/invariantType.ts +++ b/client/src/utils/_common/invariantType.ts @@ -23,3 +23,13 @@ export function invariantOf(value: T): InvariantOf { export function getInvariantObjectKeys(arg: InvariantOf): (keyof T)[] { return Object.keys(arg) as unknown as (keyof T)[]; } + +export function getInvariantObjectValues(arg: InvariantOf): T[keyof T][] { + return Object.values(arg) as unknown as T[keyof T][]; +} + +export function getInvariantObjectEntries( + arg: InvariantOf +): [keyof T, T[keyof T]][] { + return Object.entries(arg) as unknown as [keyof T, T[keyof T]][]; +} diff --git a/client/tsconfig.json b/client/tsconfig.json index 7a424ced2..90145f077 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,18 +1,15 @@ { "compilerOptions": { - "target": "es5", + "target": "ES6", "baseUrl": "./", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "strict": true, - "forceConsistentCasingInFileNames": true, + "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, @@ -36,10 +33,11 @@ "@styles/*": ["src/styles/*"], "@myTypes/*": ["src/myTypes/*"], "@utils/*": ["src/utils/*"], - "@icons/*": ["src/icons/*"], + "@icons/*": ["src/icons/*"] } }, - "include": [ - "./src" - ] + "paths": { + "ck-util-components/*": ["node_modules/ck-util-components/dist/lib/*"] + }, + "include": ["./src"] } diff --git a/client/webpack.common.js b/client/webpack.common.js index 0844bcd8b..f405eaaff 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -32,6 +32,10 @@ module.exports = { '@styles': path.resolve(__dirname, 'src/styles'), '@myTypes': path.resolve(__dirname, 'src/myTypes'), '@utils': path.resolve(__dirname, 'src/utils'), + 'ck-util-components': path.resolve( + __dirname, + 'node_modules/ck-util-components/dist/lib' + ), }, },