diff --git a/src/apis/httpClient/httpClient.ts b/src/apis/httpClient/httpClient.ts index 69b86fd3..1cc7d72b 100644 --- a/src/apis/httpClient/httpClient.ts +++ b/src/apis/httpClient/httpClient.ts @@ -168,4 +168,5 @@ export default { admin: new HttpClient("api/bamboo/admin", axiosConfig), like: new HttpClient("api/likes/update", axiosConfig), image: new HttpClient("api/image/save", axiosConfig), + meal: new HttpClient("api/meal", axiosConfig), }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a14e64fc..b2f31747 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,16 +1,20 @@ +import Provider from "@/provider/Provider"; + export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} + title: "BSM", + description: "부산소마고 학생 정보 관리 서비스 BSM입니다.", +}; export default function RootLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { return ( - {children} + + {children} + - ) + ); } diff --git a/src/assets/data/emptyMealList.ts b/src/assets/data/emptyMealList.ts new file mode 100644 index 00000000..8bd26429 --- /dev/null +++ b/src/assets/data/emptyMealList.ts @@ -0,0 +1,21 @@ +import { IMealList } from "@/interfaces"; + +const emptyMealList: IMealList = { + data: { + MORNING: { + content: "", + cal: 0, + }, + LUNCH: { + content: "", + cal: 0, + }, + DINNER: { + content: "", + cal: 0, + }, + }, + keys: [], +}; + +export default emptyMealList; diff --git a/src/assets/data/index.ts b/src/assets/data/index.ts index 1bf7607e..f03fb369 100644 --- a/src/assets/data/index.ts +++ b/src/assets/data/index.ts @@ -3,3 +3,4 @@ export { default as emptyClassInfo } from "./emptyClassInfo"; export { default as emptyClassLevel } from "./emptyClassLevel"; export { default as emptyTimetable } from "./emptyTimetable"; export { default as emptyInputPost } from "./emptyInputPost"; +export { default as emptyMealList } from "./emptyMealList"; diff --git a/src/constants/key.constant.ts b/src/constants/key.constant.ts index 7803cfc0..f1d73850 100644 --- a/src/constants/key.constant.ts +++ b/src/constants/key.constant.ts @@ -6,6 +6,7 @@ const KEY = { RECOMMENT: "useRecomment", BAMBOO: "useBamboo", BAMBOO_ADMIN: "useBambooAdmin", + MEAL: "useMeal", } as const; export default KEY; diff --git a/src/helpers/getMealName.helper.ts b/src/helpers/getMealName.helper.ts new file mode 100644 index 00000000..50e2ad54 --- /dev/null +++ b/src/helpers/getMealName.helper.ts @@ -0,0 +1,14 @@ +const getMealName = (meal: string) => { + switch (meal) { + case "MORNING": + return "조식"; + case "LUNCH": + return "중식"; + case "DINNER": + return "석식"; + default: + return meal; + } +}; + +export default getMealName; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 66367f2b..69300062 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -8,3 +8,4 @@ export { default as getWriteContentLabel } from "./getWriteContentLabel.helper"; export { default as checkPostValid } from "./checkPostValid.helper"; export { default as checkTextOverflow } from "./checkTextOverflow.helper"; export { default as getTextDepth } from "./getTextDepth.helper"; +export { default as getMealName } from "./getMealName.helper"; diff --git a/src/hooks/useDate.ts b/src/hooks/useDate.ts index a312d244..eb92ecb2 100644 --- a/src/hooks/useDate.ts +++ b/src/hooks/useDate.ts @@ -59,6 +59,22 @@ const useDate = () => { return formattedDate; }; + const getMealDate = () => { + return dayjs().format("YYMMDD"); + }; + + const getDayOfWeek = (date: string) => { + return dayjs(date, "YYMMDD").locale("ko").format("dddd"); + }; + + const getMealDateTitle = (date: string) => { + return dayjs(date, "YYMMDD").locale("ko").format("M월 D일 dddd"); + }; + + const setMealDate = (date: string, day: number) => { + return dayjs(date, "YYMMDD").add(day, "day").format("YYMMDD"); + }; + const getNowWeekDay = ({ type }: DateType) => { const today = dayjs().day(); @@ -107,6 +123,10 @@ const useDate = () => { formatDate, getHMSDate, getDate, + getDayOfWeek, + getMealDate, + getMealDateTitle, + setMealDate, getNowWeekDay, translateDay, getDiffDayTime, diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 98e27126..abee9059 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -3,7 +3,7 @@ export type { default as IClassInfo } from "./classInfo.interface"; export type { default as IClassLevel } from "./classLevel.interface"; export type { default as IEmojiState } from "./emoji.interface"; export type { default as IMealList } from "./mealList.interface"; -export type { default as IMealListItem } from "./mealListItem.interface"; +export type { default as IMeal } from "./meal.interface"; export type { default as IModalState } from "./modal.interface"; export type { default as IPost } from "./post.interface"; export type { default as IPostQuery } from "./postQuery.interface"; diff --git a/src/interfaces/meal.interface.ts b/src/interfaces/meal.interface.ts new file mode 100644 index 00000000..1248eae3 --- /dev/null +++ b/src/interfaces/meal.interface.ts @@ -0,0 +1,4 @@ +export default interface IMeal { + content: string; + cal: number; +} diff --git a/src/interfaces/mealList.interface.ts b/src/interfaces/mealList.interface.ts index 321e63e0..6e1e2966 100644 --- a/src/interfaces/mealList.interface.ts +++ b/src/interfaces/mealList.interface.ts @@ -1,3 +1,8 @@ +import IMeal from "./meal.interface"; + export default interface MealListType { - mealList: Array; + data: { + [meal: string]: IMeal; + }; + keys: Array; } diff --git a/src/interfaces/mealListItem.interface.ts b/src/interfaces/mealListItem.interface.ts deleted file mode 100644 index f4e5793e..00000000 --- a/src/interfaces/mealListItem.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface MealListItemsType { - key: number; - date: number; - menu: Array; - currentSlideIndex: number; -} diff --git a/src/page/meal/index.tsx b/src/page/meal/index.tsx index 719d8d79..97c920d2 100644 --- a/src/page/meal/index.tsx +++ b/src/page/meal/index.tsx @@ -1,18 +1,69 @@ +import React from "react"; import styled from "styled-components"; -import { Aside } from "@/components/common"; +import { emptyMealList } from "@/assets/data"; +import { IMealList } from "@/interfaces"; +import { color, flex, font } from "@/styles"; import { Column } from "@/components/Flex"; -import { font } from "@/styles"; -import MealSlider from "./layouts/MealSlider"; +import useDate from "@/hooks/useDate"; +import MealListItem from "./layouts/MealListItem"; +import { useMealQuery } from "./services/query.service"; const MealPage = () => { + const { getMealDate, getDayOfWeek, setMealDate, getMealDateTitle } = + useDate(); + const [currentDate, setCurrentDate] = React.useState(getMealDate()); + const [mealList, setMealList] = React.useState(emptyMealList); + const { refetch } = useMealQuery({ date: currentDate }); + + React.useEffect(() => { + const handleSetDateKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowLeft") + setCurrentDate((p) => { + if (getDayOfWeek(p) === "월요일") return setMealDate(p, -3); + return setMealDate(p, -1); + }); + if (e.key === "ArrowRight") + setCurrentDate((p) => { + if (getDayOfWeek(p) === "금요일") return setMealDate(p, 3); + return setMealDate(p, 1); + }); + }; + + window.addEventListener("keydown", handleSetDateKeyDown); + return () => { + window.removeEventListener("keydown", handleSetDateKeyDown); + }; + // eslint-disable-next-line + }, []); + + React.useEffect(() => { + refetch().then(({ data }) => { + if (data.keys) return setMealList(data); + return setMealList(emptyMealList); + }); + // eslint-disable-next-line + }, [currentDate]); + return ( - + - <MealSlider /> + <Description /> </Column> - <Aside /> + <MealDate>{getMealDateTitle(currentDate)}</MealDate> + <MealList> + {mealList.keys.map((mealName) => ( + <MealListItem + key={mealName} + mealName={mealName} + meal={mealList.data[mealName]} + /> + ))} + {!mealList.keys.length && ( + <NoMealText>{getMealDateTitle(currentDate)}</NoMealText> + )} + </MealList> </Container> </Layout> ); @@ -26,7 +77,8 @@ const Layout = styled.div` const Container = styled.div` width: 76%; - display: flex; + ${flex.COLUMN}; + gap: 12px; `; const Title = styled.span` @@ -36,4 +88,37 @@ const Title = styled.span` } `; +const Description = styled.span` + color: ${color.gray}; + &:after { + content: "좌우 화살표 방향키를 탭해 날짜를 조정해보세요."; + } +`; + +const MealList = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + height: 60vh; + gap: 30px; +`; + +const MealDate = styled.span` + width: 100%; + ${flex.CENTER}; + ${font.H4}; +`; + +const NoMealText = styled.span` + width: 100%; + ${flex.CENTER}; + ${font.p2}; + color: ${color.gray}; + + &:after { + content: "에 등록된 급식이 없어요."; + } +`; + export default MealPage; diff --git a/src/page/meal/layouts/BlinkerBox.tsx b/src/page/meal/layouts/BlinkerBox.tsx new file mode 100644 index 00000000..762f4ee9 --- /dev/null +++ b/src/page/meal/layouts/BlinkerBox.tsx @@ -0,0 +1,39 @@ +import { color, flex } from "@/styles"; +import React from "react"; +import styled from "styled-components"; + +const BlinkerBox = () => { + return ( + <Container> + <Red /> + <Yellow /> + <Green /> + </Container> + ); +}; + +const Container = styled.div` + ${flex.VERTICAL}; + gap: 6px; + margin-right: auto; +`; + +const Circle = styled.div` + width: 12px; + height: 12px; + border-radius: 25px; +`; + +const Red = styled(Circle)` + background-color: ${color.primary_red}; +`; + +const Yellow = styled(Circle)` + background-color: ${color.primary_yellow}; +`; + +const Green = styled(Circle)` + background-color: ${color.primary_mint}; +`; + +export default BlinkerBox; diff --git a/src/page/meal/layouts/MealListItem.tsx b/src/page/meal/layouts/MealListItem.tsx index 36784729..f15b3726 100644 --- a/src/page/meal/layouts/MealListItem.tsx +++ b/src/page/meal/layouts/MealListItem.tsx @@ -1,56 +1,89 @@ -import { Row } from "@/components/Flex"; -import { color } from "@/styles"; import React from "react"; import styled from "styled-components"; +import { color, flex, font } from "@/styles"; +import { Row } from "@/components/Flex"; +import { getMealName } from "@/helpers"; +import BlinkerBox from "./BlinkerBox"; + +interface IMealListItemProps { + mealName: string; + meal: { + content: string; + cal: number; + }; +} -const MealListItem = () => { +const MealListItem = ({ mealName, meal }: IMealListItemProps) => { return ( <Container> <MealHeader> - <Row gap="8px" alignItems="center"> - <Red /> - <Yellow /> - <Green /> + <BlinkerBox /> + <Row gap="4px"> + <MealTime>{getMealName(mealName)}</MealTime> + <MealCal>{meal?.cal}</MealCal> </Row> </MealHeader> + <MealBody> + <MealContent>{meal?.content}</MealContent> + </MealBody> </Container> ); }; const Container = styled.div` width: 100%; - height: 68vh; + height: 56vh; border-radius: 12px; background-color: ${color.white}; - margin: 10px 0; + box-shadow: 4px 4px 15px 0 rgba(0, 0, 0, 0.15); + margin: 16px 0; + transition: ease-in-out; + transition-duration: 0.2s; + + &:hover { + width: 110%; + height: 58vh; + transition: ease-in-out; + transition-duration: 0.2s; + } `; const MealHeader = styled.div` width: 100%; - height: 9%; + height: 32px; padding: 0 16px; border-radius: 12px 12px 0 0; background-color: ${color.meal_header}; - display: flex; - align-items: center; + ${flex.CENTER} + position: relative; `; -const Circle = styled.div` - width: 12px; - height: 12px; - border-radius: 25px; +const MealTime = styled.span` + ${font.p3}; `; -const Red = styled(Circle)` - background-color: ${color.primary_red}; +const MealCal = styled(MealTime)` + &:before { + content: "· "; + } + + &:after { + content: "kcal"; + } `; -const Yellow = styled(Circle)` - background-color: ${color.primary_yellow}; +const MealBody = styled.div` + width: 100%; + height: 100%; + ${flex.CENTER}; `; -const Green = styled(Circle)` - background-color: ${color.primary_mint}; +const MealContent = styled.p` + ${font.p1}; + line-height: 180%; + font-weight: 500; + white-space: pre; + margin: 14px 0 auto 0; `; export default MealListItem; diff --git a/src/page/meal/layouts/MealSlider.tsx b/src/page/meal/layouts/MealSlider.tsx deleted file mode 100644 index 8dfde907..00000000 --- a/src/page/meal/layouts/MealSlider.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import "slick-carousel/slick/slick.css"; -import "slick-carousel/slick/slick-theme.css"; -import styled from "styled-components"; - -const MealSlider = () => { - return ( - <Container> - <MealList> - {/* <Slider {...settings}> - {Array.from({ length: 10 }).map((_, i) => ( - <MealListItem key={i} /> - ))} - </Slider> */} - </MealList> - </Container> - ); -}; - -const Container = styled.div` - width: 100%; -`; - -const MealList = styled.div` - width: 100%; - height: 68vh; - overflow: hidden; -`; - -export default MealSlider; diff --git a/src/page/meal/services/api.service.ts b/src/page/meal/services/api.service.ts new file mode 100644 index 00000000..b35a8174 --- /dev/null +++ b/src/page/meal/services/api.service.ts @@ -0,0 +1,6 @@ +import httpClient from "@/apis/httpClient"; + +export const getMealList = async (date: string) => { + const { data } = await httpClient.meal.getById({ params: { id: date } }); + return data; +}; diff --git a/src/page/meal/services/query.service.ts b/src/page/meal/services/query.service.ts new file mode 100644 index 00000000..c8813713 --- /dev/null +++ b/src/page/meal/services/query.service.ts @@ -0,0 +1,12 @@ +import { KEY } from "@/constants"; +import { useQuery } from "@tanstack/react-query"; +import { getMealList } from "./api.service"; + +interface IUseMealQueryProps { + date: string; +} + +export const useMealQuery = ({ date }: IUseMealQueryProps) => { + const { data, ...queryRest } = useQuery([KEY.MEAL], () => getMealList(date)); + return { data, ...queryRest }; +}; diff --git a/src/provider/ApolloClientProvider.tsx b/src/provider/ApolloClientProvider.tsx new file mode 100644 index 00000000..6ee75228 --- /dev/null +++ b/src/provider/ApolloClientProvider.tsx @@ -0,0 +1,18 @@ +import { getToken } from "@/helpers"; +import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client"; +import { PropsWithChildren } from "react"; + +const client = new ApolloClient({ + uri: `${process.env.NEXT_PUBLIC_BASE_URL}api/graphql`, + headers: { + Authorization: getToken(), + }, + cache: new InMemoryCache(), + connectToDevTools: true, +}); + +const ApolloClientProvider = ({ children }: PropsWithChildren) => { + return <ApolloProvider client={client}>{children}</ApolloProvider>; +}; + +export default ApolloClientProvider; diff --git a/src/provider/LayoutProvider.tsx b/src/provider/LayoutProvider.tsx new file mode 100644 index 00000000..c612e43e --- /dev/null +++ b/src/provider/LayoutProvider.tsx @@ -0,0 +1,31 @@ +import { Column } from "@/components/Flex"; +import { Footer, Header, Modal } from "@/components/common"; +import { GlobalStyle } from "@/styles"; +import React from "react"; +import { ToastContainer, toast } from "react-toastify"; +import styled from "styled-components"; +import "react-toastify/dist/ReactToastify.css"; + +const LayoutProvider = ({ children }: React.PropsWithChildren) => { + return ( + <> + <StyledToastify autoClose={1000} position={toast.POSITION.TOP_RIGHT} /> + <GlobalStyle /> + <Modal /> + <Column gap="6vh"> + <Header /> + {children} + <Footer /> + </Column> + </> + ); +}; + +const StyledToastify = styled(ToastContainer)` + .Toastify__toast { + color: black; + font-size: 14px; + } +`; + +export default LayoutProvider; diff --git a/src/provider/Provider.tsx b/src/provider/Provider.tsx new file mode 100644 index 00000000..923382b8 --- /dev/null +++ b/src/provider/Provider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import React from "react"; +import { RecoilRoot } from "recoil"; +import ReactQueryProvider from "./ReactQueryProvider"; +import LayoutProvider from "./LayoutProvider"; +import ApolloClientProvider from "./ApolloClientProvider"; + +const Provider = ({ children }: React.PropsWithChildren) => { + return ( + <ReactQueryProvider> + <ApolloClientProvider> + <RecoilRoot> + <LayoutProvider>{children}</LayoutProvider> + </RecoilRoot> + </ApolloClientProvider> + </ReactQueryProvider> + ); +}; + +export default Provider; diff --git a/src/provider/ReactQueryProvider.tsx b/src/provider/ReactQueryProvider.tsx new file mode 100644 index 00000000..7d3e8257 --- /dev/null +++ b/src/provider/ReactQueryProvider.tsx @@ -0,0 +1,25 @@ +"use client"; + +import throwAxiosError from "@/apis/error/throwAxiosError"; +import React from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + suspense: false, + enabled: true, + retry: 0, + onError: (err) => throwAxiosError(err), + }, + }, +}); + +const ReactQueryProvider = ({ children }: React.PropsWithChildren) => { + return ( + <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> + ); +}; + +export default ReactQueryProvider;