diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 8c7ccb89..25511939 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -86,7 +86,7 @@ "position": "after" }, { - "pattern": "@Utils/*", + "pattern": "@Utils/**/*", "group": "internal", "position": "after" }, diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 32a8d373..b45732dc 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -23,8 +23,8 @@ export const requestGetMemberListRecord = ( memberId: string, page: number, size: number, - startDate?: string, - endDate?: string, + startDate: string | null, + endDate: string | null, ) => { if (startDate && endDate) return http.get( diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx new file mode 100644 index 00000000..6e42b671 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { STUDY_LIST_9_ARRAY } from 'mocks/mockData'; + +import format from '@Utils/format'; + +import Calendar from './Calendar'; + +type Story = StoryObj; + +/** + * `Calendar`는 일정과 같이 day에 대한 정보를 제공하는 달력 컴포넌트입니다. + */ +const meta: Meta = { + title: 'COMPONENTS/Calendar', + component: Calendar, +}; + +export default meta; + +/** + * `DefaultCalendar`는 현재 년, 월을 렌더링한 기본적인 Calendar의 스토리입니다. + */ +export const DefaultCalendar: Story = { + args: { + year: 2023, + month: 11, + }, +}; + +/** + * `Calendar202309`는 2023년 9월 달력으로 외부에서 데이터를 받는 스토리입니다. + */ +export const Calendar202309: Story = { + args: { + year: 2023, + month: 9, + children: STUDY_LIST_9_ARRAY.map((item, index) => { + return ( + + {item.name} + + ); + }), + }, +}; + +/** + * `LimitCountCalendar202309`는 데이터의 개수가 제한된 스토리입니다. + */ +export const LimitCountCalendar202309: Story = { + args: { + year: 2023, + month: 9, + limit: 3, + onClickRestDataCount: (date) => window.alert(format.date(new Date(date), '-')), + onClickTotalDataCount: (date) => window.alert(format.date(new Date(date), '-')), + children: STUDY_LIST_9_ARRAY.map((item, index) => { + return ( + + {item.name} + + ); + }), + }, +}; + +/** + * `LimitCountCalendar202309`는 데이터의 개수가 제한된 스토리입니다. + */ +export const ClickDayCalendar: Story = { + args: { + year: 2023, + month: 9, + onClickDay: (date) => window.alert(format.date(new Date(date), '-')), + }, +}; diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx new file mode 100644 index 00000000..417c000b --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -0,0 +1,116 @@ +import type { PropsWithChildren } from 'react'; +import { useRef } from 'react'; +import { styled } from 'styled-components'; + +import CalendarProvider from './CalendarContext/CalendarProvider'; +import CalendarItem from './CalendarItem/CalendarItem'; +import ControlBar from './ControlBar/ControlBar'; +import DayList from './DayList/DayList'; +import DayOfWeeks from '../common/DayOfWeeks/DayOfWeeks'; + +type Props = { + /** + * 달력의 년도를 지정하는 속성. + * + */ + year: number; + /** + * 달력의 월을 지정하는 속성. + * + */ + month: number; + /** + * 달력 내 Data 개수를 제한하는 속성. + * + */ + limit?: number; + /** + * 달력에 렌더링 되는 Data 형식이 바뀌는 기준 너비를 지정하는 속성. 지정된 값보다 달력의 너비가 줄어들면 Data의 전체 개수가 렌더링됨. + * + * * @default 750 + */ + formatChangedWidth?: number; + /** + * 달력에 렌더링되는 Data의 로딩 상태를 지정하는 속성 + * + * * @default false + */ + dataLoading?: boolean; + /** + * 달력의 년, 월이 바뀔 때 호출되는 함수. year, month를 매개변수로 받음. + * + */ + onChangeCalendar?: (year: number, month: number) => void; + /** + * 달력의 Day의 클릭할 때 호출되는 함수. 해당 Day의 Date 객체를 매개변수로 받음. + * + */ + onClickDay?: (date: Date) => void; + /** + * 달력에 보여지지 않는 Data의 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. + * + */ + onClickRestDataCount?: (date: Date) => void; + /** + * formatChangedWidth 속성의 값보다 달력의 너비가 줄어들었을 때, 렌덩이 되는 전체 데이터 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. + * + */ + onClickTotalDataCount?: (date: Date) => void; +}; + +const Calendar = ({ + year, + month, + limit, + formatChangedWidth = 750, + children, + dataLoading = false, + onChangeCalendar, + onClickDay, + onClickRestDataCount, + onClickTotalDataCount, +}: PropsWithChildren) => { + const calendarRef = useRef(null); + + return ( + + + + + + + + + + ); +}; + +Calendar.Item = CalendarItem; + +export default Calendar; + +const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 40px; + + user-select: none; +`; + +const CalendarContainer = styled.div` + display: flex; + flex-direction: column; + gap: 5px; +`; diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx new file mode 100644 index 00000000..4c435f20 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -0,0 +1,177 @@ +import type { PropsWithChildren, ReactElement, ReactNode, RefObject } from 'react'; +import { Children, createContext, useContext, useEffect, useState } from 'react'; + +import type { CalendarStorage } from '@Utils/calendar'; +import calendar from '@Utils/calendar'; +import format from '@Utils/format'; + +type CalendarContext = { + year: number; + month: number; + navigationYear: number; + navigationMonth: number; + limit?: number; + calendarStorage: CalendarStorage; + calendarDataFormat: 'long' | 'short'; + dataLoading: boolean; + isToday: (date: Date) => boolean; + shiftMonth: (type: 'next' | 'prev' | 'today') => void; + navigateYear: (year: number) => void; + navigateMonth: (month: number) => void; + navigate: (year?: number, month?: number) => void; + onClickDay?: (date: Date) => void; + onClickRestDataCount?: (date: Date) => void; + onClickTotalDataCount?: (date: Date) => void; +}; + +type Props = { + year: number; + month: number; + limit?: number; + formatChangedWidth: number; + calendarDataChildren: ReactNode; + calendarRef: RefObject; + dataLoading: boolean; + onChangeCalendar?: (year: number, month: number) => void; + onClickDay?: (date: Date) => void; + onClickRestDataCount?: (date: Date) => void; + onClickTotalDataCount?: (date: Date) => void; +}; + +const CalendarContext = createContext(null); + +const CalendarProvider = ({ + year, + month, + limit, + formatChangedWidth, + calendarDataChildren, + children, + calendarRef, + dataLoading, + onChangeCalendar, + onClickDay, + onClickRestDataCount, + onClickTotalDataCount, +}: PropsWithChildren) => { + const [navigationYear, setNavigationYear] = useState(year); + const [navigationMonth, setNavigationMonth] = useState(month); + const [calendarDataFormat, setCalendarDataFormat] = useState<'long' | 'short'>('long'); + + const isToday = (date: Date) => { + const today = format.date(new Date()); + const inputDate = format.date(date); + + return today === inputDate; + }; + + const shiftMonth = (type: 'next' | 'prev' | 'today') => { + let newYear = year; + let newMonth = month; + + if (type === 'today') { + const today = new Date(); + + newYear = today.getFullYear(); + newMonth = today.getMonth() + 1; + } + + const changedMonth = month + (type === 'next' ? +1 : -1); + + if (type !== 'today' && changedMonth === 0) { + newYear -= 1; + newMonth = 12; + } + + if (type !== 'today' && changedMonth === 13) { + newYear += 1; + newMonth = 1; + } + + if (type !== 'today' && changedMonth > 0 && changedMonth < 13) { + newMonth = changedMonth; + } + + setNavigationYear(newYear); + setNavigationMonth(newMonth); + + if (onChangeCalendar) onChangeCalendar(newYear, newMonth); + }; + + const navigateYear = (year: number) => setNavigationYear(year); + + const navigateMonth = (month: number) => setNavigationMonth(month); + + const navigate = (year?: number, month?: number) => { + if (year && month) { + if (onChangeCalendar) onChangeCalendar(year, month); + return; + } + + if (onChangeCalendar) onChangeCalendar(navigationYear, navigationMonth); + }; + + useEffect(() => { + const calendarResizeObserver = new ResizeObserver(([calendar]) => { + const calendarWidth = calendar.target.clientWidth; + + if (calendarWidth < formatChangedWidth) return setCalendarDataFormat('short'); + + return setCalendarDataFormat('long'); + }); + + if (!calendarRef.current) return; + + calendarResizeObserver.observe(calendarRef.current); + }, [calendarRef, formatChangedWidth]); + + const calendarDataObject: Record = {}; + + Children.forEach(calendarDataChildren, (child) => { + const item = child as ReactElement; + + const { date } = item.props as { date: Date }; + + const formatDate = format.date(date, '-'); + calendarDataObject[formatDate] = calendarDataObject[formatDate] + ? [...calendarDataObject[formatDate], item] + : [item]; + }); + + const calendarStorage = calendar.getCalendarStorage(year, month).map((item) => { + const formatDate = format.date(item.date, '-'); + + return { ...item, children: calendarDataObject[formatDate] }; + }); + + const initValue = { + year, + month, + navigationYear, + navigationMonth, + limit, + calendarStorage, + calendarDataFormat, + dataLoading, + isToday, + shiftMonth, + navigateYear, + navigateMonth, + navigate, + onClickDay, + onClickRestDataCount, + onClickTotalDataCount, + }; + + return {children}; +}; + +export default CalendarProvider; + +export const useCalendar = () => { + const value = useContext(CalendarContext); + + if (!value) throw new Error('적절하지 않는 곳에서 useCalendar를 호출했습니다.'); + + return value; +}; diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarItem/CalendarItem.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarItem/CalendarItem.tsx new file mode 100644 index 00000000..88610a1d --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/CalendarItem/CalendarItem.tsx @@ -0,0 +1,27 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ + +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; + +type Props = { + /** + * 달력에 데이터를 렌더링하기 위한 필수 값. + * + */ + date: Date; + /** + * Calendar Item를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. + * + */ + onClickCalendarItem?: (date: Date) => void; +} & ComponentPropsWithoutRef<'div'>; + +const CalendarItem = ({ date, onClickCalendarItem, children, ...rest }: PropsWithChildren) => { + return ( +
onClickCalendarItem && onClickCalendarItem(date)} {...rest}> + {children} +
+ ); +}; + +export default CalendarItem; diff --git a/frontend/src/components/record/member/calendar/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx b/frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx similarity index 76% rename from frontend/src/components/record/member/calendar/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx rename to frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx index 558a964e..7227a519 100644 --- a/frontend/src/components/record/member/calendar/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx @@ -9,39 +9,17 @@ import color from '@Styles/color'; import ArrowIcon from '@Assets/icons/ArrowIcon'; -type Props = { - year: number; - month: number; - navigationYear: number; - handleMonthShift: (type: 'next' | 'prev' | 'today') => void; - handleNavigationYear: (type: 'next' | 'prev') => void; - handleNavigationMonth: (month: number) => void; - updateMonth: (type: 'next' | 'prev' | 'today') => void; - updateDate: (year: number, month: number) => void; -}; +import { useCalendar } from '../CalendarContext/CalendarProvider'; + +const ControlBar = () => { + const { year, month, navigationYear, navigate, navigateYear, shiftMonth } = useCalendar(); -const MemberRecordCalendarControlBar = ({ - year, - month, - navigationYear, - handleMonthShift, - handleNavigationYear, - handleNavigationMonth, - updateMonth, - updateDate, -}: Props) => { const [isOpenCalendarNavigation, setIsOpenCalendarNavigation] = useState(false); const ref = useOutsideClick(() => setIsOpenCalendarNavigation(false)); - const handleClickMonthShiftButton = (type: 'prev' | 'next' | 'today') => { - handleMonthShift(type); - updateMonth(type); - }; - const handleClickMonthNavigation = (month: number) => { - handleNavigationMonth(month); - updateDate(navigationYear, month); + navigate(navigationYear, month); setIsOpenCalendarNavigation(false); }; @@ -52,21 +30,21 @@ const MemberRecordCalendarControlBar = ({ - handleClickMonthShiftButton('prev')}> + shiftMonth('prev')}> - handleClickMonthShiftButton('next')}> + shiftMonth('next')}> - handleClickMonthShiftButton('today')}>오늘 + shiftMonth('today')}>오늘 {isOpenCalendarNavigation && (
{navigationYear}
- handleNavigationYear('prev')} /> - handleNavigationYear('next')} /> + navigateYear(navigationYear - 1)} /> + navigateYear(navigationYear + 1)} />
@@ -86,7 +64,7 @@ const MemberRecordCalendarControlBar = ({ ); }; -export default MemberRecordCalendarControlBar; +export default ControlBar; const Layout = styled.div` position: relative; diff --git a/frontend/src/components/record/member/calendar/CalendarDay/CalendarDay.tsx b/frontend/src/components/common/Calendar/Calendar/Day/Day.tsx similarity index 79% rename from frontend/src/components/record/member/calendar/CalendarDay/CalendarDay.tsx rename to frontend/src/components/common/Calendar/Calendar/Day/Day.tsx index 1d09a153..0e11cac8 100644 --- a/frontend/src/components/record/member/calendar/CalendarDay/CalendarDay.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Day/Day.tsx @@ -4,15 +4,15 @@ import { css, styled } from 'styled-components'; import color from '@Styles/color'; type Props = { - hasStudy?: boolean; + hasClick?: boolean; isToday: boolean; isCurrentMonthDay: boolean; dayOfWeek: number; } & ComponentPropsWithoutRef<'div'>; -const CalendarDay = ({ +const Day = ({ children, - hasStudy = false, + hasClick = false, isToday, isCurrentMonthDay, dayOfWeek, @@ -27,28 +27,28 @@ const CalendarDay = ({ }; return ( - {children} - + ); }; -export default CalendarDay; +export default Day; type DayProps = { $isToday: boolean; - $hasStudy: boolean; + $hasClick: boolean; $isCurrentMonthDay: boolean; $fontColor: string; }; -const Day = styled.div` +const DayContainer = styled.div` display: flex; justify-content: center; align-items: center; @@ -58,11 +58,11 @@ const Day = styled.div` width: 30px; height: 30px; - ${({ $isToday, $hasStudy, $isCurrentMonthDay, $fontColor }) => css` + ${({ $isToday, $hasClick, $isCurrentMonthDay, $fontColor }) => css` background-color: ${$isToday && color.neutral[100]}; opacity: ${$isCurrentMonthDay ? 1 : 0.4}; color: ${$fontColor}; - cursor: ${$hasStudy && 'pointer'}; + cursor: ${$hasClick && 'pointer'}; `} @media screen and (max-width: 360px) { diff --git a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx new file mode 100644 index 00000000..2864dec8 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx @@ -0,0 +1,124 @@ +import { type ReactElement } from 'react'; +import { styled } from 'styled-components'; + +import color from '@Styles/color'; + +import { useCalendar } from '../CalendarContext/CalendarProvider'; +import Day from '../Day/Day'; + +type Props = { + data: { + day: number; + date: Date; + dayOfWeek: number; + state: 'prev' | 'cur' | 'next'; + children?: ReactElement[]; + }; +}; + +const DayItem = ({ data }: Props) => { + const { state, date, day, dayOfWeek, children } = data; + + const { limit, calendarDataFormat, isToday, onClickDay, onClickRestDataCount, onClickTotalDataCount } = useCalendar(); + + const renderCalendarItems = limit ? children?.slice(0, limit) : children; + + return ( + + + onClickDay && onClickDay(date)} + hasClick={!!onClickDay} + isCurrentMonthDay={state === 'cur'} + dayOfWeek={dayOfWeek} + > + {day} + + {limit && children && calendarDataFormat === 'long' && children.length - limit > 0 && ( + onClickRestDataCount && onClickRestDataCount(date)}> + +{children.length - limit} + + )} + + {calendarDataFormat === 'short' && children?.length ? ( + onClickTotalDataCount && onClickTotalDataCount(date)}> + {children?.length} + + ) : ( + {renderCalendarItems} + )} + + ); +}; + +export default DayItem; + +const Layout = styled.li` + display: flex; + flex-direction: column; + gap: 2px; + padding: 5px; + + background-color: ${color.white}; +`; + +const DayContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const RestRecords = styled.div` + display: flex; + justify-content: center; + + font-size: 1.4rem; + + width: 22px; + height: 22px; + + border-radius: 50%; + background-color: ${color.blue[50]}; + + cursor: pointer; +`; + +const TotalRecordCount = styled.div` + flex: 1; + display: flex; + justify-content: center; + align-items: center; + + font-size: 1.8rem; + + & > span { + display: flex; + justify-content: center; + align-items: center; + + width: 42px; + height: 42px; + + border-radius: 50%; + + background-color: ${color.neutral[100]}; + + cursor: pointer; + } + + @media screen and (max-width: 768px) { + font-size: 1.4rem; + + & > span { + width: 32px; + height: 32px; + } + } +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; diff --git a/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx new file mode 100644 index 00000000..4ea98326 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { css, styled } from 'styled-components'; + +import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; + +import color from '@Styles/color'; + +import { useCalendar } from '../CalendarContext/CalendarProvider'; +import DayItem from '../DayItem/DayItem'; + +type Props = { + calendarRef: React.RefObject; +}; + +const DayList = ({ calendarRef }: Props) => { + const { calendarStorage, dataLoading } = useCalendar(); + + return ( + + {dataLoading && ( + + + + )} + {calendarStorage.map((data, index) => ( + + ))} + + ); +}; + +export default DayList; + +type DaysProps = { + $numberOfWeeks: number; +}; + +const Layout = styled.ul` + position: relative; + + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: ${({ $numberOfWeeks }) => `repeat(${$numberOfWeeks}, minmax(135px, auto))`}; + gap: 1px; + border: 1px solid ${color.neutral[200]}; + + background-color: ${color.neutral[200]}; + + @media screen and (max-width: 510px) { + font-size: 1.4rem; + grid-template-rows: ${({ $numberOfWeeks }) => `repeat(${$numberOfWeeks}, minmax(80px, auto))`}; + } +`; + +const LoadingBar = styled.div` + position: absolute; + + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +`; diff --git a/frontend/src/components/common/Calendar/DatePicker/ConfirmCancelButton/ConfirmCancelButton.tsx b/frontend/src/components/common/Calendar/DatePicker/ConfirmCancelButton/ConfirmCancelButton.tsx new file mode 100644 index 00000000..5999215f --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/ConfirmCancelButton/ConfirmCancelButton.tsx @@ -0,0 +1,45 @@ +import { styled, css } from 'styled-components'; + +import Button from '@Components/common/Button/Button'; + +import { useDatePicker } from '../DatePickerContext/DatePickerProvider'; + +const ConfirmCancelButton = () => { + const { startDate, endDate, onClickCancel, onClickConfirm } = useDatePicker(); + + return ( + + + + + ); +}; + +export default ConfirmCancelButton; + +const Layout = styled.div` + display: flex; + gap: 20px; + justify-content: flex-end; + + margin-top: 20px; +`; diff --git a/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx b/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx new file mode 100644 index 00000000..58100097 --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx @@ -0,0 +1,138 @@ +import { css, styled } from 'styled-components'; + +import Menu from '@Components/common/Menu/Menu'; + +import color from '@Styles/color'; + +import ArrowIcon from '@Assets/icons/ArrowIcon'; + +import { useDatePicker } from '../DatePickerContext/DatePickerProvider'; + +const MENU_STYLE = css` + & > div { + padding: 0; + } +`; + +const MENU_ITEM_STYLE = css` + row-gap: 3px; + max-height: 320px; + overflow: auto; + + font-size: 1.6rem; + font-weight: 300; + + top: 40px; + left: 5px; +`; + +const ControlBar = () => { + const { year, month, handleMonthShift, handleNavigationYear, handleNavigationMonth } = useDatePicker(); + + const today = new Date(); + + return ( + + + + + {year}년 + + } + $menuListStyle={MENU_ITEM_STYLE} + $style={MENU_STYLE} + > + {Array.from({ length: today.getFullYear() - 2023 + 2 }).map((_, index) => ( + handleNavigationYear(2023 + index)}> + {2023 + index}년 + + ))} + + + + + {month}월 + + } + $menuListStyle={MENU_ITEM_STYLE} + $style={MENU_STYLE} + > + {Array.from({ length: 12 }).map((_, index) => ( + handleNavigationMonth(index + 1)}> + {index + 1}월 + + ))} + + + + + handleMonthShift('prev')} /> + handleMonthShift('today')}>● + handleMonthShift('next')} /> + + + ); +}; + +export default ControlBar; + +const Layout = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + + padding: 0px 5px; + margin-bottom: 20px; + + svg { + cursor: pointer; + } +`; + +const MenuTrigger = styled.div` + display: flex; + align-items: center; + gap: 2px; + + border-radius: 8px; + padding: 2px 5px; + + svg { + width: 6px; + height: 6px; + + opacity: 0.6; + } + + transition: background-color 0.2s ease; + + &:hover { + background-color: ${color.neutral[100]}; + } +`; + +const ShiftButton = styled.div` + display: flex; + align-items: center; + gap: 10px; + + opacity: 0.6; +`; + +const TodayButton = styled.div` + cursor: pointer; +`; + +const CurrentYearMonth = styled.span` + display: flex; + + font-size: 2rem; + font-weight: 500; + + cursor: pointer; +`; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx new file mode 100644 index 00000000..75363e9f --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import type { Meta, StoryObj } from '@storybook/react'; + +import DatePicker from './DatePicker'; + +type Story = StoryObj; + +/** + * `DatePicker`는 날짜를 선택할 수 있는 달력 컴포넌트입니다. + */ +const meta: Meta = { + title: 'COMPONENTS/DatePicker', + component: DatePicker, +}; + +export default meta; + +/** + * `DefaultDatePicker`는 기본적인 DatePicker의 스토리입니다. + */ +export const DefaultDatePicker: Story = {}; + +/** + * `StartEndDatePicker`는 startDate와 endDate가 정해진 DatePicker의 스토리입니다. + */ +export const StartEndDatePicker: Story = { + args: { + startDate: new Date('2023-11-02'), + endDate: new Date('2023-11-09'), + }, +}; + +/** + * `OnChangeDatePicker`는 startDate와 endDate를 onChangeDate 속성을 통해 받아올 수 있는 DatePicker의 스토리입니다. + */ +export const OnChangeDatePicker: Story = { + args: { + onChangeDate: (startDate, endDate) => { + window.alert(`${startDate || ''}, ${endDate || ''}`); + }, + }, +}; + +/** + * `DoubleDatePicker`는 두 개의 DatePicker 달력을 보여주는 스토리입니다. + */ +export const DoubleDatePicker: Story = { + args: { + mode: 'double', + }, +}; + +/** + * `ButtonDatePicker`는 확인, 취소 버튼이 있는 DatePicker 스토리입니다. + */ +export const ButtonDatePicker: Story = { + args: { + showButtons: true, + onClickConfirm: (startDate, endDate) => console.log(startDate, endDate), + onClickCancel: () => window.alert('취소'), + }, +}; + +/** + * `OnlyOneDayDatePicker`는 특정 하루만 선택할 수 있는 DatePicker 스토리입니다. + */ +export const OnlyOneDayDatePicker: Story = { + args: { + isOnlyOneDay: true, + onChangeDate: (date) => console.log(date), + startDate: new Date('2023-11-02'), + endDate: new Date('2023-11-09'), + }, +}; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx new file mode 100644 index 00000000..90bdfcb5 --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -0,0 +1,99 @@ +import { styled } from 'styled-components'; + +import color from '@Styles/color'; + +import ConfirmCancelButton from './ConfirmCancelButton/ConfirmCancelButton'; +import ControlBar from './ControlBar/ControlBar'; +import DatePickerProvider from './DatePickerContext/DatePickerProvider'; +import DayList from './DayList/DayList'; +import DayOfWeeks from '../common/DayOfWeeks/DayOfWeeks'; + +type Props = { + /** + * 시작일을 지정하는 속성. + * + */ + startDate: Date | null; + /** + * 마지막일을 지정하는 속성. + * + */ + endDate: Date | null; + /** + * 달력의 개수를 지정하는 속성 + * + * * @default "single" + */ + mode?: 'single' | 'double'; + /** + * Date 선택 후 확인, 취소 버튼을 통해 startDate, endDate를 반환할 수 있는 버튼을 지정하는 속성. + * + * * @default false + */ + showButtons?: boolean; + /** + * 하루를 선택할지 혹은 기간을 선택할지를 지정하는 속성. 해당 속성을 true로 할 경우 endDate 속성은 무시됨. + * + * * @default false + */ + isOnlyOneDay?: boolean; + /** + * startDate, endDate가 바뀔 때 호출되는 함수. startDate, endDate를 매개변수로 받음. + * + */ + onChangeDate?: (startDate: Date | null, endDate: Date | null) => void; + /** + * Date 선택 후 확인버튼을 누를 때 호출되는 함수. startDate, endDate를 매개변수로 받음. + * + */ + onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; + /** + * Date 선택 후 취소버튼을 누를 때 호출되는 함수. + * + */ + onClickCancel?: () => void; +}; + +const DatePicker = ({ + startDate, + endDate, + mode = 'single', + showButtons = false, + isOnlyOneDay = false, + onClickCancel, + onClickConfirm, + onChangeDate, +}: Props) => { + return ( + + + + + + {showButtons && } + + + ); +}; + +export default DatePicker; + +const Layout = styled.div` + max-width: 360px; + + background-color: ${color.white}; + + padding: 15px; + border: 1px solid ${color.neutral[100]}; + border-radius: 4px; + + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; +`; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx new file mode 100644 index 00000000..86fe6ee1 --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -0,0 +1,213 @@ +import type { PropsWithChildren } from 'react'; +import { createContext, useContext, useState } from 'react'; + +import color from '@Styles/color'; + +import type { CalendarStorage } from '@Utils/calendar'; +import calendar from '@Utils/calendar'; +import format from '@Utils/format'; + +type DatePickerContext = { + startDate: Date | null; + endDate: Date | null; + year: number; + month: number; + calendarStorage: CalendarStorage; + nextCalendarInformation: { calendarStorage: CalendarStorage; year: number; month: number } | null; + mode: 'single' | 'double'; + handleMonthShift: (type: 'next' | 'prev' | 'today') => void; + handleNavigationYear: (year: number) => void; + handleNavigationMonth: (year: number) => void; + getDayBackgroundColor: (date: Date) => string; + updateHoverDays: (date: Date) => void; + updateStartEndDate: (date: Date) => void; + onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; + onClickCancel?: () => void; +}; + +const DatePickerContext = createContext(null); + +type Props = { + initStartDate: Date | null; + initEndDate: Date | null; + mode: 'single' | 'double'; + isOnlyOneDay: boolean; + onChangeDate?: (startDate: Date | null, endDate: Date | null) => void; + onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; + onClickCancel?: () => void; +}; + +const DatePickerProvider = ({ + initStartDate, + initEndDate, + mode, + children, + isOnlyOneDay, + onChangeDate, + onClickConfirm, + onClickCancel, +}: PropsWithChildren) => { + const [startDate, setStart] = useState(initStartDate); + const [endDate, setEnd] = useState(isOnlyOneDay ? null : initEndDate); + + const today = new Date(); + + const [year, setYear] = useState(startDate ? startDate.getFullYear() : today.getFullYear()); + const [month, setMonth] = useState(startDate ? startDate.getMonth() + 1 : today.getMonth() + 1); + const [hoveredDay, setHoveredDay] = useState(null); + + const calendarStorage = calendar.getCalendarStorage(year, month); + + const nextCalendarInformation = + mode === 'double' + ? { + calendarStorage: calendar.getCalendarStorage(year, month + 1), + year: month === 12 ? year + 1 : year, + month: month === 12 ? 1 : month + 1, + } + : null; + + const handleMonthShift = (type: 'next' | 'prev' | 'today') => { + if (type === 'today') { + const today = new Date(); + + const newYear = today.getFullYear(); + const newMonth = today.getMonth() + 1; + + setYear(newYear); + setMonth(newMonth); + + return; + } + + const getMonth = () => { + let number = 0; + + if (type === 'next') number += 1; + if (type === 'prev') number -= 1; + + if (mode === 'double') number *= 2; + + return month + number; + }; + + const newDate = new Date(year, getMonth() - 1); + + const newYear = newDate.getFullYear(); + const newMonth = newDate.getMonth() + 1; + + setYear(newYear); + setMonth(newMonth); + }; + + const handleNavigationYear = (year: number) => setYear(year); + + const handleNavigationMonth = (month: number) => setMonth(month); + + const getDayBackgroundColor = (date: Date) => { + const fullDate = format.date(date, '-'); + + if (startDate && format.date(startDate, '-') === fullDate) return color.blue[200]; + + if (endDate && format.date(endDate, '-') === fullDate) return color.blue[200]; + + if (isSoonSelectedDate(date) || isIncludeSelectDate(date)) return color.blue[100]; + + if (fullDate === format.date(today, '-')) return color.neutral[100]; + + return 'transparent'; + }; + + const isSoonSelectedDate = (date: Date) => { + if (!hoveredDay || !startDate) return false; + + if (hoveredDay > startDate) { + if (startDate <= date && hoveredDay >= date) return true; + + return false; + } else { + if (startDate >= date && hoveredDay <= date) return true; + + return false; + } + }; + + const isIncludeSelectDate = (date: Date) => { + if (!startDate || !endDate) return false; + + if (new Date(startDate) < date && new Date(endDate) >= date) return true; + + return false; + }; + + const updateHoverDays = (date: Date) => { + if (isOnlyOneDay) return; + + if (!startDate) return; + if (startDate && endDate) return; + + setHoveredDay(date); + }; + + const updateStartEndDate = (date: Date) => { + if (isOnlyOneDay) { + setStart(date); + if (onChangeDate) onChangeDate(date, endDate); + return; + } + + setHoveredDay(date); + + let newStartDate: null | Date = null; + let newEndDate: null | Date = null; + + if (!startDate) newStartDate = date; + + if (startDate && !endDate && new Date(startDate) > date) { + newStartDate = date; + newEndDate = startDate; + } + + if (startDate && !endDate && new Date(startDate) < date) { + newStartDate = startDate; + newEndDate = date; + } + + if (startDate && endDate) newStartDate = date; + + setStart(newStartDate); + setEnd(newEndDate); + + if (onChangeDate) onChangeDate(newStartDate, newEndDate); + }; + + const initValue = { + startDate, + endDate, + year, + month, + mode, + calendarStorage, + nextCalendarInformation, + handleMonthShift, + handleNavigationYear, + handleNavigationMonth, + getDayBackgroundColor, + updateHoverDays, + updateStartEndDate, + onClickConfirm, + onClickCancel, + }; + + return {children}; +}; + +export default DatePickerProvider; + +export const useDatePicker = () => { + const value = useContext(DatePickerContext); + + if (!value) throw new Error('적절하지 않는 곳에서 useDatePicker를 호출했습니다.'); + + return value; +}; diff --git a/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx b/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx new file mode 100644 index 00000000..6dd265da --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx @@ -0,0 +1,101 @@ +import { css, styled } from 'styled-components'; + +import { useDatePicker } from '../DatePickerContext/DatePickerProvider'; + +const DayList = () => { + const { calendarStorage, nextCalendarInformation, getDayBackgroundColor, updateHoverDays, updateStartEndDate } = + useDatePicker(); + + return ( + <> + + {calendarStorage.map(({ day, state, date }, index) => ( + updateStartEndDate(date)} + onMouseEnter={() => { + if (!!nextCalendarInformation && state !== 'next') updateHoverDays(date); + else if (!nextCalendarInformation) updateHoverDays(date); + }} + $backgroundColor={getDayBackgroundColor(date)} + $isTransparent={!!nextCalendarInformation && state === 'next'} + > + {day} + + ))} + + {nextCalendarInformation && ( + <> + + {nextCalendarInformation.year}년 + {nextCalendarInformation.month}월 + + + {nextCalendarInformation.calendarStorage.map(({ day, state, date }, index) => ( + updateStartEndDate(date)} + onMouseEnter={() => { + if (state !== 'prev') updateHoverDays(date); + }} + $backgroundColor={getDayBackgroundColor(date)} + $isTransparent={state === 'prev'} + > + {day} + + ))} + + + )} + + ); +}; + +export default DayList; + +const Layout = styled.ul` + display: grid; + row-gap: 5px; + grid-template-columns: repeat(7, 1fr); +`; + +type DayProps = { + $isCurrentMonthDay: boolean; + $backgroundColor: string; + $isTransparent?: boolean; +}; + +const Day = styled.li` + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + padding: 5px 10px; + text-align: center; + + height: 50px; + + cursor: pointer; + + transition: background-color 0.1s ease; + + ${({ $isCurrentMonthDay, $backgroundColor, $isTransparent }) => css` + opacity: ${$isTransparent ? 0 : $isCurrentMonthDay ? 1 : 0.4}; + background-color: ${$backgroundColor}; + `} +`; + +const NextYearMonth = styled.span` + display: flex; + gap: 15px; + + font-size: 2rem; + font-weight: 500; + + margin-top: 10px; + padding: 0px 10px; +`; diff --git a/frontend/src/components/record/member/calendar/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx b/frontend/src/components/common/Calendar/common/DayOfWeeks/DayOfWeeks.tsx similarity index 89% rename from frontend/src/components/record/member/calendar/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx rename to frontend/src/components/common/Calendar/common/DayOfWeeks/DayOfWeeks.tsx index 9617bd51..0cb6c1b5 100644 --- a/frontend/src/components/record/member/calendar/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx +++ b/frontend/src/components/common/Calendar/common/DayOfWeeks/DayOfWeeks.tsx @@ -14,7 +14,7 @@ const DAY_COLOR = { const DAY_OF_WEEKS = ['일', '월', '화', '수', '목', '금', '토'] as const; -const CalendarDayOfWeeks = ({ position = 'left' }: { position?: 'left' | 'center' }) => { +const DayOfWeeks = ({ position = 'left' }: { position?: 'left' | 'center' }) => { return ( {DAY_OF_WEEKS.map((dayOfWeek) => ( @@ -26,7 +26,7 @@ const CalendarDayOfWeeks = ({ position = 'left' }: { position?: 'left' | 'center ); }; -export default CalendarDayOfWeeks; +export default DayOfWeeks; const Layout = styled.ul` display: flex; diff --git a/frontend/src/components/record/contexts/MemberRecordPeriodProvider.tsx b/frontend/src/components/record/contexts/MemberRecordPeriodProvider.tsx index aacb0a04..24ce1f34 100644 --- a/frontend/src/components/record/contexts/MemberRecordPeriodProvider.tsx +++ b/frontend/src/components/record/contexts/MemberRecordPeriodProvider.tsx @@ -11,15 +11,13 @@ export type Period = keyof typeof PERIOD; export type MemberRecordPeriodContextType = { period: Period; - startDate?: string; - endDate?: string; + startDate: Date | null; + endDate: Date | null; page: number; hasSelectedCustomPeriod: boolean; triggerSearchRecord: number; - isMiddleSelectedCustomDate: (date: Date) => boolean; updatePeriod: (period: Period) => void; - updateStartEndDate: (date: Date) => void; - updateHoverDays: (date: Date) => void; + updateStartEndDate: (startDate: Date | null, endDate: Date | null) => void; updatePage: (page: number) => void; }; @@ -34,7 +32,6 @@ const MemberRecordPeriodProvider = ({ children }: PropsWithChildren) => { }>(); const [triggerSearchRecord, setTriggerSearchRecord] = useState(0); - const [hoveredDay, setHoveredDay] = useState(null); const updatePeriod = (period: Period) => { const today = new Date(); @@ -73,74 +70,22 @@ const MemberRecordPeriodProvider = ({ children }: PropsWithChildren) => { setTriggerSearchRecord((prev) => prev + 1); }; - const updateStartEndDate = (date: Date) => { - setHoveredDay(date); - - let newStartDate: null | string = null; - let newEndDate: null | string = null; - - if (!searchParams.start) newStartDate = format.date(new Date(date), '-'); - - if (searchParams.start && !searchParams.end && new Date(searchParams.start) > date) { - newStartDate = format.date(new Date(date), '-'); - newEndDate = searchParams.start; - } - - if (searchParams.start && !searchParams.end && new Date(searchParams.start) < date) { - newStartDate = searchParams.start; - newEndDate = format.date(new Date(date), '-'); - } - - if (searchParams.start && searchParams.end) newStartDate = format.date(new Date(date), '-'); - + const updateStartEndDate = (startDate: Date | null, endDate: Date | null) => { updateSearchParams({ - start: newStartDate, - end: newEndDate, + start: startDate ? format.date(new Date(startDate), '-') : null, + end: endDate ? format.date(new Date(endDate), '-') : null, }); }; - const isSoonSelectedDate = (date: Date) => { - if (!hoveredDay || !searchParams.start) return false; - - const startDateObject = new Date(searchParams.start); - - if (hoveredDay > startDateObject) { - if (startDateObject <= date && hoveredDay >= date) return true; - - return false; - } else { - if (startDateObject >= date && hoveredDay <= date) return true; - - return false; - } - }; - - const isIncludeSelectDate = (date: Date) => { - if (!searchParams.start || !searchParams.end) return false; - - if (new Date(searchParams.start) < date && new Date(searchParams.end) >= date) return true; - - return false; - }; - - const updateHoverDays = (date: Date) => { - if (!searchParams.start) return; - if (searchParams.start && searchParams.end) return; - - setHoveredDay(date); - }; - const value = { period: searchParams.period, - startDate: searchParams.start, - endDate: searchParams.end, + startDate: searchParams.start ? new Date(searchParams.start) : null, + endDate: searchParams.end ? new Date(searchParams.end) : null, page: searchParams.page ? Number(searchParams.page) : 1, hasSelectedCustomPeriod: !!searchParams.start || !!searchParams.end, triggerSearchRecord, - isMiddleSelectedCustomDate: (date: Date) => isSoonSelectedDate(date) || isIncludeSelectDate(date), updatePeriod, updateStartEndDate, - updateHoverDays, updatePage, }; return {children}; diff --git a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts index 1aaddab9..3d7731e2 100644 --- a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts +++ b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts @@ -3,81 +3,86 @@ import { useEffect, useState } from 'react'; import useCacheFetch from '@Hooks/api/useCacheFetch'; import usePreFetch from '@Hooks/api/usePreFetch'; +import useSearchParams from '@Hooks/common/useSearchParams'; import calendar from '@Utils/calendar'; import format from '@Utils/format'; import { requestGetMemberCalendarRecord } from '@Apis/index'; -import type { CalendarRecord, MonthStorage } from '@Types/record'; +import type { StudyInfo } from '@Types/study'; -type Props = { - monthStorage: MonthStorage; - calendarRef: React.RefObject; - memberId: string; -}; +const useMemberCalendarRecord = (memberId: string) => { + const { searchParams, updateSearchParams } = useSearchParams<{ + year: string; + month: string; + }>(); -const useMemberCalendarRecord = ({ monthStorage, calendarRef, memberId }: Props) => { - const [calendarRecord, setCalendarRecord] = useState( - monthStorage.map((item) => { - return { ...item, records: [], restRecordsNumber: 0 }; - }), - ); + const year = Number(searchParams.year); + const month = Number(searchParams.month); + + const [renderYear, setRenderYear] = useState(null); + const [renderMonth, setRenderMonth] = useState(null); - const [calendarData, setCalendarData] = useState<'name' | 'count' | null>(null); + const [calendarData, setCalendarData] = useState(null); - const startDate = format.date(new Date(monthStorage.at(0)!.date), '-'); - const endDate = format.date(new Date(monthStorage.at(-1)!.date), '-'); + const [startDate, endDate] = calendar.getMonthFirstLastDate(year, month).map((item) => { + if (!item) return ''; + + return format.date(item.date, '-'); + }); const { cacheFetch, result, isLoading } = useCacheFetch( () => requestGetMemberCalendarRecord(memberId, startDate, endDate), { cacheKey: [startDate, endDate], - cacheTime: 30 * 1000, + cacheTime: 300 * 1000, enabled: false, }, ); const { prefetch } = usePreFetch(); - const prefetchSidesCalendarData = (calendarRecord: CalendarRecord[]) => { - const currentFirstDay = calendarRecord.find((record) => record.state === 'cur')?.date; - - if (!currentFirstDay) return; - - const currentYear = currentFirstDay.getFullYear(); - const currentMonth = currentFirstDay.getMonth(); - - const prevMonth = new Date(currentYear, currentMonth - 1); - const nextMonth = new Date(currentYear, currentMonth + 1); + const prefetchSidesCalendarData = () => { + const prevMonth = new Date(year, month - 2); + const nextMonth = new Date(year, month); const [prevMonthStartDate, prevMonthEndDate] = calendar .getMonthFirstLastDate(prevMonth.getFullYear(), prevMonth.getMonth() + 1) - .map((date) => { - if (!date) return ''; + .map((item) => { + if (!item) return ''; - return format.date(date.date, '-'); + return format.date(item.date, '-'); }); const [nextMonthStartDate, nextMonthEndDate] = calendar .getMonthFirstLastDate(nextMonth.getFullYear(), nextMonth.getMonth() + 1) - .map((date) => { - if (!date) return ''; + .map((item) => { + if (!item) return ''; - return format.date(date.date, '-'); + return format.date(item.date, '-'); }); prefetch(() => requestGetMemberCalendarRecord(memberId, prevMonthStartDate, prevMonthEndDate), { cacheKey: [prevMonthStartDate, prevMonthEndDate], - cacheTime: 30 * 1000, + cacheTime: 300 * 1000, }); prefetch(() => requestGetMemberCalendarRecord(memberId, nextMonthStartDate, nextMonthEndDate), { cacheKey: [nextMonthStartDate, nextMonthEndDate], - cacheTime: 30 * 1000, + cacheTime: 300 * 1000, }); }; + const getStudies = (date: Date) => + calendarData?.filter((item) => format.date(new Date(item.createdDate), '-') === format.date(date, '-')) || []; + + const updateYearMonth = (year: number, month: number) => + updateSearchParams({ + year: String(year), + month: String(month), + }); + useEffect(() => { cacheFetch(); }, [startDate, endDate]); @@ -86,31 +91,25 @@ const useMemberCalendarRecord = ({ monthStorage, calendarRef, memberId }: Props) if (!result) return; const studyRecords = result.data.studyRecords; - const calendarRecord = monthStorage.map((item) => { - const records = studyRecords[format.date(item.date, '-')] || []; - const restRecordsNumber = records && records.length > 3 ? records.length - 3 : 0; - return { ...item, records, restRecordsNumber }; - }); - setCalendarRecord(calendarRecord); - prefetchSidesCalendarData(calendarRecord); + setRenderYear(year); + setRenderMonth(month); + + setCalendarData(Object.values(studyRecords).flat()); }, [result]); useEffect(() => { - const calendarResizeObserver = new ResizeObserver(([calendar]) => { - const calendarWidth = calendar.target.clientWidth; - - if (calendarWidth < 750) return setCalendarData('count'); - - return setCalendarData('name'); - }); - - if (!calendarRef.current) return; - - calendarResizeObserver.observe(calendarRef.current); - }, [calendarRef]); - - return { calendarRecord, calendarData, isLoading }; + prefetchSidesCalendarData(); + }, [year, month]); + + return { + year: renderYear || year, + month: renderMonth || month, + calendarData, + isLoading, + getStudies, + updateYearMonth, + }; }; export default useMemberCalendarRecord; diff --git a/frontend/src/components/record/hooks/useMemberCalendarRecordSearchParams.ts b/frontend/src/components/record/hooks/useMemberCalendarRecordSearchParams.ts deleted file mode 100644 index 15dedb0b..00000000 --- a/frontend/src/components/record/hooks/useMemberCalendarRecordSearchParams.ts +++ /dev/null @@ -1,64 +0,0 @@ -import useSearchParams from '@Hooks/common/useSearchParams'; - -const useMemberCalendarRecordSearchParams = () => { - const { searchParams, updateSearchParams } = useSearchParams<{ - year: string; - month: string; - }>(); - - const urlDate = - searchParams.year && searchParams.month - ? new Date(Number(searchParams.year), Number(searchParams.month) - 1) - : new Date(); - - const updateMonth = (type: 'next' | 'prev' | 'today') => { - let newYear: string | null = null; - let newMonth: string | null = null; - - const today = new Date(); - const currentYear = String(today.getFullYear()); - const currentMonth = String(today.getMonth() + 1); - - const updatedMonth = Number(searchParams.month) + (type === 'next' ? +1 : -1); - - if (updatedMonth === 0) { - newYear = String(Number(searchParams.year) - 1); - newMonth = '12'; - } - - if (updatedMonth === 13) { - newYear = String(Number(searchParams.year) + 1); - newMonth = '1'; - } - - if (updatedMonth < 13 && updatedMonth > 0) { - newYear = searchParams.year || currentYear; - newMonth = String(updatedMonth); - } - - if (type === 'today') { - newYear = currentYear; - newMonth = currentMonth; - } - - updateSearchParams({ - year: newYear, - month: newMonth, - }); - }; - - const updateDate = (year: number, month: number) => { - updateSearchParams({ - year: String(year), - month: String(month), - }); - }; - - return { - urlDate, - updateMonth, - updateDate, - }; -}; - -export default useMemberCalendarRecordSearchParams; diff --git a/frontend/src/components/record/hooks/useMemberListRecord.ts b/frontend/src/components/record/hooks/useMemberListRecord.ts index 909cfc05..09143987 100644 --- a/frontend/src/components/record/hooks/useMemberListRecord.ts +++ b/frontend/src/components/record/hooks/useMemberListRecord.ts @@ -4,6 +4,8 @@ import { useEffect, useState } from 'react'; import useCacheFetch from '@Hooks/api/useCacheFetch'; import usePreFetch from '@Hooks/api/usePreFetch'; +import format from '@Utils/format'; + import { requestGetMemberListRecord } from '@Apis/index'; import type { StudyInfo } from '@Types/study'; @@ -17,13 +19,16 @@ type Props = { const useMemberListRecord = ({ memberId }: Props) => { const { startDate, endDate, page, triggerSearchRecord, updatePage } = useMemberRecordPeriod(); + const stringStartDate = startDate ? format.date(startDate, '-') : null; + const stringEndDate = endDate ? format.date(endDate, '-') : null; + const [memberRecords, setMemberRecords] = useState(null); const [totalPagesNumber, setTotalPagesNumber] = useState(1); const { cacheFetch, result, isLoading } = useCacheFetch( - () => requestGetMemberListRecord(memberId, page - 1, 20, startDate, endDate), + () => requestGetMemberListRecord(memberId, page - 1, 20, stringStartDate, stringEndDate), { - cacheKey: [startDate || '', endDate || '', String(page)], + cacheKey: [stringStartDate || '', stringEndDate || '', String(page)], cacheTime: 30 * 1000, enabled: false, }, @@ -46,8 +51,8 @@ const useMemberListRecord = ({ memberId }: Props) => { if (totalPagesNumber === 1 || pageInfo.totalPages !== pageInfo.totalPages + 1) setTotalPagesNumber(pageInfo.totalPages); - prefetch(() => requestGetMemberListRecord(memberId, page, 20, startDate, endDate), { - cacheKey: [startDate || '', endDate || '', String(page + 1)], + prefetch(() => requestGetMemberListRecord(memberId, page, 20, stringStartDate, stringEndDate), { + cacheKey: [stringStartDate || '', stringEndDate || '', String(page + 1)], cacheTime: 30 * 1000, }); }, [result]); diff --git a/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx b/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx new file mode 100644 index 00000000..3d6f023c --- /dev/null +++ b/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx @@ -0,0 +1,85 @@ +import { useNavigate } from 'react-router-dom'; +import { styled } from 'styled-components'; + +import Calendar from '@Components/common/Calendar/Calendar/Calendar'; +import useMemberCalendarRecord from '@Components/record/hooks/useMemberCalendarRecord'; + +import color from '@Styles/color'; + +import { ROUTES_PATH } from '@Constants/routes'; + +import { useModal } from '@Contexts/ModalProvider'; +import { useNotification } from '@Contexts/NotificationProvider'; + +import format from '@Utils/format'; + +import MemberRecordListModal from '../MemberRecordListModal/MemberRecordListModal'; + +type Props = { + memberId: string; +}; + +const MemberRecordCalendar = ({ memberId }: Props) => { + const navigate = useNavigate(); + const { openModal } = useModal(); + const { send } = useNotification(); + + const { year, month, calendarData, isLoading, getStudies, updateYearMonth } = useMemberCalendarRecord(memberId); + + const handleClickStudyItem = (studyId: string) => navigate(`${ROUTES_PATH.record}/${studyId}`); + + const handleOpenMemberRecordListModal = (date: Date) => { + const studies = getStudies(date); + const fullDate = format.date(date); + + if (studies.length === 0) { + send({ + type: 'error', + message: `${fullDate}에 진행한 스터디가 없어요.`, + }); + return; + } + + openModal( + , + ); + }; + + return ( + updateYearMonth(year, month)} + > + {calendarData?.map((item) => ( + + handleClickStudyItem(item.studyId)}>{item.name} + + ))} + + ); +}; + +export default MemberRecordCalendar; + +const Record = styled.div` + padding: 2px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + background-color: ${color.neutral[100]}; + border-radius: 5px; + + cursor: pointer; +`; diff --git a/frontend/src/components/record/member/calendar/MemberRecordListModal/MemberRecordListModal.tsx b/frontend/src/components/record/member/MemberRecordListModal/MemberRecordListModal.tsx similarity index 95% rename from frontend/src/components/record/member/calendar/MemberRecordListModal/MemberRecordListModal.tsx rename to frontend/src/components/record/member/MemberRecordListModal/MemberRecordListModal.tsx index 6b406b3e..2c93a5cb 100644 --- a/frontend/src/components/record/member/calendar/MemberRecordListModal/MemberRecordListModal.tsx +++ b/frontend/src/components/record/member/MemberRecordListModal/MemberRecordListModal.tsx @@ -7,7 +7,7 @@ import { useModal } from '@Contexts/ModalProvider'; import type { StudyInfo } from '@Types/study'; -import MemberRecordItem from '../../MemberRecordItem/MemberRecordItem'; +import MemberRecordItem from '../MemberRecordItem/MemberRecordItem'; type Props = { fullDate: string; diff --git a/frontend/src/components/record/member/MemberRecordMode/MemberRecordMode.tsx b/frontend/src/components/record/member/MemberRecordMode/MemberRecordMode.tsx index 67822156..9bbf19ea 100644 --- a/frontend/src/components/record/member/MemberRecordMode/MemberRecordMode.tsx +++ b/frontend/src/components/record/member/MemberRecordMode/MemberRecordMode.tsx @@ -1,5 +1,5 @@ import MemberRecordPeriodProvider from '../../contexts/MemberRecordPeriodProvider'; -import MemberRecordCalendar from '../calendar/MemberRecordCalendar/MemberRecordCalendar'; +import MemberRecordCalendar from '../MemberRecordCalendar/MemberRecordCalendar'; import MemberRecordPeriod from '../period/MemberRecordPeriod/MemberRecordPeriod'; type Props = { diff --git a/frontend/src/components/record/member/calendar/MemberRecordCalendar/MemberRecordCalendar.tsx b/frontend/src/components/record/member/calendar/MemberRecordCalendar/MemberRecordCalendar.tsx deleted file mode 100644 index 70911269..00000000 --- a/frontend/src/components/record/member/calendar/MemberRecordCalendar/MemberRecordCalendar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useRef } from 'react'; -import { styled } from 'styled-components'; - -import useCalendar from '@Hooks/common/useCalendar'; - -import color from '@Styles/color'; - -import useMemberCalendarRecordSearchParams from '../../../hooks/useMemberCalendarRecordSearchParams'; -import CalendarDayOfWeeks from '../CalendarDayOfWeeks/CalendarDayOfWeeks'; -import MemberRecordCalendarControlBar from '../MemberRecordCalendarControlBar/MemberRecordCalendarControlBar'; -import MemberRecordCalendarDayList from '../MemberRecordCalendarDayList/MemberRecordCalendarDayList'; - -type Props = { - memberId: string; -}; - -const MemberRecordCalendar = ({ memberId }: Props) => { - const calendarRef = useRef(null); - - const { urlDate, updateDate, updateMonth } = useMemberCalendarRecordSearchParams(); - - const { year, month, navigationYear, monthStorage, handleMonthShift, handleNavigationMonth, handleNavigationYear } = - useCalendar(urlDate); - - return ( - - - - - - - - - - ); -}; - -export default MemberRecordCalendar; - -const Layout = styled.div` - display: flex; - flex-direction: column; - gap: 40px; - - user-select: none; -`; - -const Calendar = styled.div` - display: flex; - flex-direction: column; - gap: 5px; -`; - -type DaysProps = { - $numberOfWeeks: number; -}; - -const CalendarWrapper = styled.ul` - position: relative; - - display: grid; - grid-template-columns: repeat(7, 1fr); - grid-template-rows: ${({ $numberOfWeeks }) => `repeat(${$numberOfWeeks}, minmax(135px, auto))`}; - gap: 1px; - border: 1px solid ${color.neutral[200]}; - - background-color: ${color.neutral[200]}; - - @media screen and (max-width: 510px) { - font-size: 1.4rem; - grid-template-rows: ${({ $numberOfWeeks }) => `repeat(${$numberOfWeeks}, minmax(80px, auto))`}; - } -`; diff --git a/frontend/src/components/record/member/calendar/MemberRecordCalendarDayItem/MemberRecordCalendarDayItem.tsx b/frontend/src/components/record/member/calendar/MemberRecordCalendarDayItem/MemberRecordCalendarDayItem.tsx deleted file mode 100644 index 7da8bd61..00000000 --- a/frontend/src/components/record/member/calendar/MemberRecordCalendarDayItem/MemberRecordCalendarDayItem.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { css, styled } from 'styled-components'; - -import color from '@Styles/color'; - -import { ROUTES_PATH } from '@Constants/routes'; - -import { useModal } from '@Contexts/ModalProvider'; - -import format from '@Utils/format'; - -import type { CalendarRecord } from '@Types/record'; -import type { StudyInfo } from '@Types/study'; - -import CalendarDay from '../CalendarDay/CalendarDay'; -import MemberRecordListModal from '../MemberRecordListModal/MemberRecordListModal'; - -type Props = { - record: CalendarRecord; - calendarData: 'name' | 'count' | null; -}; - -const MemberRecordCalendarDayItem = ({ record, calendarData }: Props) => { - const { state, records, day, date, restRecordsNumber, dayOfWeek } = record; - - const today = new Date(); - - const navigate = useNavigate(); - - const { openModal } = useModal(); - - const handleClickStudyItem = (studyId: string) => navigate(`${ROUTES_PATH.record}/${studyId}`); - - const openRecordsDetail = (fullDate: string, studies: StudyInfo[]) => { - if (studies.length < 1) return; - - openModal( - , - ); - }; - - return ( - - - 0} - isToday={format.date(date) === format.date(today)} - onClick={() => openRecordsDetail(format.date(date), records)} - isCurrentMonthDay={state === 'cur'} - dayOfWeek={dayOfWeek} - > - {day} - - openRecordsDetail(format.date(date), records)} - > - +{restRecordsNumber} - - - {calendarData === 'name' ? ( - - {records.slice(0, 3).map(({ studyId, name }) => ( - handleClickStudyItem(studyId)}> - {name} - - ))} - - ) : ( - openRecordsDetail(format.date(date), records)}> - {records.length > 0 ? {records.length} : ''} - - )} - - ); -}; - -export default MemberRecordCalendarDayItem; - -const Layout = styled.li` - display: flex; - flex-direction: column; - gap: 2px; - padding: 5px; - - background-color: ${color.white}; -`; - -const DayContainer = styled.div` - display: flex; - align-items: center; - justify-content: space-between; -`; - -type RestRecordsProps = { - $isHidden: boolean; -}; - -const RestRecords = styled.div` - display: flex; - justify-content: center; - - font-size: 1.4rem; - - width: 22px; - height: 22px; - - border-radius: 50%; - background-color: ${color.blue[50]}; - - cursor: pointer; - - ${({ $isHidden }) => css` - display: ${$isHidden ? 'none' : 'block'}; - `} -`; - -const Records = styled.ul` - display: grid; - row-gap: 4px; -`; - -const Record = styled.li` - padding: 2px; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - background-color: ${color.neutral[100]}; - border-radius: 5px; - - cursor: pointer; -`; - -const TotalRecordCount = styled.div` - flex: 1; - display: flex; - justify-content: center; - align-items: center; - - font-size: 1.8rem; - - & > span { - display: flex; - justify-content: center; - align-items: center; - - width: 42px; - height: 42px; - - border-radius: 50%; - - background-color: ${color.neutral[100]}; - - cursor: pointer; - } - - @media screen and (max-width: 768px) { - font-size: 1.4rem; - - & > span { - width: 32px; - height: 32px; - } - } -`; diff --git a/frontend/src/components/record/member/calendar/MemberRecordCalendarDayList/MemberRecordCalendarDayList.tsx b/frontend/src/components/record/member/calendar/MemberRecordCalendarDayList/MemberRecordCalendarDayList.tsx deleted file mode 100644 index 517f5e04..00000000 --- a/frontend/src/components/record/member/calendar/MemberRecordCalendarDayList/MemberRecordCalendarDayList.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { styled, css } from 'styled-components'; - -import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; - -import color from '@Styles/color'; - -import type { MonthStorage } from '@Types/record'; - -import useMemberCalendarRecord from '../../../hooks/useMemberCalendarRecord'; -import MemberRecordCalendarDayItem from '../MemberRecordCalendarDayItem/MemberRecordCalendarDayItem'; - -type Props = { - monthStorage: MonthStorage; - memberId: string; - calendarRef: React.RefObject; -}; - -const MemberRecordCalendarDayList = ({ monthStorage, memberId, calendarRef }: Props) => { - const { calendarRecord, calendarData, isLoading } = useMemberCalendarRecord({ monthStorage, calendarRef, memberId }); - - return ( - <> - {isLoading && ( - - - - )} - {calendarRecord.map((record, index) => ( - - ))} - - ); -}; - -export default MemberRecordCalendarDayList; - -const LoadingBar = styled.div` - position: absolute; - - top: 0; - left: 0; - width: 100%; - height: 100%; - - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx b/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx index f05eb536..61898a9a 100644 --- a/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx +++ b/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx @@ -1,116 +1,18 @@ -import { css, styled } from 'styled-components'; +import { styled } from 'styled-components'; -import Menu from '@Components/common/Menu/Menu'; +import DatePicker from '@Components/common/Calendar/DatePicker/DatePicker'; import { useMemberRecordPeriod } from '@Components/record/contexts/MemberRecordPeriodProvider'; -import useCalendar from '@Hooks/common/useCalendar'; - -import color from '@Styles/color'; - -import ArrowIcon from '@Assets/icons/ArrowIcon'; - -import format from '@Utils/format'; - -import CalendarDayOfWeeks from '../../calendar/CalendarDayOfWeeks/CalendarDayOfWeeks'; - -const MENU_STYLE = css` - & > div { - padding: 0; - } -`; - -const MENU_ITEM_STYLE = css` - row-gap: 3px; - max-height: 320px; - overflow: auto; - - font-size: 1.6rem; - font-weight: 300; - - top: 40px; - left: 5px; -`; - const PeriodSelectCalendar = () => { - const today = new Date(); - - const { startDate, endDate, isMiddleSelectedCustomDate, updateStartEndDate, updateHoverDays } = - useMemberRecordPeriod(); - - const { year, month, monthStorage, handleMonthShift, handleNavigationMonth, handleYearShift } = useCalendar( - new Date(startDate || today), - ); + const { startDate, endDate, updateStartEndDate } = useMemberRecordPeriod(); - const getDayBackgroundColor = (date: Date, fullDate: string) => { - if (startDate && startDate === fullDate) return color.blue[200]; - - if (endDate && endDate === fullDate) return color.blue[200]; - - if (isMiddleSelectedCustomDate(date)) return color.blue[100]; - - if (fullDate === format.date(today)) return color.neutral[100]; - - return 'transparent'; - }; return ( - - - - - {year}년 - - } - $menuListStyle={MENU_ITEM_STYLE} - $style={MENU_STYLE} - > - {Array.from({ length: today.getFullYear() - 2023 + 2 }).map((_, index) => ( - handleYearShift(2023 + index)}> - {2023 + index}년 - - ))} - - - - - {month}월 - - } - $menuListStyle={MENU_ITEM_STYLE} - $style={MENU_STYLE} - > - {Array.from({ length: 12 }).map((_, index) => ( - handleNavigationMonth(index + 1)}> - {index + 1}월 - - ))} - - - - - handleMonthShift('prev')} /> - handleMonthShift('today')}>● - handleMonthShift('next')} /> - - - - - {monthStorage.map(({ day, state, date }, index) => ( - updateStartEndDate(date)} - onMouseEnter={() => updateHoverDays(date)} - $backgroundColor={getDayBackgroundColor(date, format.date(date, '-'))} - > - {day} - - ))} - + updateStartEndDate(start, end)} + /> ); }; @@ -123,107 +25,5 @@ const Layout = styled.div` right: 0; left: 0; - background-color: ${color.white}; - - padding: 15px; - border: 1px solid ${color.neutral[100]}; - border-radius: 4px; - - box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; - z-index: 5; `; - -const Month = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - - padding: 0px 5px; - margin-bottom: 20px; - - svg { - cursor: pointer; - } -`; - -const MenuTrigger = styled.div` - display: flex; - align-items: center; - gap: 2px; - - border-radius: 8px; - padding: 2px 5px; - - svg { - width: 6px; - height: 6px; - - opacity: 0.6; - } - - transition: background-color 0.2s ease; - - &:hover { - background-color: ${color.neutral[100]}; - } -`; - -const ShiftButton = styled.div` - display: flex; - align-items: center; - gap: 10px; - - opacity: 0.6; -`; - -const TodayButton = styled.div` - cursor: pointer; -`; - -const CurrentYearMonth = styled.span` - display: flex; - - font-size: 2rem; - font-weight: 500; - - cursor: pointer; -`; - -const Days = styled.ul` - display: grid; - row-gap: 5px; - grid-template-columns: repeat(7, 1fr); - justify-content: center; - - margin-top: 10px; -`; - -type DayProps = { - $isCurrentMonthDay: boolean; - $backgroundColor: string; -}; - -const Day = styled.li` - position: relative; - - display: flex; - justify-content: center; - align-items: center; - - padding: 5px 10px; - text-align: center; - - max-width: 50px; - height: 50px; - - cursor: pointer; - - transition: background-color 0.1s ease; - - ${({ $isCurrentMonthDay, $backgroundColor }) => css` - opacity: ${$isCurrentMonthDay ? 1 : 0.4}; - background-color: ${$backgroundColor}; - `} -`; diff --git a/frontend/src/components/record/member/period/PeriodSelectionBar/PeriodSelectionBar.tsx b/frontend/src/components/record/member/period/PeriodSelectionBar/PeriodSelectionBar.tsx index 6a45641f..34e445f7 100644 --- a/frontend/src/components/record/member/period/PeriodSelectionBar/PeriodSelectionBar.tsx +++ b/frontend/src/components/record/member/period/PeriodSelectionBar/PeriodSelectionBar.tsx @@ -13,6 +13,8 @@ import { useNotification } from '@Contexts/NotificationProvider'; import CalenderIcon from '@Assets/icons/CalenderIcon'; +import format from '@Utils/format'; + import type { Period } from '../../../contexts/MemberRecordPeriodProvider'; import { useMemberRecordPeriod } from '../../../contexts/MemberRecordPeriodProvider'; import PeriodSelectCalendar from '../PeriodSelectCalendar/PeriodSelectCalendar'; @@ -61,9 +63,9 @@ const PeriodSelectionBar = () => { {hasSelectedCustomPeriod ? ( <> -
{startDate && startDate}
+
{startDate && format.date(startDate, '-')}
~
-
{endDate && endDate}
+
{endDate && format.date(endDate, '-')}
) : ( '날짜를 선택해주세요.' diff --git a/frontend/src/hooks/common/useCalendar.ts b/frontend/src/hooks/common/useCalendar.ts deleted file mode 100644 index 808695b0..00000000 --- a/frontend/src/hooks/common/useCalendar.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useState } from 'react'; - -import calendar from '@Utils/calendar'; - -const useCalendar = (date: Date | null) => { - const standardDate = date || new Date(); - - const [year, setYear] = useState(standardDate.getFullYear()); - const [month, setMonth] = useState(standardDate.getMonth() + 1); - const [navigationYear, setNavigationYear] = useState(year); - - const handleMonthShift = (type: 'next' | 'prev' | 'today') => { - if (type === 'today') { - const today = new Date(); - - setYear(today.getFullYear()); - setNavigationYear(today.getFullYear()); - setMonth(today.getMonth() + 1); - - return; - } - - const changedMonth = month + (type === 'next' ? +1 : -1); - - if (changedMonth === 0) { - setYear((prev) => prev - 1); - setNavigationYear((prev) => prev - 1); - setMonth(12); - return; - } - - if (changedMonth === 13) { - setYear((prev) => prev + 1); - setNavigationYear((prev) => prev + 1); - setMonth(1); - return; - } - - setMonth(changedMonth); - }; - - const handleYearShift = (year: number) => setYear(year); - - const handleNavigationYear = (type: 'next' | 'prev' | number) => { - if (type === 'next') setNavigationYear((prev) => prev + 1); - else setNavigationYear((prev) => prev - 1); - }; - - const handleNavigationMonth = (month: number) => { - setYear(navigationYear); - setMonth(month); - }; - - const monthStorage = calendar.getMonthStorage(year, month); - - return { - year, - month, - navigationYear, - handleMonthShift, - handleYearShift, - handleNavigationMonth, - handleNavigationYear, - monthStorage, - }; -}; - -export default useCalendar; diff --git a/frontend/src/mocks/handlers/queryHandler.ts b/frontend/src/mocks/handlers/queryHandler.ts index d0ed6cd6..4e5bac19 100644 --- a/frontend/src/mocks/handlers/queryHandler.ts +++ b/frontend/src/mocks/handlers/queryHandler.ts @@ -9,6 +9,7 @@ import { ACCESS_TOKEN, NEW_ACCESS_TOKEN, STUDY_LIST_10, + STUDY_LIST_11, STUDY_LIST_8, STUDY_LIST_9, STUDY_LIST_ALL, @@ -96,22 +97,13 @@ export const queryHandler = [ const startDate = 'startDate' in searchParams ? searchParams.startDate : null; const studyList = - startDate === '2023-07-30' ? STUDY_LIST_8 : startDate === '2023-08-27' ? STUDY_LIST_9 : STUDY_LIST_10; - - setTimeout(() => { - const addDate = - startDate === '2023-07-30' ? '2023-08-18' : startDate === '2023-08-27' ? '2023-09-03' : '2023-10-10'; - - studyList.studyRecords[addDate] = Array.from({ length: 2 }).map((_, index) => { - return { - studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, - totalCycle: Math.floor(Math.random() * 8) + 1, - timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', - }; - }); - }, 3000); + startDate === '2023-07-30' + ? STUDY_LIST_8 + : startDate === '2023-08-27' + ? STUDY_LIST_9 + : startDate === '2023-10-01' + ? STUDY_LIST_10 + : STUDY_LIST_11; if (requestAuthToken === NEW_ACCESS_TOKEN) return res(ctx.status(200), ctx.json(studyList), ctx.delay(150)); diff --git a/frontend/src/mocks/mockData.ts b/frontend/src/mocks/mockData.ts index c87c9c47..2577d8fd 100644 --- a/frontend/src/mocks/mockData.ts +++ b/frontend/src/mocks/mockData.ts @@ -313,7 +313,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-01T13:33:02.810Z', }; }), '2023-08-02': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -322,7 +322,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-02T13:33:02.810Z', }; }), '2023-08-03': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -331,7 +331,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-03T13:33:02.810Z', }; }), '2023-08-09': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -340,7 +340,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-09T13:33:02.810Z', }; }), '2023-08-14': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -349,7 +349,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-14T13:33:02.810Z', }; }), '2023-08-15': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -358,7 +358,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-15T13:33:02.810Z', }; }), '2023-08-19': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -367,7 +367,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-19T13:33:02.810Z', }; }), '2023-08-20': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -376,7 +376,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-20T13:33:02.810Z', }; }), '2023-08-29': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -385,7 +385,7 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-29T13:33:02.810Z', }; }), '2023-08-30': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { @@ -394,12 +394,123 @@ export const STUDY_LIST_8: { name: `안오면 지상렬${index + 1} 8월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-30T13:33:02.810Z', }; }), }, }; +export const STUDY_LIST_9_ARRAY = [ + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-08-29T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-08-30T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-01T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-02T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-08T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-12T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-16T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-17T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-20T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-21T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-25T13:33:02.810Z', + }; + }), + ...Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1}`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-09-29T13:33:02.810Z', + }; + }), +]; + // 9월 달력 기록 export const STUDY_LIST_9: { studyRecords: Record< @@ -411,118 +522,118 @@ export const STUDY_LIST_9: { '2023-08-29': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-29T13:33:02.810Z', }; }), '2023-08-30': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-08-30T13:33:02.810Z', }; }), '2023-09-01': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-016T13:33:02.810Z', }; }), '2023-09-02': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-02T13:33:02.810Z', }; }), '2023-09-13': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-13T13:33:02.810Z', }; }), '2023-09-14': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-14T13:33:02.810Z', }; }), '2023-09-15': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-15T13:33:02.810Z', }; }), '2023-09-21': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-21T13:33:02.810Z', }; }), '2023-09-22': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-22T13:33:02.810Z', }; }), '2023-09-26': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-26T13:33:02.810Z', }; }), '2023-09-27': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-27T13:33:02.810Z', }; }), '2023-09-28': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-28T13:33:02.810Z', }; }), '2023-09-30': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 9월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-09-30T13:33:02.810Z', }; }), }, @@ -539,37 +650,138 @@ export const STUDY_LIST_10: { '2023-10-01': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 10월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-10-01T13:33:02.810Z', }; }), '2023-10-02': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 10월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-10-02T13:33:02.810Z', }; }), '2023-10-03': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 10월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-10-03T13:33:02.810Z', }; }), '2023-10-04': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { return { studyId: String(index), - name: `안오면 지상렬${index + 1} 8월`, + name: `안오면 지상렬${index + 1} 10월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-10-04T13:33:02.810Z', + }; + }), + '2023-10-12': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 10월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-10-12T13:33:02.810Z', + }; + }), + '2023-10-16': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 10월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-10-16T13:33:02.810Z', + }; + }), + '2023-10-20': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 10월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-10-20T13:33:02.810Z', + }; + }), + }, +}; + +// 11월 달력 기록 +export const STUDY_LIST_11: { + studyRecords: Record< + string, + { studyId: string; name: string; timePerCycle: number; totalCycle: number; createdDate: string }[] + >; +} = { + studyRecords: { + '2023-10-29': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-10-29T13:33:02.810Z', + }; + }), + '2023-11-01': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-11-01T13:33:02.810Z', + }; + }), + '2023-11-03': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-11-03T13:33:02.810Z', + }; + }), + '2023-11-04': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-11-04T13:33:02.810Z', + }; + }), + '2023-11-05': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-11-05T13:33:02.810Z', + }; + }), + '2023-11-07': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, + totalCycle: Math.floor(Math.random() * 8) + 1, + timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, + createdDate: '2023-11-07T13:33:02.810Z', + }; + }), + '2023-11-08': Array.from({ length: Math.floor(Math.random() * 5) + 1 }).map((_, index) => { + return { + studyId: String(index), + name: `안오면 지상렬${index + 1} 11월`, totalCycle: Math.floor(Math.random() * 8) + 1, timePerCycle: (Math.floor(Math.random() * (12 - 1 + 1)) + 1) * 5, - createdDate: '2023-08-16T13:33:02.810Z', + createdDate: '2023-11-08T13:33:02.810Z', }; }), }, diff --git a/frontend/src/utils/calendar.ts b/frontend/src/utils/calendar.ts index 5453b576..fa2929f5 100644 --- a/frontend/src/utils/calendar.ts +++ b/frontend/src/utils/calendar.ts @@ -1,49 +1,40 @@ -import type { MonthStorage } from '@Types/record'; +import type { ReactElement } from 'react'; + +export type CalendarStorage = { + day: number; + dayOfWeek: number; + date: Date; + state: 'prev' | 'next' | 'cur'; + children?: ReactElement[]; +}[]; const calendar = { - // year, month에 해당하는 요일을 담은 저장소 가져오기 - getMonthStorage: (year: number, month: number) => { - const lastDatePrevMonth = calendar.getLastDatePrevMonth(year, month); - const firstDateNextMonth = calendar.getFirstDateNextMonth(year, month); - const firstDateCurrentMonth = calendar.getFirstDateCurrentMonth(year, month); - const lastDateCurrentMonth = calendar.getLastDateCurrentMonth(year, month); - - const prevMonthStorage = calendar.getDatesPrevMonth(lastDatePrevMonth); - const nextMonthStorage = calendar.getDatesNextMonth(firstDateNextMonth); - const currentMonthStorage = calendar.getDatesCurrentMonth(firstDateCurrentMonth, lastDateCurrentMonth); - - return [...prevMonthStorage, ...currentMonthStorage, ...nextMonthStorage]; + getCalendarStorage: (year: number, month: number): CalendarStorage => { + return [ + ...calendar.getPrevMonthLastWeekDays(year, month), + ...calendar.getCurMonthDays(year, month), + ...calendar.getNextMonthFirstWeekDays(year, month), + ]; }, getMonthFirstLastDate: (year: number, month: number) => { - const monthStorage = calendar.getMonthStorage(year, month); + const calendarStorage = calendar.getCalendarStorage(year, month); - return [monthStorage[0], monthStorage.at(-1)]; + return [calendarStorage[0], calendarStorage.at(-1)]; }, - // 이전달의 마지막 날 - getLastDatePrevMonth: (year: number, month: number) => new Date(year, month - 1, 0), - - // 다음달의 첫번째 날 - getFirstDateNextMonth: (year: number, month: number) => new Date(year, month, 1), - - // 이번달의 첫번째 날 - getFirstDateCurrentMonth: (year: number, month: number) => new Date(year, month - 1), + getPrevMonthLastWeekDays: (year: number, month: number): CalendarStorage => { + const prevMonthLastDateObject = new Date(year, month - 1, 0); - // 이번달의 마지막 날 - getLastDateCurrentMonth: (year: number, month: number) => new Date(year, month, 0), + const prevYear = prevMonthLastDateObject.getFullYear(); // 이전 달의 년도 + const prevMonth = prevMonthLastDateObject.getMonth(); // 이전 달 + const prevLastDayOfWeek = prevMonthLastDateObject.getDay(); // 이전 달의 마지막 요일 + const prevLastDay = prevMonthLastDateObject.getDate(); // 이전 달의 마지막 일 - // 지난 달의 마지막 주 가져오기 - getDatesPrevMonth: (lastDatePrevMonth: Date): MonthStorage => { - const lastDate = lastDatePrevMonth.getDate(); // 이전달의 마지막 일 - const lastDay = lastDatePrevMonth.getDay(); // 이전달의 마지막 요일 - const prevMonth = lastDatePrevMonth.getMonth(); // 이전 달 - const prevYear = lastDatePrevMonth.getFullYear(); // 이전 년도 + if (prevLastDayOfWeek === 6) return []; // 이전 달의 마지막 요일이 토요일인 경우 - if (lastDay === 6) return []; // 이전 달의 마지막 요일이 토요일 경우 - - return Array.from({ length: lastDay + 1 }).map((_, index) => { - const day = lastDate - lastDay + index; + return Array.from({ length: prevLastDayOfWeek + 1 }).map((_, index) => { + const day = prevLastDay - prevLastDayOfWeek + index; const dayOfWeek = index; return { @@ -55,38 +46,39 @@ const calendar = { }); }, - // 이번달 가져오기 - getDatesCurrentMonth: (firstDateCurrentMonth: Date, lastDateCurrentMonth: Date): MonthStorage => { - const firstDay = firstDateCurrentMonth.getDay(); // 이번달의 첫번째 요일 - const lastDate = lastDateCurrentMonth.getDate(); // 이번달의 마지막 일 - const currentMonth = firstDateCurrentMonth.getMonth(); // 이번 달 - const currentYear = firstDateCurrentMonth.getFullYear(); // 이번 년도 + getCurMonthDays: (year: number, month: number): CalendarStorage => { + const curMonthFirstDateObject = new Date(year, month - 1); // 이번 달의 첫 번째 날 + const curMonthLastDateObject = new Date(year, month, 0); // 이번 달의 마지막 날 + + const curFirstDayOfWeek = curMonthFirstDateObject.getDay(); // 이번 달의 첫 번째 요일 + const curLastDay = curMonthLastDateObject.getDate(); // 이번 달의 마지막 일 - return Array.from({ length: lastDate }).map((_, index) => { - const day = index + 1; - const dayOfWeek = (firstDay + index) % 7; + return Array.from({ length: curLastDay }).map((_, index) => { + const day = index + 1; // 일은 index에 1을 더한 값 + const dayOfWeek = (curFirstDayOfWeek + index) % 7; // 첫 번째 요일과 index를 더한 값을 7로 나눈 값의 나머지 return { day, dayOfWeek, - date: new Date(currentYear, currentMonth, day), + date: new Date(year, month - 1, day), state: 'cur', }; }); }, - // 다음 달의 첫번째 주 가져오기 - getDatesNextMonth: (firstDateNextMonth: Date): MonthStorage => { - const firstDate = firstDateNextMonth.getDate(); // 다음달의 첫번째 일 - const firstDay = firstDateNextMonth.getDay(); // 다음달의 첫번째 요일 - const nextMonth = firstDateNextMonth.getMonth(); // 다음 달 - const nextYear = firstDateNextMonth.getFullYear(); // 다음 년도 + getNextMonthFirstWeekDays: (year: number, month: number): CalendarStorage => { + const nextMonthFirstDateObject = new Date(year, month); + + const nextYear = nextMonthFirstDateObject.getFullYear(); // 다음 달의 년도 + const nextMonth = nextMonthFirstDateObject.getMonth(); // 다음 달 + const nextFirstDayOfWeek = nextMonthFirstDateObject.getDay(); // 다음 달의 마지막 요일 + const nextFirstDay = nextMonthFirstDateObject.getDate(); // 다음 달의 마지막 일 - if (firstDay === 0) return []; // 다음 달의 첫번째 요일이 일요일 경우 + if (nextFirstDayOfWeek === 0) return []; // 다음 달의 첫 번재 날이 일요일인 경우 - return Array.from({ length: 7 - firstDay }).map((_, index) => { - const day = firstDate + index; - const dayOfWeek = firstDay + index; + return Array.from({ length: 7 - nextFirstDayOfWeek }).map((_, index) => { + const day = nextFirstDay + index; + const dayOfWeek = nextFirstDayOfWeek + index; return { day,