diff --git a/app/(sub-page)/my/page.tsx b/app/(sub-page)/my/page.tsx index a1bf7f11..5163cb5a 100644 --- a/app/(sub-page)/my/page.tsx +++ b/app/(sub-page)/my/page.tsx @@ -1,9 +1,10 @@ import LectureSearch from '@/app/ui/lecture/lecture-search'; import TakenLecture from '@/app/ui/lecture/taken-lecture'; -import UserInfoNavigator, { UserInfoNavigatorSkeleton } from '@/app/ui/user/user-info-navigator/user-info-navigator'; +import UserInfoNavigator from '@/app/ui/user/user-info-navigator/user-info-navigator'; +import UserInfoNavigatorSkeleton from '@/app/ui/user/user-info-navigator/user-info-navigator.skeleton'; import ContentContainer from '@/app/ui/view/atom/content-container'; import Drawer from '@/app/ui/view/molecule/drawer/drawer'; -import { DIALOG_KEY } from '@/app/utils/key/dialog.key'; +import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util'; import { Suspense } from 'react'; export default function MyPage() { diff --git a/app/(sub-page)/result/components/result-category-detail-container.tsx b/app/(sub-page)/result/components/result-category-detail-container.tsx deleted file mode 100644 index ad596679..00000000 --- a/app/(sub-page)/result/components/result-category-detail-container.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Modal from '../../../ui/view/molecule/modal/modal'; -import { DIALOG_KEY } from '@/app/utils/key/dialog.key'; -import Drawer from '../../../ui/view/molecule/drawer/drawer'; -import ResultCategoryDetailContent from '@/app/ui/result/result-category/result-category-detail-content'; -import Responsive from '@/app/ui/responsive'; -import { fetchResultCategoryDetailInfo } from '@/app/business/result/result.query'; - -async function ResultCategoryDetailContainer() { - const data = await fetchResultCategoryDetailInfo(); - - return ( - <> - - - - - - - - - - - > - ); -} - -export default ResultCategoryDetailContainer; diff --git a/app/(sub-page)/result/layout.tsx b/app/(sub-page)/result/layout.tsx deleted file mode 100644 index 57b427e2..00000000 --- a/app/(sub-page)/result/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import ResultCategoryDetailContainer from './components/result-category-detail-container'; - -interface ResultLayoutProps { - children: React.ReactNode; -} - -function ResultLayout({ children }: ResultLayoutProps) { - return ( - <> - - {children} - > - ); -} - -export default ResultLayout; diff --git a/app/(sub-page)/result/page.tsx b/app/(sub-page)/result/page.tsx index 1c41de4a..6e3946dd 100644 --- a/app/(sub-page)/result/page.tsx +++ b/app/(sub-page)/result/page.tsx @@ -1,10 +1,16 @@ -import ResultCategoryCard from '@/app/ui/result/result-category/result-category-card'; +import ResultCategoryCard from '@/app/ui/result/result-category-card/result-category-card'; import UserInfoCard from '@/app/ui/user/user-info-card/user-info-card'; import ContentContainer from '@/app/ui/view/atom/content-container'; import { cn } from '@/app/utils/shadcn/utils'; -import { RESULT_CATEGORY, ResultCategoryKey } from '@/app/utils/key/result-category.key'; +import { RESULT_CATEGORY } from '@/app/utils/key/result-category.key'; +import ResultCategoryDetail from '@/app/ui/result/result-category-detail/result-category-detail'; -function ResultPage() { +interface ResultPageProp { + searchParams: { category: string }; +} + +function ResultPage({ searchParams }: ResultPageProp) { + const { category } = searchParams; const DUMMY_DATA = { category: 'COMMON_CULTURE' as keyof typeof RESULT_CATEGORY, totalCredit: 70, @@ -32,7 +38,9 @@ function ResultPage() { /> ))} + {category && } ); } + export default ResultPage; diff --git a/app/business/result/result.query.ts b/app/business/result/result.query.ts index de3ccbef..5101fa06 100644 --- a/app/business/result/result.query.ts +++ b/app/business/result/result.query.ts @@ -1,5 +1,7 @@ import { LectureInfo } from '@/app/type/lecture'; import { API_PATH } from '../api-path'; +import { cookies } from 'next/headers'; +import { httpErrorHandler } from '@/app/utils/http/http-error-handler'; export interface ResultCategoryDetailLectures { categoryName: string; @@ -26,8 +28,20 @@ export interface ResultUserInfo { takenCredit: number; } -export const fetchResultCategoryDetailInfo = async (): Promise => { - const response = await fetch(API_PATH.resultCategoryDetailInfo); - const data = await response.json(); - return data; +export const fetchResultCategoryDetailInfo = async (category: string): Promise => { + //FIX : category를 querystring으로 호출하는 건은 mock단계에서는 불필요할 것으로 예상, 실제 api 연결시 변경 예정 + try { + const response = await fetch(`${API_PATH.resultCategoryDetailInfo}`, { + headers: { + Authorization: `Bearer ${cookies().get('accessToken')?.value}`, + }, + }); + const result = await response.json(); + + httpErrorHandler(response, result); + + return result; + } catch (error) { + throw error; + } }; diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 4a31401e..101e3f71 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,7 +1,3 @@ -import RevenueChart from '@/app/ui/invoice/revenu-chart'; -import { RevenueChartSkeleton } from '@/app/ui/skeletons'; -import { Suspense } from 'react'; - export default async function Page() { return ( diff --git a/app/hooks/useDialog.tsx b/app/hooks/useDialog.tsx index abf6263a..5147f610 100644 --- a/app/hooks/useDialog.tsx +++ b/app/hooks/useDialog.tsx @@ -1,20 +1,27 @@ -import { DialogKey } from '../utils/key/dialog.key'; +import { DialogKey } from '../utils/key/dialog-key.util'; import { updateDialogAtom } from '../store/dialog'; import { useAtom } from 'jotai'; -export default function useDialog(key: DialogKey) { +export default function useDialog(key: DialogKey, onClose?: () => void) { const [isOpenDialogList, setOpenDialogList] = useAtom(updateDialogAtom); const isOpen = isOpenDialogList[key]; + const open = () => { + setOpenDialogList([key, true]); + }; + const close = () => { setOpenDialogList([key, false]); + onClose?.(); }; const toggle = () => { const prevState = isOpenDialogList[key]; setOpenDialogList([key, !prevState]); + + if (prevState) onClose?.(); }; - return { isOpen, close, toggle }; + return { isOpen, open, close, toggle }; } diff --git a/app/mocks/handlers/result-handler.mock.ts b/app/mocks/handlers/result-handler.mock.ts index f67aaf07..b25ced53 100644 --- a/app/mocks/handlers/result-handler.mock.ts +++ b/app/mocks/handlers/result-handler.mock.ts @@ -5,7 +5,7 @@ import { mockDatabase } from '../db.mock'; export const resultHandlers = [ http.get(API_PATH.resultCategoryDetailInfo, async () => { const resultCategoryDetailInfo = mockDatabase.getResultCategoryDetailInfo(); - await delay(100); + await delay(4000); return HttpResponse.json(resultCategoryDetailInfo); }), ]; diff --git a/app/store/dialog.ts b/app/store/dialog.ts index 5b554c53..680b9d37 100644 --- a/app/store/dialog.ts +++ b/app/store/dialog.ts @@ -1,5 +1,5 @@ import { atom } from 'jotai'; -import { DIALOG_KEY } from '../utils/key/dialog.key'; +import { DIALOG_KEY } from '../utils/key/dialog-key.util'; const initialState = { [DIALOG_KEY.RESULT_CATEGORY]: false, diff --git a/app/ui/lecture/lecture-search/lecture-search.stories.tsx b/app/ui/lecture/lecture-search/lecture-search.stories.tsx index d9a0e368..9a8c5992 100644 --- a/app/ui/lecture/lecture-search/lecture-search.stories.tsx +++ b/app/ui/lecture/lecture-search/lecture-search.stories.tsx @@ -6,7 +6,7 @@ import { screen } from '@storybook/testing-library'; import TakenLectureLabel from '../taken-lecture/taken-lecture-label'; import TakenLectureList from '../taken-lecture/taken-lecture-list'; import LectureSearch from '.'; -import { DIALOG_KEY } from '@/app/utils/key/dialog.key'; +import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util'; import Drawer from '../../view/molecule/drawer/drawer'; import { delay } from 'msw'; diff --git a/app/ui/lecture/taken-lecture/taken-lecture-label.tsx b/app/ui/lecture/taken-lecture/taken-lecture-label.tsx index 8e593b5d..45f15706 100644 --- a/app/ui/lecture/taken-lecture/taken-lecture-label.tsx +++ b/app/ui/lecture/taken-lecture/taken-lecture-label.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; import Button from '../../view/atom/button/button'; import LabelContainer from '../../view/atom/label-container/label-container'; import useDialog from '@/app/hooks/useDialog'; -import { DIALOG_KEY } from '@/app/utils/key/dialog.key'; +import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util'; export default function TakenLectureLabel() { const { toggle } = useDialog(DIALOG_KEY.LECTURE_SEARCH); diff --git a/app/ui/result/result-category/result-category-card.stories.tsx b/app/ui/result/result-category-card/result-category-card.stories.tsx similarity index 100% rename from app/ui/result/result-category/result-category-card.stories.tsx rename to app/ui/result/result-category-card/result-category-card.stories.tsx diff --git a/app/ui/result/result-category/result-category-card.tsx b/app/ui/result/result-category-card/result-category-card.tsx similarity index 88% rename from app/ui/result/result-category/result-category-card.tsx rename to app/ui/result/result-category-card/result-category-card.tsx index d18b9b8d..1862c034 100644 --- a/app/ui/result/result-category/result-category-card.tsx +++ b/app/ui/result/result-category-card/result-category-card.tsx @@ -5,10 +5,11 @@ import Book from '@/public/assets/book.svg'; import Image from 'next/image'; import useDialog from '@/app/hooks/useDialog'; import * as React from 'react'; -import { DIALOG_KEY } from '@/app/utils/key/dialog.key'; +import { RESULT_CATEGORY, RESULT_CATEGORY_KO, ResultCategoryKey } from '@/app/utils/key/result-category.key'; +import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util'; import PieChart from '../../view/molecule/pie-chart/pie-chart'; import Button from '../../view/atom/button/button'; -import { RESULT_CATEGORY, RESULT_CATEGORY_KO, ResultCategoryKey } from '@/app/utils/key/result-category.key'; +import { useRouter } from 'next/navigation'; interface ResultCategoryCardProps { category: ResultCategoryKey; @@ -34,9 +35,14 @@ const filterSeveralMajor = (category: ResultCategoryKey) => { function ResultCategoryCard({ category, totalCredit, takenCredit }: ResultCategoryCardProps) { const { toggle } = useDialog(DIALOG_KEY.RESULT_CATEGORY); + const { replace } = useRouter(); const percentage = Number(((takenCredit / totalCredit) * 100).toFixed(0)); + function handleClickButton() { + toggle(); + replace('/result?category=COMMON_CULTURE'); + } return ( - + + ); diff --git a/app/ui/result/result-category-detail-content/result-category-detail-content.skeleton.tsx b/app/ui/result/result-category-detail-content/result-category-detail-content.skeleton.tsx new file mode 100644 index 00000000..27f5ea6d --- /dev/null +++ b/app/ui/result/result-category-detail-content/result-category-detail-content.skeleton.tsx @@ -0,0 +1,22 @@ +import LabelContainerSkeleton from '@/app/ui/view/atom/label-container/label-container.skeleton'; +import Skeleton from '@/app/ui/view/atom/skeleton'; + +export default function ResultCategoryDetailContentSkeleton() { + return ( + + + + + + + + + {Array.from({ length: 3 }).map((_, index) => ( + + } /> + + + ))} + + ); +} diff --git a/app/ui/result/result-category-detail-content/result-category-detail-content.stories.tsx b/app/ui/result/result-category-detail-content/result-category-detail-content.stories.tsx new file mode 100644 index 00000000..24d4e5e7 --- /dev/null +++ b/app/ui/result/result-category-detail-content/result-category-detail-content.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent } from '@storybook/test'; +import { mockDatabase } from '@/app/mocks/db.mock'; +import ResultCategoryDetailContent from './result-category-detail-content'; +import { screen } from '@storybook/testing-library'; +import { delay } from 'msw'; + +const meta = { + title: 'ui/result-category/ResultCategoryDetailContent', + component: ResultCategoryDetailContent, + args: { info: mockDatabase.getResultCategoryDetailInfo() }, + decorators: [(Story) => ], +} as Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + play: async ({ step }) => { + await step('미이수 과목을 노출합니다.', async () => { + await delay(2000); + + expect(screen.getByText('영어2')).toBeInTheDocument(); + }); + + await step('토글을 클릭하면 기이수 과목을 노출합니다.', async () => { + const lectureToggle = await screen.findAllByTestId('lecture-toggle'); + await userEvent.click(lectureToggle[0]); + + await delay(3000); + + expect(screen.queryByText('성서와 인간 이해')).not.toBeInTheDocument(); + }); + }, +}; diff --git a/app/ui/result/result-category/result-category-detail-content.tsx b/app/ui/result/result-category-detail-content/result-category-detail-content.tsx similarity index 74% rename from app/ui/result/result-category/result-category-detail-content.tsx rename to app/ui/result/result-category-detail-content/result-category-detail-content.tsx index e341cc31..ec6c41f2 100644 --- a/app/ui/result/result-category/result-category-detail-content.tsx +++ b/app/ui/result/result-category-detail-content/result-category-detail-content.tsx @@ -2,8 +2,8 @@ import { cn } from '@/app/utils/shadcn/utils'; import { useState } from 'react'; -import { ResultCategoryDetailLectureToggle } from './result-category-detail-lecture-toggle'; -import ResultCagegoryDetailLecture from './result-cagegory-detail-lecture'; +import { ResultCategoryDetailLectureToggle } from '../result-category-detail-lecture/result-category-detail-lecture-toggle'; +import ResultCagegoryDetailLecture from '../result-category-detail-lecture/result-cagegory-detail-lecture'; import { ResultCategoryDetailInfo } from '@/app/business/result/result.query'; interface ResultCategoryDetailContentProps { @@ -16,21 +16,22 @@ function ResultCategoryDetailContent({ info }: ResultCategoryDetailContentProps) const [isTakenLecture, setIsTakenLectrue] = useState(false); return ( - + 전공필수 - + 전공필수 과목 중 과목이 표시됩니다. - + {takenCredit} / {totalCredit} diff --git a/app/ui/result/result-category/result-cagegory-detail-lecture.tsx b/app/ui/result/result-category-detail-lecture/result-cagegory-detail-lecture.tsx similarity index 100% rename from app/ui/result/result-category/result-cagegory-detail-lecture.tsx rename to app/ui/result/result-category-detail-lecture/result-cagegory-detail-lecture.tsx diff --git a/app/ui/result/result-category/result-category-detail-lecture-toggle.tsx b/app/ui/result/result-category-detail-lecture/result-category-detail-lecture-toggle.tsx similarity index 100% rename from app/ui/result/result-category/result-category-detail-lecture-toggle.tsx rename to app/ui/result/result-category-detail-lecture/result-category-detail-lecture-toggle.tsx diff --git a/app/ui/result/result-category-detail/result-category-detail-dialog.tsx b/app/ui/result/result-category-detail/result-category-detail-dialog.tsx new file mode 100644 index 00000000..5ca865d7 --- /dev/null +++ b/app/ui/result/result-category-detail/result-category-detail-dialog.tsx @@ -0,0 +1,41 @@ +'use client'; +import Modal from '../../view/molecule/modal/modal'; +import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util'; +import Drawer from '../../view/molecule/drawer/drawer'; +import Responsive from '@/app/ui/responsive'; +import React, { useEffect } from 'react'; +import useDialog from '@/app/hooks/useDialog'; +import { useRouter } from 'next/navigation'; + +interface ResultCategoryDetailDialogProp { + children: React.ReactNode; + querystring?: string; +} + +export default function ResultCategoryDetailDialog({ children, querystring }: ResultCategoryDetailDialogProp) { + const { replace } = useRouter(); + const { isOpen, open } = useDialog(DIALOG_KEY.RESULT_CATEGORY); + + useEffect(() => { + if (querystring && !isOpen) open(); + }, []); + + const handleCloseDialog = () => { + replace('/result'); + }; + + return ( + <> + + + {children} + + + + + {children} + + + > + ); +} diff --git a/app/ui/result/result-category-detail/result-category-detail.tsx b/app/ui/result/result-category-detail/result-category-detail.tsx new file mode 100644 index 00000000..48dca3ca --- /dev/null +++ b/app/ui/result/result-category-detail/result-category-detail.tsx @@ -0,0 +1,20 @@ +import ResultCategoryDetailContent from '@/app/ui/result/result-category-detail-content/result-category-detail-content'; +import { fetchResultCategoryDetailInfo } from '@/app/business/result/result.query'; +import ResultCategoryDetailContentSkeleton from '@/app/ui/result/result-category-detail-content/result-category-detail-content.skeleton'; +import ResultCategoryDetailDialog from './result-category-detail-dialog'; +import { Suspense } from 'react'; + +export default function ResultCategoryDetail({ category }: { category: string }) { + return ( + + }> + + + + ); +} + +async function ResultCategoryDetailInfo({ category }: { category: string }) { + const data = await fetchResultCategoryDetailInfo(category); + return ; +} diff --git a/app/ui/user/user-info-navigator/user-info-navigator.skeleton.tsx b/app/ui/user/user-info-navigator/user-info-navigator.skeleton.tsx new file mode 100644 index 00000000..18c3ccab --- /dev/null +++ b/app/ui/user/user-info-navigator/user-info-navigator.skeleton.tsx @@ -0,0 +1,14 @@ +import Skeleton from '../../view/atom/skeleton'; + +export default function UserInfoNavigatorSkeleton() { + return ( + + + + + + + + + ); +} diff --git a/app/ui/user/user-info-navigator/user-info-navigator.tsx b/app/ui/user/user-info-navigator/user-info-navigator.tsx index e975e6bc..69701268 100644 --- a/app/ui/user/user-info-navigator/user-info-navigator.tsx +++ b/app/ui/user/user-info-navigator/user-info-navigator.tsx @@ -1,7 +1,6 @@ import Avatar from '../../view/atom/avatar/avatar'; import Button from '../../view/atom/button/button'; import { getUserInfo } from '@/app/business/user/user.query'; -import Skeleton from '../../view/atom/skeleton'; export default async function UserInfoNavigator() { const userInfo = await getUserInfo(); @@ -26,16 +25,3 @@ export default async function UserInfoNavigator() { ); } - -export function UserInfoNavigatorSkeleton() { - return ( - - - - - - - - - ); -} diff --git a/app/ui/view/atom/label-container/label-container.skeleton.tsx b/app/ui/view/atom/label-container/label-container.skeleton.tsx new file mode 100644 index 00000000..5a506fcd --- /dev/null +++ b/app/ui/view/atom/label-container/label-container.skeleton.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import Skeleton from '../skeleton'; + +interface LabelContainerProps { + rightElement: ReactNode; +} + +export default function LabelContainerSkeleton({ rightElement }: LabelContainerProps) { + return ( + + + + + {rightElement} + + ); +} diff --git a/app/ui/view/molecule/drawer/drawer.stories.tsx b/app/ui/view/molecule/drawer/drawer.stories.tsx index cec371f7..dfe3cb60 100644 --- a/app/ui/view/molecule/drawer/drawer.stories.tsx +++ b/app/ui/view/molecule/drawer/drawer.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import Drawer from './drawer'; -import { DIALOG_KEY, DialogKey } from '@/app/utils/key/dialog.key'; +import { DIALOG_KEY, DialogKey } from '@/app/utils/key/dialog-key.util'; const meta = { title: 'ui/view/molecule/Drawer', diff --git a/app/ui/view/molecule/drawer/drawer.tsx b/app/ui/view/molecule/drawer/drawer.tsx index 7abdbdc8..b9494403 100644 --- a/app/ui/view/molecule/drawer/drawer.tsx +++ b/app/ui/view/molecule/drawer/drawer.tsx @@ -3,15 +3,17 @@ import * as React from 'react'; import { Drawer as DrawerPrimitive } from 'vaul'; import { cn } from '@/app/utils/shadcn/utils'; -import { DialogKey } from '@/app/utils/key/dialog.key'; +import { DialogKey } from '@/app/utils/key/dialog-key.util'; import useDialog from '@/app/hooks/useDialog'; interface DrawerProps extends React.PropsWithChildren { drawerKey: DialogKey; + onClose?: () => void; + className?: string; } -const Drawer = ({ children, drawerKey }: DrawerProps) => { - const { isOpen, close } = useDialog(drawerKey); +const Drawer = ({ children, drawerKey, onClose, className }: DrawerProps) => { + const { isOpen, close } = useDialog(drawerKey, onClose); return ( @@ -19,10 +21,10 @@ const Drawer = ({ children, drawerKey }: DrawerProps) => { - {children} diff --git a/app/ui/view/molecule/modal/modal.stories.tsx b/app/ui/view/molecule/modal/modal.stories.tsx index df69ac4a..f1ec169d 100644 --- a/app/ui/view/molecule/modal/modal.stories.tsx +++ b/app/ui/view/molecule/modal/modal.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import Modal from './modal'; -import { DIALOG_KEY, DialogKey } from '@/app/utils/key/dialog.key'; +import { DIALOG_KEY, DialogKey } from '@/app/utils/key/dialog-key.util'; const meta = { title: 'ui/view/molecule/Modal', diff --git a/app/ui/view/molecule/modal/modal.tsx b/app/ui/view/molecule/modal/modal.tsx index f2b1f353..91c28dae 100644 --- a/app/ui/view/molecule/modal/modal.tsx +++ b/app/ui/view/molecule/modal/modal.tsx @@ -4,7 +4,7 @@ import { Portal, Overlay, Content, Root } from '@radix-ui/react-dialog'; import { cn } from '../../../../utils/shadcn/utils'; import useDialog from '@/app/hooks/useDialog'; -import { DialogKey } from '@/app/utils/key/dialog.key'; +import { DialogKey } from '@/app/utils/key/dialog-key.util'; const fadeAnimation = 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0'; @@ -13,10 +13,11 @@ const noneSlideAnimation = interface ModalProp extends React.PropsWithChildren { modalKey: DialogKey; + onClose?: () => void; } -const Modal = ({ modalKey, children }: ModalProp) => { - const { isOpen, toggle } = useDialog(modalKey); +const Modal = ({ modalKey, children, onClose }: ModalProp) => { + const { isOpen, toggle } = useDialog(modalKey, onClose); return ( diff --git a/app/utils/key/dialog.key.ts b/app/utils/key/dialog-key.util.ts similarity index 100% rename from app/utils/key/dialog.key.ts rename to app/utils/key/dialog-key.util.ts
+