diff --git a/src/App.tsx b/src/App.tsx index 675b8b5..66e1a75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,45 @@ +import ErrorBoundary from '@components/common/errorBoundary'; +import Error from '@components/errorFallback'; import ModalProvider from '@components/ui/modal/modal-provider'; +import { Spinner } from '@components/ui/spinner/indext'; +import { Toaster } from '@components/ui/toast/toaster'; +import { useResetError } from '@hooks/useResetErrorBoundary'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React from 'react'; +import React, { Suspense, lazy } from 'react'; import { BrowserRouter } from 'react-router-dom'; -import Router from './Router'; +import Router from '@/Router'; +const DefaultLayout = lazy(() => import('@components/layouts/DefaultLayout')); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0, refetchOnWindowFocus: false, - throwOnError: true, + throwOnError: true, }, }, }); -export default function App() { +const App = () => { + const { handleErrorReset } = useResetError(); return ( + - + + }> + + + + + ); -} +}; + +export default App; \ No newline at end of file diff --git a/src/Router.tsx b/src/Router.tsx index 8fde523..187b5f2 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,4 +1,4 @@ -import DefaultLayout from '@components/layouts/DefaultLayout'; +import MainSkeleton from '@components/ui/skeleton/main'; import { ROUTES } from '@constants/route'; import NotFound from '@pages/404'; import BusinessBoard from '@pages/business'; @@ -29,44 +29,55 @@ import SignupVerify from '@pages/signup'; import SignupInfo from '@pages/signup/info'; import SignupSuccess from '@pages/signup/success'; import SignupTerms from '@pages/signup/terms'; -import React from 'react'; +import React, { Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; - export default function Router() { return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + }> +
+ + } + /> + } /> + {/* 아이디 및 비밀번호 재설정 */} + } /> + } /> + } /> + } /> + {/* 마이페이지 설정 */} + } /> + } /> + } /> + } /> + {/* 회원가입 */} + } /> + } /> + } /> + } /> + {/* 총학생회 */} + } /> + } /> + } /> + } /> + } /> + } /> + {/* 청원 */} + } /> + } /> + } /> + {/* 총학공지 */} + } /> + } /> + } /> + } /> + {/* 제휴 */} + } /> + } /> + ); } diff --git a/src/components/business/index.tsx b/src/components/business/index.tsx new file mode 100644 index 0000000..4258643 --- /dev/null +++ b/src/components/business/index.tsx @@ -0,0 +1,48 @@ +import Board from '@components/ui/board'; +import IntersectionBox from '@components/ui/box/intersectionBox'; +import ItemList from '@components/ui/item-list'; +import { Spinner } from '@components/ui/spinner/indext'; +import { ROUTES } from '@constants/route'; +import { CoalitionContentResponse, useGetCoalitionList } from '@hooks/api/coalition/useGetCoalitionList'; +import { useInfiniteScroll } from '@hooks/useInfiniteScroll'; +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { CoalitionType } from '@/types/coalition'; + +export default function BusinessList({categoryType}: {categoryType: string}) { + const navigate = useNavigate(); + + const { + data: coalition, + refetch, + fetchNextPage, + isFetchingNextPage, + } = useGetCoalitionList(categoryType as CoalitionType); + const intersectionRef = useInfiniteScroll(fetchNextPage); + + useEffect(() => { + refetch(); + }, [categoryType, refetch]); + + + const goToBusinessDetail = (item: CoalitionContentResponse) => { + navigate(ROUTES.BUSINESS.DETAIL(categoryType.toLowerCase() as string, item.id.toString()), { + state: item, + }); + }; + + return ( + + {coalition?.pages.map((page) => + page.content.map((item) => ( + goToBusinessDetail(item)}> + + + )), + )} + + {isFetchingNextPage && } + + ); +} \ No newline at end of file diff --git a/src/components/common/carousel/index.tsx b/src/components/common/carousel/index.tsx index e1fd730..efe8021 100644 --- a/src/components/common/carousel/index.tsx +++ b/src/components/common/carousel/index.tsx @@ -47,7 +47,7 @@ export default function Carousel({ data, className }: CarouselProps) {
{data.map((_, index) => ( - + ))}
{data.map((item, index) => ( diff --git a/src/components/common/gnb/index.tsx b/src/components/common/gnb/index.tsx index 4c21e50..7ec76d3 100644 --- a/src/components/common/gnb/index.tsx +++ b/src/components/common/gnb/index.tsx @@ -5,11 +5,10 @@ import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; interface Props extends React.ComponentProps<'header'> { - left?: JSX.Element | null; - center?: JSX.Element | null; + children: React.ReactNode; } -export default function Gnb({ left, center, ...props }: Props) { +const Gnb = ({ children, ...props }: Props) => { return (
-
- {left && left} - {center && center} -
+
{children}
); -} +}; -Gnb.Logo = function Logo() { +const GnbLogo = () => { return ( 단국대학교 로고 @@ -33,11 +29,21 @@ Gnb.Logo = function Logo() { ); }; -Gnb.GoBack = function GoBack() { +const GnbGoBack = ({ url }: { url?: string }) => { const navigate = useNavigate(); - return navigate(-1)} />; + return ( + (url ? navigate(url) : navigate(-1))} + /> + ); }; -Gnb.Title = function Title({ children }: { children: string }) { +const GnbTitle = ({ children }: { children: string }) => { return

{children}

; }; + +export { Gnb, GnbLogo, GnbGoBack, GnbTitle }; diff --git a/src/components/common/gnh/index.tsx b/src/components/common/gnh/index.tsx index ea19bad..219c8aa 100644 --- a/src/components/common/gnh/index.tsx +++ b/src/components/common/gnh/index.tsx @@ -1,22 +1,17 @@ -import Selector, { TOption } from '@components/ui/selector'; import React from 'react'; -interface GnhProps { - headingText: string; - subHeadingText?: string; - subHeadingStyle: string; - headingStyle: string; - dropDown?: TOption[]; -} +const GnhTitle = ({ children, className }: React.ComponentProps<'h1'>) => { + return ( +

{children}

+ ); +}; + +const GnhSubtitle = ({ children, className }: React.ComponentProps<'h2'>) => { + return ( +

{children}

+ ); +}; + + +export { GnhTitle, GnhSubtitle }; -const Gnh = ({ headingText, subHeadingText, headingStyle, subHeadingStyle, dropDown }: GnhProps) => ( - - {headingText &&

{headingText}

} - {dropDown !== undefined && dropDown.length > 0 && subHeadingText ? ( - - ) : ( -

{subHeadingText}

- )} -
-); -export default Gnh; diff --git a/src/components/conference/index.tsx b/src/components/conference/index.tsx new file mode 100644 index 0000000..83c9242 --- /dev/null +++ b/src/components/conference/index.tsx @@ -0,0 +1,33 @@ +import Board from '@components/ui/board'; +import IntersectionBox from '@components/ui/box/intersectionBox'; +import { Spinner } from '@components/ui/spinner/indext'; +import { Date } from '@components/ui/text/board'; +import { useGetConference } from '@hooks/api/conference/useGetConference'; +import { ConferenceContentResponse } from '@hooks/api/conference/useGetConference'; +import { useInfiniteScroll } from '@hooks/useInfiniteScroll'; +import React from 'react'; + +export default function ConferenceList() { + const { data: conference, fetchNextPage, isFetchingNextPage } = useGetConference(); + const intersectionRef = useInfiniteScroll(fetchNextPage); + + const openFile = (item: ConferenceContentResponse) => { + window.open(item.files[0].url); + }; + return ( + + {conference?.pages.map((page) => + page.content.map((item) => ( + openFile(item)}> +
+

{item.title}

+ +
+
+ )), + )} + + {isFetchingNextPage && } +
+ ); +} \ No newline at end of file diff --git a/src/components/layouts/ContentSection.tsx b/src/components/layouts/ContentSection.tsx new file mode 100644 index 0000000..9a468e8 --- /dev/null +++ b/src/components/layouts/ContentSection.tsx @@ -0,0 +1,23 @@ +import Nav from '@components/common/nav'; +import { AnimatePresence } from 'framer-motion'; +import React from 'react'; + +interface ContentSectionProps extends React.ComponentProps<'div'> { + showNav?: boolean; +} + +const ContentSection = ({ children, className, showNav, ...props }: ContentSectionProps) => { + return ( +
+ {children} + {showNav && ( + +
+ ); +}; + +export default ContentSection; diff --git a/src/components/layouts/DefaultLayout.tsx b/src/components/layouts/DefaultLayout.tsx index c58777a..078f6c4 100644 --- a/src/components/layouts/DefaultLayout.tsx +++ b/src/components/layouts/DefaultLayout.tsx @@ -1,88 +1,18 @@ -import Gnb from '@components/common/gnb'; -import Gnh from '@components/common/gnh'; -import Nav from '@components/common/nav'; import Menu from '@components/main/menu'; -import { Toaster } from '@components/ui/toast/toaster'; -import { bottomNavSize } from '@constants/nav'; -import { useDefaultModal } from '@hooks/useDefaultModal'; -import { useEnrollmentStore } from '@stores/enrollment-store'; -import { gnbState } from '@stores/gnb-store'; -import { gnhState } from '@stores/gnh-store'; import { menuStore } from '@stores/menu-store'; -import { navStore } from '@stores/nav-store'; -import { isLoggedIn } from '@utils/token'; -import { AnimatePresence } from 'framer-motion'; -import React, { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; +import React from 'react'; -import { WithReactChildren } from '@/types/default-interfaces'; - -type DefaultLayoutProps = WithReactChildren & React.HTMLAttributes; - -export default function DefaultLayout({ children, ...props }: DefaultLayoutProps) { - const { title, backButton, isMain } = gnbState(); - const { headingText, subHeadingText, headingStyle, subHeadingStyle, dropDown } = gnhState(); - const { fullscreen, rounded, margin } = navStore(); +const DefaultLayout = ({ children, className, ...props }: React.ComponentProps<'div'>) => { const { menuOpen } = menuStore(); - const defaultStyle = 'w-[390px] mx-auto bg-black'; - - const { enrollment } = useEnrollmentStore(); - const { pathname } = useLocation(); - const { modal } = useDefaultModal(); - - useEffect(() => { - if (pathname.indexOf('/mypage') === -1 && enrollment === false && isLoggedIn) { - setTimeout(() => { - modal({ - content: '회원 정보 업데이트 후 이용 가능합니다.', - target: '/mypage/update', - disableCancle: true, - }); - }, 500); - } - }, [pathname, enrollment]); - return ( -
+
{menuOpen ? ( ) : ( - <> - : isMain ? : null} - center={title ? {title} : null} - /> - {headingText && ( - - )} -
-
- {children} -
-
- - - {!fullscreen && ( - <> -
); -} +}; + +export default DefaultLayout; diff --git a/src/components/layouts/HeaderSection.tsx b/src/components/layouts/HeaderSection.tsx new file mode 100644 index 0000000..c453f9d --- /dev/null +++ b/src/components/layouts/HeaderSection.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const HeaderSection = ({ children, className, ...props }: React.ComponentProps<'div'>) => { + return ( +
+ {children} +
+ ); +}; + +export default HeaderSection; \ No newline at end of file diff --git a/src/components/layouts/MyPageLayout.tsx b/src/components/layouts/MyPageLayout.tsx index 7853b5d..d420eea 100644 --- a/src/components/layouts/MyPageLayout.tsx +++ b/src/components/layouts/MyPageLayout.tsx @@ -3,11 +3,13 @@ import { useApi } from '@hooks/useApi'; import { useEffectOnce } from '@hooks/useEffectOnce'; import { useLayout } from '@hooks/useLayout'; import React from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { Gnb, GnbGoBack } from '../common/gnb'; import SvgIcon from '@/components/common/icon/SvgIcon'; import { IMyInfo } from '@/types/mypage/edit'; - export default function MyPageLayout({ children, getStudentId, @@ -18,6 +20,8 @@ export default function MyPageLayout({ const { setLayout } = useLayout(); const [myInfo, setMyInfo] = React.useState(null); const { get } = useApi(); + const { pathname } = useLocation(); + const navigate = useNavigate(); const fetchMyInfo = async () => { const data = await get(API_PATH.USER.ME, { authenticate: true }); @@ -25,7 +29,7 @@ export default function MyPageLayout({ getStudentId && getStudentId(data.studentId); }; useEffectOnce(() => { - fetchMyInfo(); + localStorage.getItem('damda-atk') !== null ? fetchMyInfo() : navigate('/login'); setLayout({ title: '', backButton: true, @@ -40,19 +44,24 @@ export default function MyPageLayout({ }); return ( -
-
-
- {myInfo?.nickname} -

- {myInfo?.studentId}
- {myInfo?.department} {myInfo?.major} -

+ <> + + + +
+
+
+ {myInfo?.nickname} +

+ {myInfo?.studentId}
+ {myInfo?.department} {myInfo?.major} +

+
+
- + {children}
- {children} -
+ ); } diff --git a/src/components/layouts/index.ts b/src/components/layouts/index.ts new file mode 100644 index 0000000..4fcc49a --- /dev/null +++ b/src/components/layouts/index.ts @@ -0,0 +1,3 @@ +export { default as HeaderSection } from './HeaderSection'; +export { default as ContentSection } from './ContentSection'; +export { default as DefaultLayout } from './DefaultLayout'; diff --git a/src/components/login/form.tsx b/src/components/login/form.tsx index 6af8c59..ad1cfe6 100644 --- a/src/components/login/form.tsx +++ b/src/components/login/form.tsx @@ -4,12 +4,13 @@ import { Input } from '@components/ui/input/index'; import { Label } from '@components/ui/label'; import { ROUTES } from '@constants/route'; import { usePostLogin } from '@hooks/api/auth/usePostLogin'; +import { usePostOAuthLogin } from '@hooks/api/auth/usePostOAuthLogin'; +import { isOAuthFlow } from '@utils/oAuth'; import React from 'react'; import { Link, useSearchParams } from 'react-router-dom'; -import { usePostOAuthLogin } from '@/hooks/api/auth/usePostOAuthLogin'; +import { useAlert } from '@/hooks/useAlert'; import { IdPassword } from '@/types/default-interfaces'; -import { isOAuthFlow } from '@/utils/oAuth'; export default function LoginForm() { const initLoginInfo: IdPassword = { @@ -21,14 +22,18 @@ export default function LoginForm() { const { mutate: login } = usePostLogin(); const { mutate: oAuthLogin } = usePostOAuthLogin(); const [searchParams] = useSearchParams(); - + const { alert } = useAlert(); const handleLogin = (e: React.FormEvent) => { e.preventDefault(); - if (isOAuthFlow(searchParams)) { - oAuthLogin(loginInfo); + if (loginInfo.studentId.length > 0 && loginInfo.password.length > 0) { + if (isOAuthFlow(searchParams)) { + oAuthLogin(loginInfo); + } + login(loginInfo); + } else { + alert('모두 입력해주세요'); } - login(loginInfo); }; return ( diff --git a/src/components/main/post.tsx b/src/components/main/post.tsx index 9a9f263..8cf7027 100644 --- a/src/components/main/post.tsx +++ b/src/components/main/post.tsx @@ -1,13 +1,15 @@ +import { Gnb, GnbGoBack } from '@components/common/gnb'; +import { GnhSubtitle, GnhTitle } from '@components/common/gnh'; +import { ContentSection, HeaderSection } from '@components/layouts'; import PostBox from '@components/ui/box/PostBox'; import FloatingButton from '@components/ui/button/FloatingButton'; import { Textarea } from '@components/ui/textarea'; -import { HEADING_STYLE, HEADING_TEXT } from '@constants/heading'; -import { useEffectOnce } from '@hooks/useEffectOnce'; +import { HEADING_TEXT } from '@constants/heading'; import { ImageProps } from '@hooks/useImageUpload'; -import { useLayout } from '@hooks/useLayout'; import React, { ReactNode } from 'react'; import { FaCamera } from 'react-icons/fa'; + export interface PostFormInfo { title: string; body: string; @@ -35,80 +37,80 @@ export default function Post({ deleteImage, postType, }: PostProps) { - const { setLayout } = useLayout(); - useEffectOnce(() => { - setLayout({ - title: null, - backButton: true, - isMain: false, - fullscreen: false, - headingText: HEADING_TEXT.PETITION.HEAD, - headingStyle: `${HEADING_STYLE.COUNCIL.HEAD} mb-[30px]`, - rounded: true, - }); - }); //TODO) 이미지 리스트 컴포넌트로 분리 //TODO) Input 도 onChange를 useUploadForm 에 작성 return ( -
- -

{`${postType} 제목`}

- ) => { - setFormInfo((prev) => { - return { ...prev, title: e.target.value }; - }); - }} - /> -
- -

{`${postType} 내용`}

-