From 071cd9d952f6df29a79d5d1d9d47da2645678a95 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 5 Nov 2023 14:24:59 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20Calendar,=20DatePicker=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/Calendar/Calendar.ts | 158 ++++++++++++++++++++++ frontend/src/utils/Calendar/DatePicker.ts | 72 ++++++++++ 2 files changed, 230 insertions(+) create mode 100644 frontend/src/utils/Calendar/Calendar.ts create mode 100644 frontend/src/utils/Calendar/DatePicker.ts diff --git a/frontend/src/utils/Calendar/Calendar.ts b/frontend/src/utils/Calendar/Calendar.ts new file mode 100644 index 00000000..e83fc2aa --- /dev/null +++ b/frontend/src/utils/Calendar/Calendar.ts @@ -0,0 +1,158 @@ +import format from '@Utils/format'; + +export type CalendarStorage = { + day: number; + dayOfWeek: number; + date: Date; + state: 'prev' | 'next' | 'cur'; +}[]; + +class Calendar { + private year; + + private month; + + private navigationYear; + + private navigationMonth; + + constructor(year: number, month: number) { + this.year = year; + this.month = month; + this.navigationYear = year; + this.navigationMonth = month; + } + + getCalendarStorage = () => { + return [ + ...this.getPrevMonthLastWeekDays(this.year, this.month), + ...this.getCurMonthDays(this.year, this.month), + ...this.getNextMonthFirstWeekDays(this.year, this.month), + ]; + }; + + getYear = () => this.year; + + getMonth = () => this.month; + + isToday = (date: Date) => { + const today = format.date(new Date()); + const inputDate = format.date(date); + + return today === inputDate; + }; + + shiftMonth = (type: 'next' | 'prev' | 'today') => { + let newYear = this.year; + let newMonth = this.month; + + if (type === 'today') { + const today = new Date(); + + newYear = today.getFullYear(); + newMonth = today.getMonth() + 1; + } + + const changedMonth = this.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; + } + + this.year = newYear; + this.navigationYear = newYear; + this.month = newMonth; + this.navigationMonth = newMonth; + }; + + navigateYear = (year: number) => (this.navigationYear = year); + + navigateMonth = (month: number) => (this.navigationMonth = month); + + navigate = () => { + this.year = this.navigationYear; + this.month = this.navigationMonth; + }; + + getNavigationYear = () => this.navigationYear; + + getNavigationMonth = () => this.navigationMonth; + + private getPrevMonthLastWeekDays = (year: number, month: number): CalendarStorage => { + const prevMonthLastDateObject = new Date(year, month - 1, 0); + + const prevYear = prevMonthLastDateObject.getFullYear(); // 이전 달의 년도 + const prevMonth = prevMonthLastDateObject.getMonth(); // 이전 달 + const prevLastDayOfWeek = prevMonthLastDateObject.getDay(); // 이전 달의 마지막 요일 + const prevLastDay = prevMonthLastDateObject.getDate(); // 이전 달의 마지막 일 + + if (prevLastDayOfWeek === 6) return []; // 이전 달의 마지막 요일이 토요일인 경우 + + return Array.from({ length: prevLastDayOfWeek + 1 }).map((_, index) => { + const day = prevLastDay - prevLastDayOfWeek + index; + const dayOfWeek = index; + + return { + day, + dayOfWeek, + date: new Date(prevYear, prevMonth, day), + state: 'prev', + }; + }); + }; + + private 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: curLastDay }).map((_, index) => { + const day = index + 1; // 일은 index에 1을 더한 값 + const dayOfWeek = (curFirstDayOfWeek + index) % 7; // 첫 번째 요일과 index를 더한 값을 7로 나눈 값의 나머지 + + return { + day, + dayOfWeek, + date: new Date(year, month - 1, day), + state: 'cur', + }; + }); + }; + + private 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 (nextFirstDayOfWeek === 0) return []; // 다음 달의 첫 번재 날이 일요일인 경우 + + return Array.from({ length: 7 - nextFirstDayOfWeek }).map((_, index) => { + const day = nextFirstDay + index; + const dayOfWeek = nextFirstDayOfWeek + index; + + return { + day, + dayOfWeek, + date: new Date(nextYear, nextMonth, day), + state: 'next', + }; + }); + }; +} + +export default Calendar; diff --git a/frontend/src/utils/Calendar/DatePicker.ts b/frontend/src/utils/Calendar/DatePicker.ts new file mode 100644 index 00000000..cca43170 --- /dev/null +++ b/frontend/src/utils/Calendar/DatePicker.ts @@ -0,0 +1,72 @@ +import format from '@Utils/format'; + +import Calendar from './Calendar'; + +class DatePickerCalendar extends Calendar { + private startDate: Date | null = null; + + private endDate: Date | null = null; + + init = () => { + this.startDate = null; + this.endDate = null; + }; + + setDate = (date: Date) => { + if (!this.startDate) { + this.startDate = date; + + return; + } + + if (!this.endDate && date > this.startDate) { + this.endDate = date; + + return; + } + + if (!this.endDate) { + this.endDate = this.startDate; + this.startDate = date; + + return; + } + + if (this.startDate && this.endDate) { + this.startDate = date; + this.endDate = null; + + return; + } + }; + + getStartDate = () => this.startDate; + + getEndDate = () => this.endDate; + + isEqualStartDate = (date: Date) => { + if (!this.startDate) return false; + + const startDate = format.date(this.startDate); + const inputDate = format.date(date); + + return startDate === inputDate; + }; + + isEqualEndDate = (date: Date) => { + if (!this.endDate) return false; + + const endDate = format.date(this.endDate); + const inputDate = format.date(date); + + return endDate === inputDate; + }; + + isIncludedDate = (date: Date) => { + if (!this.startDate || !this.endDate) return; + + return date < this.endDate && date > this.startDate; + }; +} + +export default DatePickerCalendar; From 94c1c59cdc193c26a866ba95ddf820f727fdeb90 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 5 Nov 2023 14:25:08 +0900 Subject: [PATCH 02/27] =?UTF-8?q?test:=20Calendar=20test=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/Calendar.stories.tsx | 21 +++++++++++++++++++ .../components/common/Calendar/Calendar.tsx | 0 2 files changed, 21 insertions(+) create mode 100644 frontend/src/components/common/Calendar/Calendar.stories.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar.tsx diff --git a/frontend/src/components/common/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar.stories.tsx new file mode 100644 index 00000000..d7566573 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar.stories.tsx @@ -0,0 +1,21 @@ +import Calendar from '@Utils/Calendar/Calendar'; +import type { Meta, StoryObj } from '@storybook/react'; + +type Story = StoryObj; + +/** + * `Calendar` 컴포넌트는 특정 이벤트 실행, 라우팅을 위한 컴포넌트입니다. + */ +const meta: Meta = { + title: 'INPUTS/Calendar', + component: Calendar, +}; + +export default meta; + +/** + * `DefaultCalendar`는 메인 색상을 가지는 `Calendar` 스토리입니다. + */ +export const DefaultCalendar: Story = { + args: {}, +}; diff --git a/frontend/src/components/common/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar.tsx new file mode 100644 index 00000000..e69de29b From 23554aee7c90e747b4158a0a9a41d19701c75fd4 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 13:17:48 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20Calendar=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.eslintrc | 2 +- .../common/Calendar/Calendar.stories.tsx | 21 -- .../components/common/Calendar/Calendar.tsx | 0 .../Calendar/Calendar/Calendar.stories.tsx | 51 ++++ .../common/Calendar/Calendar/Calendar.tsx | 68 +++++ .../CalendarContext/CalendarProvider.tsx | 145 ++++++++++ .../Calendar/ControlBar/ControlBar.tsx | 209 +++++++++++++++ .../common/Calendar/Calendar/Day/Day.tsx | 72 +++++ .../Calendar/Calendar/DayItem/DayItem.tsx | 161 +++++++++++ .../DayItemWrapper/DayItemWrapper.tsx | 30 +++ .../Calendar/Calendar/DayList/DayList.tsx | 70 +++++ .../Calendar/DayOfWeeks/DayOfWeeks.tsx | 50 ++++ frontend/src/utils/Calendar/Calendar.ts | 252 ++++++++++++------ frontend/src/utils/Calendar/DatePicker.ts | 72 ----- 14 files changed, 1021 insertions(+), 182 deletions(-) delete mode 100644 frontend/src/components/common/Calendar/Calendar.stories.tsx delete mode 100644 frontend/src/components/common/Calendar/Calendar.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/Calendar.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/Day/Day.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx create mode 100644 frontend/src/components/common/Calendar/Calendar/DayOfWeeks/DayOfWeeks.tsx delete mode 100644 frontend/src/utils/Calendar/DatePicker.ts 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/components/common/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar.stories.tsx deleted file mode 100644 index d7566573..00000000 --- a/frontend/src/components/common/Calendar/Calendar.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Calendar from '@Utils/Calendar/Calendar'; -import type { Meta, StoryObj } from '@storybook/react'; - -type Story = StoryObj; - -/** - * `Calendar` 컴포넌트는 특정 이벤트 실행, 라우팅을 위한 컴포넌트입니다. - */ -const meta: Meta = { - title: 'INPUTS/Calendar', - component: Calendar, -}; - -export default meta; - -/** - * `DefaultCalendar`는 메인 색상을 가지는 `Calendar` 스토리입니다. - */ -export const DefaultCalendar: Story = { - args: {}, -}; diff --git a/frontend/src/components/common/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar.tsx deleted file mode 100644 index e69de29b..00000000 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..0b6b8ddd --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { STUDY_LIST_9 } from 'mocks/mockData'; + +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 = {}; + +/** + * `Calendar202309`는 2023년 9월 달력으로 외부에서 데이터를 받습니다. + */ +export const Calendar202309: Story = { + args: { + year: 2023, + month: 9, + onChangeCalendar: (year, month) => { + console.log(year, month); + }, + children: Object.entries(STUDY_LIST_9.studyRecords).map(([date, data], index) => { + return ( + window.alert('모달 창 열기')} + onClickDay={() => window.alert('날짜 클릭하기')} + > +
    + {data.slice(0, 3).map(({ studyId, name }) => ( +
  • {name}
  • + ))} +
+
+ ); + }), + }, +}; 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..a05026dd --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -0,0 +1,68 @@ +import type { PropsWithChildren } from 'react'; +import { useRef } from 'react'; +import { styled } from 'styled-components'; + +import CalendarProvider from './CalendarContext/CalendarProvider'; +import ControlBar from './ControlBar/ControlBar'; +import DayItemWrapper from './DayItemWrapper/DayItemWrapper'; +import DayList from './DayList/DayList'; +import DayOfWeeks from './DayOfWeeks/DayOfWeeks'; + +type Props = { + /** + * 달력의 년도를 지정하는 속성. + * + * * @default 2023 + */ + year: number; + /** + * 달력의 월을 지정하는 속성. + * + * * @default 11 + */ + month: number; + /** + * 달력의 년, 월이 바뀔 때 호출되는 함수. year, month를 매개변수로 받음. + * + */ + onChangeCalendar?: (year: number, month: number) => void; +}; + +const Calendar = ({ + year = new Date().getFullYear(), + month = new Date().getMonth() + 1, + onChangeCalendar, + children, +}: PropsWithChildren) => { + const calendarRef = useRef(null); + + return ( + + + + + + + + + + ); +}; + +Calendar.Item = DayItemWrapper; + +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..abe05d15 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -0,0 +1,145 @@ +import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; +import { Children, createContext, useContext, useState } from 'react'; + +import type { CalendarStorage } from '@Utils/Calendar/Calendar'; +import calendar from '@Utils/Calendar/Calendar'; +import format from '@Utils/format'; + +type CalendarContext = { + year: number; + month: number; + navigationYear: number; + navigationMonth: number; + calendarStorage: CalendarStorage; + isToday: (date: Date) => boolean; + shiftMonth: (type: 'next' | 'prev' | 'today') => void; + navigateYear: (year: number) => void; + navigateMonth: (month: number) => void; + navigate: (year?: number, month?: number) => void; + getCalendarDayChildren: (date: Date) => ReactNode; +}; + +type Props = { + initYear: number; + initMonth: number; + calendarData: ReactNode; + onChangeCalendar?: (year: number, month: number) => void; +}; + +const CalendarContext = createContext(null); + +const CalendarProvider = ({ + initYear, + initMonth, + calendarData, + children, + onChangeCalendar, +}: PropsWithChildren) => { + const [year, setYear] = useState(initYear); + const [month, setMonth] = useState(initMonth); + const [navigationYear, setNavigationYear] = useState(initYear); + const [navigationMonth, setNavigationMonth] = useState(initMonth); + const [calendarStorage, setCalendarStorage] = useState(calendar.getCalendarStorage(year, month)); + + const getCalendarDayChildren = (date: Date) => { + return Children.toArray(calendarData).find((child) => { + const item = child as ReactElement; + const { date: inputDate } = item.props as { date: string }; + + if (format.date(date, '-') === inputDate) return item; + }); + }; + + 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; + } + + setYear(newYear); + setMonth(newMonth); + setNavigationYear(newYear); + setNavigationMonth(newMonth); + + setCalendarStorage(calendar.getCalendarStorage(newYear, 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) { + setYear(year); + setMonth(month); + + setCalendarStorage(calendar.getCalendarStorage(year, month)); + + if (onChangeCalendar) onChangeCalendar(year, month); + return; + } + + setYear(navigationYear); + setMonth(navigationMonth); + + setCalendarStorage(calendar.getCalendarStorage(navigationYear, navigationMonth)); + + if (onChangeCalendar) onChangeCalendar(navigationYear, navigationMonth); + }; + + const initValue = { + year, + month, + navigationYear, + navigationMonth, + calendarStorage, + isToday, + shiftMonth, + navigateYear, + navigateMonth, + navigate, + getCalendarDayChildren, + }; + + return {children}; +}; + +export default CalendarProvider; + +export const useCalendar = () => { + const value = useContext(CalendarContext); + + if (!value) throw new Error('calendar가 적절하지 않는 곳에서 호출되었습니다.'); + + return value; +}; diff --git a/frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx b/frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx new file mode 100644 index 00000000..7227a519 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/ControlBar/ControlBar.tsx @@ -0,0 +1,209 @@ +import { useState } from 'react'; +import { css, styled } from 'styled-components'; + +import Typography from '@Components/common/Typography/Typography'; + +import useOutsideClick from '@Hooks/common/useOutsideClick'; + +import color from '@Styles/color'; + +import ArrowIcon from '@Assets/icons/ArrowIcon'; + +import { useCalendar } from '../CalendarContext/CalendarProvider'; + +const ControlBar = () => { + const { year, month, navigationYear, navigate, navigateYear, shiftMonth } = useCalendar(); + + const [isOpenCalendarNavigation, setIsOpenCalendarNavigation] = useState(false); + + const ref = useOutsideClick(() => setIsOpenCalendarNavigation(false)); + + const handleClickMonthNavigation = (month: number) => { + navigate(navigationYear, month); + setIsOpenCalendarNavigation(false); + }; + + return ( + + setIsOpenCalendarNavigation((prev) => !prev)}> + {year}년 {month}월 + + + + shiftMonth('prev')}> + + + shiftMonth('next')}> + + + shiftMonth('today')}>오늘 + + {isOpenCalendarNavigation && ( + + +
{navigationYear}
+ + navigateYear(navigationYear - 1)} /> + navigateYear(navigationYear + 1)} /> + +
+ + {Array.from({ length: 12 }).map((_, index) => ( + handleClickMonthNavigation(index + 1)} + > + {index + 1}월 + + ))} + +
+ )} +
+ ); +}; + +export default ControlBar; + +const Layout = styled.div` + position: relative; + + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + + width: fit-content; + + p { + width: 170px; + + padding: 0px 3px; + + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + + border-radius: 4px; + + transition: background-color 0.2s ease; + + cursor: pointer; + + &:hover { + background-color: ${color.neutral[100]}; + } + } + + @media screen and (max-width: 768px) { + font-size: 1.4rem; + } +`; + +const MonthShiftButtonContainer = styled.div` + display: flex; + align-items: center; + gap: 10px; + + margin-left: 20px; + + @media screen and (max-width: 768px) { + margin-left: 0px; + } +`; + +const MonthShiftButton = styled.div` + padding: 8px; + border-radius: 50%; + + border: 1px solid ${color.neutral[200]}; + + cursor: pointer; +`; + +const ShiftTodayButton = styled.div` + padding: 4px 16px; + border-radius: 16px; + + border: 1px solid ${color.neutral[200]}; + + cursor: pointer; +`; + +const CalendarNavigation = styled.div` + position: absolute; + top: 40px; + + background-color: ${color.white}; + + padding: 10px; + border: 1px solid ${color.neutral[100]}; + border-radius: 4px; + + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; + + z-index: 5; +`; + +const YearNavigation = styled.div` + display: flex; + justify-content: space-between; + + padding: 10px; + + & > div { + font-weight: 700; + } +`; + +const YearNavigationButton = styled.div` + display: flex; + gap: 20px; + + opacity: 0.6; + + svg { + cursor: pointer; + } +`; + +const MonthNavigation = styled.ul` + display: grid; + grid-template-columns: repeat(4, 80px); + row-gap: 5px; + justify-items: center; + + padding: 10px; + + @media screen and (max-width: 768px) { + grid-template-columns: repeat(4, 60px); + } +`; + +type MonthProps = { + $isCurMonth: boolean; +}; + +const Month = styled.li` + padding: 10px 20px; + border-radius: 4px; + + transition: background-color 0.2s ease; + + cursor: pointer; + + ${({ $isCurMonth }) => css` + color: ${$isCurMonth ? color.blue[500] : color.neutral[600]}; + font-weight: ${$isCurMonth ? 500 : 300}; + `} + + &:hover { + background-color: ${color.blue[100]}; + } + + @media screen and (max-width: 768px) { + padding: 4px 8px; + } +`; diff --git a/frontend/src/components/common/Calendar/Calendar/Day/Day.tsx b/frontend/src/components/common/Calendar/Calendar/Day/Day.tsx new file mode 100644 index 00000000..0e11cac8 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/Day/Day.tsx @@ -0,0 +1,72 @@ +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; +import { css, styled } from 'styled-components'; + +import color from '@Styles/color'; + +type Props = { + hasClick?: boolean; + isToday: boolean; + isCurrentMonthDay: boolean; + dayOfWeek: number; +} & ComponentPropsWithoutRef<'div'>; + +const Day = ({ + children, + hasClick = false, + isToday, + isCurrentMonthDay, + dayOfWeek, + ...rest +}: PropsWithChildren) => { + const getDayFontColor = (dayOfWeek: number) => { + if (dayOfWeek === 0) return color.red[600]; + + if (dayOfWeek === 6) return color.blue[600]; + + return color.black; + }; + + return ( + + {children} + + ); +}; + +export default Day; + +type DayProps = { + $isToday: boolean; + $hasClick: boolean; + $isCurrentMonthDay: boolean; + $fontColor: string; +}; + +const DayContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + + border-radius: 50%; + + width: 30px; + height: 30px; + + ${({ $isToday, $hasClick, $isCurrentMonthDay, $fontColor }) => css` + background-color: ${$isToday && color.neutral[100]}; + opacity: ${$isCurrentMonthDay ? 1 : 0.4}; + color: ${$fontColor}; + cursor: ${$hasClick && 'pointer'}; + `} + + @media screen and (max-width: 360px) { + margin: 0 auto; + margin-top: 5px; + } +`; 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..8a446ed3 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx @@ -0,0 +1,161 @@ +import { useState, type PropsWithChildren, type ReactElement, useEffect } 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'; + }; +}; + +const DayItem = ({ data }: PropsWithChildren) => { + const { state, date, day, dayOfWeek } = data; + const { getCalendarDayChildren, isToday } = useCalendar(); + + const [calendarDayChildrenProps, setCalendarDayChildrenProps] = useState<{ + restDataCount: number; + onClickRestDataCount: () => void; + onClickDay: () => void; + } | null>(null); + + const calendarDayChildren = getCalendarDayChildren(date) as ReactElement; + + useEffect(() => { + if (!calendarDayChildren) { + setCalendarDayChildrenProps(null); + return; + } + + const props = calendarDayChildren.props as { + restDataCount: number; + onClickRestDataCount: () => void; + onClickDay: () => void; + }; + setCalendarDayChildrenProps(props); + }, [calendarDayChildren]); + + return ( + + + + {day} + + {calendarDayChildrenProps && calendarDayChildrenProps.restDataCount > 0 && ( + + +{calendarDayChildrenProps.restDataCount} + + )} + + {/* {calendarDayChildren === 'name' ? ( + + {records.slice(0, 3).map(({ studyId, name }) => ( + handleClickStudyItem(studyId)}> + {name} + + ))} + + ) : ( + openRecordsDetail(format.date(date), records)}> + {records.length > 0 ? {records.length} : ''} + + )} */} + {calendarDayChildren && calendarDayChildren} + + ); +}; + +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 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/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx new file mode 100644 index 00000000..1718dfaa --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx @@ -0,0 +1,30 @@ +import type { PropsWithChildren } from 'react'; + +type Props = { + /** + * 달력에 데이터를 렌더링하기 위한 필수 값. "yyyy-mm-dd" 형식으로 이와 일치하는 곳에 자식 컴포넌트가 렌더링 됨. + * + */ + date: string; + /** + * 남은 데이터의 개수를 보여주기 위한 값. 0보다 크면 Day 오른쪽에 해당 값이 렌더링 됨. + * + */ + restDataCount?: number; + /** + * 남은 데이터의 개수를 클릭하면 발생되는 이벤트. 남은 데이터의 개수가 0보다 클 경우 이벤트를 등록할 수 있음. + * + */ + onClickRestDataCount?: () => void; + /** + * Day을 클릭하면 발생되는 이벤트. Day에 해당하는 데이터가 있는 경우 이벤트를 등록할 수 있음. + * + */ + onClickDay?: () => void; +}; + +const DayItemWrapper = ({ children }: PropsWithChildren) => { + return
{children}
; +}; + +export default DayItemWrapper; 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..670bb410 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { styled } from 'styled-components'; + +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 } = useCalendar(); + + return ( + + {/* {isLoading && ( + + + + )} */} + {calendarStorage.map((data, index) => ( + + ))} + + ); +}; + +export default DayList; + +const LoadingBar = styled.div` + position: absolute; + + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +`; + +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))`}; + } +`; diff --git a/frontend/src/components/common/Calendar/Calendar/DayOfWeeks/DayOfWeeks.tsx b/frontend/src/components/common/Calendar/Calendar/DayOfWeeks/DayOfWeeks.tsx new file mode 100644 index 00000000..0cb6c1b5 --- /dev/null +++ b/frontend/src/components/common/Calendar/Calendar/DayOfWeeks/DayOfWeeks.tsx @@ -0,0 +1,50 @@ +import { css, styled } from 'styled-components'; + +import color from '@Styles/color'; + +const DAY_COLOR = { + 일: color.red[600], + 월: color.black, + 화: color.black, + 수: color.black, + 목: color.black, + 금: color.black, + 토: color.blue[600], +} as const; + +const DAY_OF_WEEKS = ['일', '월', '화', '수', '목', '금', '토'] as const; + +const DayOfWeeks = ({ position = 'left' }: { position?: 'left' | 'center' }) => { + return ( + + {DAY_OF_WEEKS.map((dayOfWeek) => ( + + {dayOfWeek} + + ))} + + ); +}; + +export default DayOfWeeks; + +const Layout = styled.ul` + display: flex; +`; + +type DayOfWeekProps = { + $dayOfWeek: (typeof DAY_OF_WEEKS)[number]; + position: 'left' | 'center'; +}; + +const DayOfWeek = styled.li` + flex: 1; + + color: ${color.black}; + + ${({ $dayOfWeek, position }) => css` + color: ${position === 'left' ? DAY_COLOR[$dayOfWeek] : color.black}; + margin-left: ${position === 'left' && '5px'}; + text-align: ${position === 'center' && 'center'}; + `} +`; diff --git a/frontend/src/utils/Calendar/Calendar.ts b/frontend/src/utils/Calendar/Calendar.ts index e83fc2aa..57685e93 100644 --- a/frontend/src/utils/Calendar/Calendar.ts +++ b/frontend/src/utils/Calendar/Calendar.ts @@ -1,5 +1,3 @@ -import format from '@Utils/format'; - export type CalendarStorage = { day: number; dayOfWeek: number; @@ -7,88 +5,16 @@ export type CalendarStorage = { state: 'prev' | 'next' | 'cur'; }[]; -class Calendar { - private year; - - private month; - - private navigationYear; - - private navigationMonth; - - constructor(year: number, month: number) { - this.year = year; - this.month = month; - this.navigationYear = year; - this.navigationMonth = month; - } - - getCalendarStorage = () => { +const calendar = { + getCalendarStorage: (year: number, month: number): CalendarStorage => { return [ - ...this.getPrevMonthLastWeekDays(this.year, this.month), - ...this.getCurMonthDays(this.year, this.month), - ...this.getNextMonthFirstWeekDays(this.year, this.month), + ...calendar.getPrevMonthLastWeekDays(year, month), + ...calendar.getCurMonthDays(year, month), + ...calendar.getNextMonthFirstWeekDays(year, month), ]; - }; - - getYear = () => this.year; - - getMonth = () => this.month; - - isToday = (date: Date) => { - const today = format.date(new Date()); - const inputDate = format.date(date); - - return today === inputDate; - }; - - shiftMonth = (type: 'next' | 'prev' | 'today') => { - let newYear = this.year; - let newMonth = this.month; - - if (type === 'today') { - const today = new Date(); - - newYear = today.getFullYear(); - newMonth = today.getMonth() + 1; - } - - const changedMonth = this.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; - } - - this.year = newYear; - this.navigationYear = newYear; - this.month = newMonth; - this.navigationMonth = newMonth; - }; - - navigateYear = (year: number) => (this.navigationYear = year); - - navigateMonth = (month: number) => (this.navigationMonth = month); - - navigate = () => { - this.year = this.navigationYear; - this.month = this.navigationMonth; - }; - - getNavigationYear = () => this.navigationYear; - - getNavigationMonth = () => this.navigationMonth; - - private getPrevMonthLastWeekDays = (year: number, month: number): CalendarStorage => { + getPrevMonthLastWeekDays: (year: number, month: number): CalendarStorage => { const prevMonthLastDateObject = new Date(year, month - 1, 0); const prevYear = prevMonthLastDateObject.getFullYear(); // 이전 달의 년도 @@ -109,9 +35,9 @@ class Calendar { state: 'prev', }; }); - }; + }, - private getCurMonthDays = (year: number, month: number): CalendarStorage => { + getCurMonthDays: (year: number, month: number): CalendarStorage => { const curMonthFirstDateObject = new Date(year, month - 1); // 이번 달의 첫 번째 날 const curMonthLastDateObject = new Date(year, month, 0); // 이번 달의 마지막 날 @@ -129,9 +55,9 @@ class Calendar { state: 'cur', }; }); - }; + }, - private getNextMonthFirstWeekDays = (year: number, month: number): CalendarStorage => { + getNextMonthFirstWeekDays: (year: number, month: number): CalendarStorage => { const nextMonthFirstDateObject = new Date(year, month); const nextYear = nextMonthFirstDateObject.getFullYear(); // 다음 달의 년도 @@ -152,7 +78,157 @@ class Calendar { state: 'next', }; }); - }; -} + }, +}; + +export default calendar; + +// class Calendar { +// private year; + +// private month; + +// private navigationYear; + +// private navigationMonth; + +// constructor(year: number, month: number) { +// this.year = year; +// this.month = month; +// this.navigationYear = year; +// this.navigationMonth = month; +// } + +// getCalendarStorage = () => { +// return [ +// ...this.getPrevMonthLastWeekDays(this.year, this.month), +// ...this.getCurMonthDays(this.year, this.month), +// ...this.getNextMonthFirstWeekDays(this.year, this.month), +// ]; +// }; + +// getYear = () => this.year; + +// getMonth = () => this.month; + +// isToday = (date: Date) => { +// const today = format.date(new Date()); +// const inputDate = format.date(date); + +// return today === inputDate; +// }; + +// shiftMonth = (type: 'next' | 'prev' | 'today') => { +// let newYear = this.year; +// let newMonth = this.month; + +// if (type === 'today') { +// const today = new Date(); + +// newYear = today.getFullYear(); +// newMonth = today.getMonth() + 1; +// } + +// const changedMonth = this.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; +// } + +// this.year = newYear; +// this.navigationYear = newYear; +// this.month = newMonth; +// this.navigationMonth = newMonth; +// }; + +// navigateYear = (year: number) => (this.navigationYear = year); + +// navigateMonth = (month: number) => (this.navigationMonth = month); + +// navigate = () => { +// this.year = this.navigationYear; +// this.month = this.navigationMonth; +// }; + +// getNavigationYear = () => this.navigationYear; + +// getNavigationMonth = () => this.navigationMonth; + +// private getPrevMonthLastWeekDays = (year: number, month: number): CalendarStorage => { +// const prevMonthLastDateObject = new Date(year, month - 1, 0); + +// const prevYear = prevMonthLastDateObject.getFullYear(); // 이전 달의 년도 +// const prevMonth = prevMonthLastDateObject.getMonth(); // 이전 달 +// const prevLastDayOfWeek = prevMonthLastDateObject.getDay(); // 이전 달의 마지막 요일 +// const prevLastDay = prevMonthLastDateObject.getDate(); // 이전 달의 마지막 일 + +// if (prevLastDayOfWeek === 6) return []; // 이전 달의 마지막 요일이 토요일인 경우 + +// return Array.from({ length: prevLastDayOfWeek + 1 }).map((_, index) => { +// const day = prevLastDay - prevLastDayOfWeek + index; +// const dayOfWeek = index; + +// return { +// day, +// dayOfWeek, +// date: new Date(prevYear, prevMonth, day), +// state: 'prev', +// }; +// }); +// }; + +// private 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: curLastDay }).map((_, index) => { +// const day = index + 1; // 일은 index에 1을 더한 값 +// const dayOfWeek = (curFirstDayOfWeek + index) % 7; // 첫 번째 요일과 index를 더한 값을 7로 나눈 값의 나머지 + +// return { +// day, +// dayOfWeek, +// date: new Date(year, month - 1, day), +// state: 'cur', +// }; +// }); +// }; + +// private 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(); // 다음 달의 마지막 일 -export default Calendar; +// if (nextFirstDayOfWeek === 0) return []; // 다음 달의 첫 번재 날이 일요일인 경우 + +// return Array.from({ length: 7 - nextFirstDayOfWeek }).map((_, index) => { +// const day = nextFirstDay + index; +// const dayOfWeek = nextFirstDayOfWeek + index; + +// return { +// day, +// dayOfWeek, +// date: new Date(nextYear, nextMonth, day), +// state: 'next', +// }; +// }); +// }; +// } + +// export default Calendar; diff --git a/frontend/src/utils/Calendar/DatePicker.ts b/frontend/src/utils/Calendar/DatePicker.ts deleted file mode 100644 index cca43170..00000000 --- a/frontend/src/utils/Calendar/DatePicker.ts +++ /dev/null @@ -1,72 +0,0 @@ -import format from '@Utils/format'; - -import Calendar from './Calendar'; - -class DatePickerCalendar extends Calendar { - private startDate: Date | null = null; - - private endDate: Date | null = null; - - init = () => { - this.startDate = null; - this.endDate = null; - }; - - setDate = (date: Date) => { - if (!this.startDate) { - this.startDate = date; - - return; - } - - if (!this.endDate && date > this.startDate) { - this.endDate = date; - - return; - } - - if (!this.endDate) { - this.endDate = this.startDate; - this.startDate = date; - - return; - } - - if (this.startDate && this.endDate) { - this.startDate = date; - this.endDate = null; - - return; - } - }; - - getStartDate = () => this.startDate; - - getEndDate = () => this.endDate; - - isEqualStartDate = (date: Date) => { - if (!this.startDate) return false; - - const startDate = format.date(this.startDate); - const inputDate = format.date(date); - - return startDate === inputDate; - }; - - isEqualEndDate = (date: Date) => { - if (!this.endDate) return false; - - const endDate = format.date(this.endDate); - const inputDate = format.date(date); - - return endDate === inputDate; - }; - - isIncludedDate = (date: Date) => { - if (!this.startDate || !this.endDate) return; - - return date < this.endDate && date > this.startDate; - }; -} - -export default DatePickerCalendar; From 1055b9af6a0fb2a22565a4d3c888d435228f7fd6 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 15:41:57 +0900 Subject: [PATCH 04/27] =?UTF-8?q?refactor:=20=EB=8B=AC=EB=A0=A5=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=EB=8B=AC=ED=95=98=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 객체에서 배열로 변경 --- .../Calendar/Calendar/Calendar.stories.tsx | 23 +--- .../common/Calendar/Calendar/Calendar.tsx | 25 +++- .../CalendarContext/CalendarProvider.tsx | 55 ++++++++- .../Calendar/Calendar/DayItem/DayItem.tsx | 60 ++++------ .../DayItemWrapper/DayItemWrapper.tsx | 9 +- .../Calendar/Calendar/DayList/DayList.tsx | 24 ---- frontend/src/mocks/mockData.ts | 111 ++++++++++++++++++ frontend/src/utils/Calendar/Calendar.ts | 3 + 8 files changed, 221 insertions(+), 89 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx index 0b6b8ddd..d47dc17d 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { STUDY_LIST_9 } from 'mocks/mockData'; +import { STUDY_LIST_9_ARRAY } from 'mocks/mockData'; import Calendar from './Calendar'; @@ -21,29 +21,16 @@ export default meta; export const DefaultCalendar: Story = {}; /** - * `Calendar202309`는 2023년 9월 달력으로 외부에서 데이터를 받습니다. + * `Calendar202309`는 2023년 9월 달력으로 외부에서 데이터를 받는 스토리입니다. */ export const Calendar202309: Story = { args: { year: 2023, month: 9, - onChangeCalendar: (year, month) => { - console.log(year, month); - }, - children: Object.entries(STUDY_LIST_9.studyRecords).map(([date, data], index) => { + children: STUDY_LIST_9_ARRAY.map((item, index) => { return ( - window.alert('모달 창 열기')} - onClickDay={() => window.alert('날짜 클릭하기')} - > -
    - {data.slice(0, 3).map(({ studyId, name }) => ( -
  • {name}
  • - ))} -
+ + {item.name} ); }), diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index a05026dd..22826228 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -21,23 +21,44 @@ type Props = { * * @default 11 */ month: number; + /** + * 달력에 렌더링 되는 데이터 형식이 바뀌는 기준 너비를 지정하는 속성. 해당 속성에 따라 Calendar.Item의 fullDataCount가 보여지는 기준이 달라짐. + * + * * @default 750 + */ + formatChangedWidth?: number; /** * 달력의 년, 월이 바뀔 때 호출되는 함수. year, month를 매개변수로 받음. * */ onChangeCalendar?: (year: number, month: number) => void; + /** + * 달력의 Day의 클릭할 때 호출되는 함수. 해당 Day의 Date 객체를 매개변수로 받음. + * + */ + onClickDay?: (date: Date) => void; }; const Calendar = ({ year = new Date().getFullYear(), month = new Date().getMonth() + 1, - onChangeCalendar, + formatChangedWidth = 750, children, + onChangeCalendar, + onClickDay, }: PropsWithChildren) => { const calendarRef = useRef(null); return ( - + diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index abe05d15..8e35ce60 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -1,5 +1,5 @@ -import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; -import { Children, createContext, useContext, useState } from 'react'; +import type { PropsWithChildren, ReactElement, ReactNode, RefObject } from 'react'; +import { Children, createContext, useContext, useEffect, useState } from 'react'; import type { CalendarStorage } from '@Utils/Calendar/Calendar'; import calendar from '@Utils/Calendar/Calendar'; @@ -11,19 +11,24 @@ type CalendarContext = { navigationYear: number; navigationMonth: number; calendarStorage: CalendarStorage; + calendarDataFormat: 'long' | 'short'; isToday: (date: Date) => boolean; shiftMonth: (type: 'next' | 'prev' | 'today') => void; navigateYear: (year: number) => void; navigateMonth: (month: number) => void; navigate: (year?: number, month?: number) => void; getCalendarDayChildren: (date: Date) => ReactNode; + onClickDay?: (date: Date) => void; }; type Props = { initYear: number; initMonth: number; - calendarData: ReactNode; + formatChangedWidth: number; + calendarDataChildren: ReactNode; + calendarRef: RefObject; onChangeCalendar?: (year: number, month: number) => void; + onClickDay?: (date: Date) => void; }; const CalendarContext = createContext(null); @@ -31,18 +36,22 @@ const CalendarContext = createContext(null); const CalendarProvider = ({ initYear, initMonth, - calendarData, + formatChangedWidth, + calendarDataChildren, children, + calendarRef, onChangeCalendar, + onClickDay, }: PropsWithChildren) => { const [year, setYear] = useState(initYear); const [month, setMonth] = useState(initMonth); const [navigationYear, setNavigationYear] = useState(initYear); const [navigationMonth, setNavigationMonth] = useState(initMonth); const [calendarStorage, setCalendarStorage] = useState(calendar.getCalendarStorage(year, month)); + const [calendarDataFormat, setCalendarDataFormat] = useState<'long' | 'short'>('long'); const getCalendarDayChildren = (date: Date) => { - return Children.toArray(calendarData).find((child) => { + return Children.toArray(calendarDataChildren).find((child) => { const item = child as ReactElement; const { date: inputDate } = item.props as { date: string }; @@ -117,18 +126,54 @@ const CalendarProvider = ({ 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]); + + useEffect(() => { + const calendarDataMap: Record = {}; + + Children.forEach(calendarDataChildren, (child) => { + const item = child as ReactElement; + const { date } = item.props as { date: Date }; + + const formatDate = format.date(new Date(date), '-'); + calendarDataMap[formatDate] = calendarDataMap[formatDate] ? [...calendarDataMap[formatDate], item] : [item]; + }); + + setCalendarStorage( + calendar.getCalendarStorage(year, month).map((item) => { + const formatDate = format.date(new Date(item.date), '-'); + + return { ...item, children: calendarDataMap[formatDate] }; + }), + ); + }, [year, month, calendarDataFormat, calendarDataChildren]); + const initValue = { year, month, navigationYear, navigationMonth, calendarStorage, + calendarDataFormat, isToday, shiftMonth, navigateYear, navigateMonth, navigate, getCalendarDayChildren, + onClickDay, }; return {children}; diff --git a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx index 8a446ed3..ef7942b5 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx @@ -1,4 +1,4 @@ -import { useState, type PropsWithChildren, type ReactElement, useEffect } from 'react'; +import { useState, type ReactElement, useEffect } from 'react'; import { styled } from 'styled-components'; import color from '@Styles/color'; @@ -12,17 +12,21 @@ type Props = { date: Date; dayOfWeek: number; state: 'prev' | 'cur' | 'next'; + children?: ReactElement[]; }; }; -const DayItem = ({ data }: PropsWithChildren) => { - const { state, date, day, dayOfWeek } = data; - const { getCalendarDayChildren, isToday } = useCalendar(); +const DayItem = ({ data }: Props) => { + const { state, date, day, dayOfWeek, children } = data; + + const { calendarDataFormat, getCalendarDayChildren, isToday, onClickDay } = useCalendar(); const [calendarDayChildrenProps, setCalendarDayChildrenProps] = useState<{ restDataCount: number; onClickRestDataCount: () => void; onClickDay: () => void; + fullDataCount: number; + onClickFullDataCount: () => void; } | null>(null); const calendarDayChildren = getCalendarDayChildren(date) as ReactElement; @@ -37,6 +41,8 @@ const DayItem = ({ data }: PropsWithChildren) => { restDataCount: number; onClickRestDataCount: () => void; onClickDay: () => void; + fullDataCount: number; + onClickFullDataCount: () => void; }; setCalendarDayChildrenProps(props); }, [calendarDayChildren]); @@ -46,33 +52,29 @@ const DayItem = ({ data }: PropsWithChildren) => { onClickDay && onClickDay(date)} + hasClick={!!onClickDay} isCurrentMonthDay={state === 'cur'} dayOfWeek={dayOfWeek} > {day} - {calendarDayChildrenProps && calendarDayChildrenProps.restDataCount > 0 && ( + {calendarDayChildrenProps && calendarDataFormat === 'long' && calendarDayChildrenProps.restDataCount > 0 && ( +{calendarDayChildrenProps.restDataCount} )} - {/* {calendarDayChildren === 'name' ? ( - - {records.slice(0, 3).map(({ studyId, name }) => ( - handleClickStudyItem(studyId)}> - {name} - - ))} - - ) : ( - openRecordsDetail(format.date(date), records)}> - {records.length > 0 ? {records.length} : ''} + {calendarDayChildrenProps && + calendarDayChildrenProps.fullDataCount && + calendarDayChildren && + calendarDataFormat === 'short' ? ( + + {calendarDayChildrenProps?.fullDataCount} - )} */} - {calendarDayChildren && calendarDayChildren} + ) : ( + children?.map((item) => item) + )} ); }; @@ -109,24 +111,6 @@ const RestRecords = styled.div` cursor: pointer; `; -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; diff --git a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx index 1718dfaa..4219b64c 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx @@ -17,10 +17,15 @@ type Props = { */ onClickRestDataCount?: () => void; /** - * Day을 클릭하면 발생되는 이벤트. Day에 해당하는 데이터가 있는 경우 이벤트를 등록할 수 있음. + * 전체 데이터의 개수를 보여주기 위한 값. 해당 속성을 지정하면 달력의 너비가 750px보다 작아지면 전체 데이터의 개수가 달력에 렌더링 됨. * */ - onClickDay?: () => void; + fullDataCount?: number; + /** + * 전체 데이터의 개수를 클릭하면 발생되는 이벤트. 달력의 너비가 750px보다 작아지면 전체 데이터 개수에 이벤트를 등록할 수 있음. + * + */ + onClickFullDataCount?: () => void; }; const DayItemWrapper = ({ children }: PropsWithChildren) => { diff --git a/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx index 670bb410..c02db897 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx @@ -15,17 +15,6 @@ const DayList = ({ calendarRef }: Props) => { return ( - {/* {isLoading && ( - - - - )} */} {calendarStorage.map((data, index) => ( ))} @@ -35,19 +24,6 @@ const DayList = ({ calendarRef }: Props) => { export default DayList; -const LoadingBar = styled.div` - position: absolute; - - top: 0; - left: 0; - width: 100%; - height: 100%; - - display: flex; - align-items: center; - justify-content: center; -`; - type DaysProps = { $numberOfWeeks: number; }; diff --git a/frontend/src/mocks/mockData.ts b/frontend/src/mocks/mockData.ts index c87c9c47..47411cec 100644 --- a/frontend/src/mocks/mockData.ts +++ b/frontend/src/mocks/mockData.ts @@ -400,6 +400,117 @@ export const STUDY_LIST_8: { }, }; +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< diff --git a/frontend/src/utils/Calendar/Calendar.ts b/frontend/src/utils/Calendar/Calendar.ts index 57685e93..af8e6fea 100644 --- a/frontend/src/utils/Calendar/Calendar.ts +++ b/frontend/src/utils/Calendar/Calendar.ts @@ -1,8 +1,11 @@ +import type { ReactElement } from 'react'; + export type CalendarStorage = { day: number; dayOfWeek: number; date: Date; state: 'prev' | 'next' | 'cur'; + children?: ReactElement[]; }[]; const calendar = { From 20ddc2de4565c4229f1119ee55540bf73f0c8672 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 16:15:10 +0900 Subject: [PATCH 05/27] =?UTF-8?q?feat:=20=EB=8B=AC=EB=A0=A5=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=9C=EC=88=98=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/Calendar/Calendar.stories.tsx | 21 ++++++++ .../common/Calendar/Calendar/Calendar.tsx | 16 +++++- .../CalendarContext/CalendarProvider.tsx | 19 +++---- .../Calendar/Calendar/DayItem/DayItem.tsx | 49 ++++--------------- 4 files changed, 54 insertions(+), 51 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx index d47dc17d..8fd19af0 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -1,6 +1,8 @@ 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; @@ -36,3 +38,22 @@ export const Calendar202309: Story = { }), }, }; + +/** + * `LimitCountCalendar202309`는 데이터의 개수가 제한된 스토리입니다. + */ +export const LimitCountCalendar202309: Story = { + args: { + year: 2023, + month: 9, + limit: 3, + onClickRestData: (date) => window.alert(format.date(new Date(date), '-')), + children: STUDY_LIST_9_ARRAY.map((item, index) => { + return ( + + {item.name} + + ); + }), + }, +}; diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index 22826228..6f198783 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -22,7 +22,12 @@ type Props = { */ month: number; /** - * 달력에 렌더링 되는 데이터 형식이 바뀌는 기준 너비를 지정하는 속성. 해당 속성에 따라 Calendar.Item의 fullDataCount가 보여지는 기준이 달라짐. + * 달력 내 Data 개수를 제한하는 속성. + * + */ + limit?: number; + /** + * 달력에 렌더링 되는 Data 형식이 바뀌는 기준 너비를 지정하는 속성. 지정된 값보다 달력의 너비가 줄어들면 Data의 전체 개수가 렌더링됨. * * * @default 750 */ @@ -37,15 +42,22 @@ type Props = { * */ onClickDay?: (date: Date) => void; + /** + * 달력에 보여지지 않는 Data의 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. + * + */ + onClickRestData?: (date: Date) => void; }; const Calendar = ({ year = new Date().getFullYear(), month = new Date().getMonth() + 1, + limit, formatChangedWidth = 750, children, onChangeCalendar, onClickDay, + onClickRestData, }: PropsWithChildren) => { const calendarRef = useRef(null); @@ -53,11 +65,13 @@ const Calendar = ({ diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index 8e35ce60..da8d3f3e 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -10,6 +10,7 @@ type CalendarContext = { month: number; navigationYear: number; navigationMonth: number; + limit?: number; calendarStorage: CalendarStorage; calendarDataFormat: 'long' | 'short'; isToday: (date: Date) => boolean; @@ -17,18 +18,20 @@ type CalendarContext = { navigateYear: (year: number) => void; navigateMonth: (month: number) => void; navigate: (year?: number, month?: number) => void; - getCalendarDayChildren: (date: Date) => ReactNode; onClickDay?: (date: Date) => void; + onClickRestData?: (date: Date) => void; }; type Props = { initYear: number; initMonth: number; + limit?: number; formatChangedWidth: number; calendarDataChildren: ReactNode; calendarRef: RefObject; onChangeCalendar?: (year: number, month: number) => void; onClickDay?: (date: Date) => void; + onClickRestData?: (date: Date) => void; }; const CalendarContext = createContext(null); @@ -36,12 +39,14 @@ const CalendarContext = createContext(null); const CalendarProvider = ({ initYear, initMonth, + limit, formatChangedWidth, calendarDataChildren, children, calendarRef, onChangeCalendar, onClickDay, + onClickRestData, }: PropsWithChildren) => { const [year, setYear] = useState(initYear); const [month, setMonth] = useState(initMonth); @@ -50,15 +55,6 @@ const CalendarProvider = ({ const [calendarStorage, setCalendarStorage] = useState(calendar.getCalendarStorage(year, month)); const [calendarDataFormat, setCalendarDataFormat] = useState<'long' | 'short'>('long'); - const getCalendarDayChildren = (date: Date) => { - return Children.toArray(calendarDataChildren).find((child) => { - const item = child as ReactElement; - const { date: inputDate } = item.props as { date: string }; - - if (format.date(date, '-') === inputDate) return item; - }); - }; - const isToday = (date: Date) => { const today = format.date(new Date()); const inputDate = format.date(date); @@ -165,6 +161,7 @@ const CalendarProvider = ({ month, navigationYear, navigationMonth, + limit, calendarStorage, calendarDataFormat, isToday, @@ -172,8 +169,8 @@ const CalendarProvider = ({ navigateYear, navigateMonth, navigate, - getCalendarDayChildren, onClickDay, + onClickRestData, }; return {children}; diff --git a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx index ef7942b5..25d44df5 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx @@ -1,4 +1,4 @@ -import { useState, type ReactElement, useEffect } from 'react'; +import { type ReactElement } from 'react'; import { styled } from 'styled-components'; import color from '@Styles/color'; @@ -19,33 +19,9 @@ type Props = { const DayItem = ({ data }: Props) => { const { state, date, day, dayOfWeek, children } = data; - const { calendarDataFormat, getCalendarDayChildren, isToday, onClickDay } = useCalendar(); + const { limit, calendarDataFormat, isToday, onClickDay, onClickRestData } = useCalendar(); - const [calendarDayChildrenProps, setCalendarDayChildrenProps] = useState<{ - restDataCount: number; - onClickRestDataCount: () => void; - onClickDay: () => void; - fullDataCount: number; - onClickFullDataCount: () => void; - } | null>(null); - - const calendarDayChildren = getCalendarDayChildren(date) as ReactElement; - - useEffect(() => { - if (!calendarDayChildren) { - setCalendarDayChildrenProps(null); - return; - } - - const props = calendarDayChildren.props as { - restDataCount: number; - onClickRestDataCount: () => void; - onClickDay: () => void; - fullDataCount: number; - onClickFullDataCount: () => void; - }; - setCalendarDayChildrenProps(props); - }, [calendarDayChildren]); + const renderCalendarItems = limit ? children?.slice(0, limit) : children; return ( @@ -53,27 +29,22 @@ const DayItem = ({ data }: Props) => { onClickDay && onClickDay(date)} - hasClick={!!onClickDay} + hasClick={!onClickDay} isCurrentMonthDay={state === 'cur'} dayOfWeek={dayOfWeek} > {day} - {calendarDayChildrenProps && calendarDataFormat === 'long' && calendarDayChildrenProps.restDataCount > 0 && ( - - +{calendarDayChildrenProps.restDataCount} - + {limit && children && calendarDataFormat === 'long' && children.length - limit > 0 && ( + onClickRestData && onClickRestData(date)}>+{children.length - limit} )} - {calendarDayChildrenProps && - calendarDayChildrenProps.fullDataCount && - calendarDayChildren && - calendarDataFormat === 'short' ? ( - - {calendarDayChildrenProps?.fullDataCount} + {calendarDataFormat === 'short' && children?.length ? ( + + {children?.length} ) : ( - children?.map((item) => item) + renderCalendarItems )} ); From 284070cd896454dc0089eb570917ef0c81f9a95d Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 16:26:28 +0900 Subject: [PATCH 06/27] =?UTF-8?q?teat:=20ClickDayCalendar=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/Calendar/Calendar.stories.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx index 8fd19af0..661bbed7 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -47,7 +47,8 @@ export const LimitCountCalendar202309: Story = { year: 2023, month: 9, limit: 3, - onClickRestData: (date) => window.alert(format.date(new Date(date), '-')), + 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 ( @@ -57,3 +58,14 @@ export const LimitCountCalendar202309: Story = { }), }, }; + +/** + * `LimitCountCalendar202309`는 데이터의 개수가 제한된 스토리입니다. + */ +export const ClickDayCalendar: Story = { + args: { + year: 2023, + month: 9, + onClickDay: (date) => window.alert(format.date(new Date(date), '-')), + }, +}; From f51646cb1be17f496e0ec1687c87bbdc7083ffd7 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 16:26:35 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat:=20onClickTotalDataCount=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/Calendar/Calendar.tsx | 13 ++++++++++--- .../Calendar/CalendarContext/CalendarProvider.tsx | 12 ++++++++---- .../common/Calendar/Calendar/DayItem/DayItem.tsx | 10 ++++++---- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index 6f198783..a2aa1840 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -46,7 +46,12 @@ type Props = { * 달력에 보여지지 않는 Data의 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. * */ - onClickRestData?: (date: Date) => void; + onClickRestDataCount?: (date: Date) => void; + /** + * formatChangedWidth 속성의 값보다 달력의 너비가 줄어들었을 때, 렌덩이 되는 전체 데이터 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. + * + */ + onClickTotalDataCount?: (date: Date) => void; }; const Calendar = ({ @@ -57,7 +62,8 @@ const Calendar = ({ children, onChangeCalendar, onClickDay, - onClickRestData, + onClickRestDataCount, + onClickTotalDataCount, }: PropsWithChildren) => { const calendarRef = useRef(null); @@ -71,7 +77,8 @@ const Calendar = ({ calendarRef={calendarRef} onChangeCalendar={onChangeCalendar} onClickDay={onClickDay} - onClickRestData={onClickRestData} + onClickRestDataCount={onClickRestDataCount} + onClickTotalDataCount={onClickTotalDataCount} > diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index da8d3f3e..ed334890 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -19,7 +19,8 @@ type CalendarContext = { navigateMonth: (month: number) => void; navigate: (year?: number, month?: number) => void; onClickDay?: (date: Date) => void; - onClickRestData?: (date: Date) => void; + onClickRestDataCount?: (date: Date) => void; + onClickTotalDataCount?: (date: Date) => void; }; type Props = { @@ -31,7 +32,8 @@ type Props = { calendarRef: RefObject; onChangeCalendar?: (year: number, month: number) => void; onClickDay?: (date: Date) => void; - onClickRestData?: (date: Date) => void; + onClickRestDataCount?: (date: Date) => void; + onClickTotalDataCount?: (date: Date) => void; }; const CalendarContext = createContext(null); @@ -46,7 +48,8 @@ const CalendarProvider = ({ calendarRef, onChangeCalendar, onClickDay, - onClickRestData, + onClickRestDataCount, + onClickTotalDataCount, }: PropsWithChildren) => { const [year, setYear] = useState(initYear); const [month, setMonth] = useState(initMonth); @@ -170,7 +173,8 @@ const CalendarProvider = ({ navigateMonth, navigate, onClickDay, - onClickRestData, + onClickRestDataCount, + onClickTotalDataCount, }; return {children}; diff --git a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx index 25d44df5..86fa4430 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx @@ -19,7 +19,7 @@ type Props = { const DayItem = ({ data }: Props) => { const { state, date, day, dayOfWeek, children } = data; - const { limit, calendarDataFormat, isToday, onClickDay, onClickRestData } = useCalendar(); + const { limit, calendarDataFormat, isToday, onClickDay, onClickRestDataCount, onClickTotalDataCount } = useCalendar(); const renderCalendarItems = limit ? children?.slice(0, limit) : children; @@ -29,18 +29,20 @@ const DayItem = ({ data }: Props) => { onClickDay && onClickDay(date)} - hasClick={!onClickDay} + hasClick={!!onClickDay} isCurrentMonthDay={state === 'cur'} dayOfWeek={dayOfWeek} > {day} {limit && children && calendarDataFormat === 'long' && children.length - limit > 0 && ( - onClickRestData && onClickRestData(date)}>+{children.length - limit} + onClickRestDataCount && onClickRestDataCount(date)}> + +{children.length - limit} + )} {calendarDataFormat === 'short' && children?.length ? ( - + onClickTotalDataCount && onClickTotalDataCount(date)}> {children?.length} ) : ( From b068f90d465976b7b5d1423325f820756f1a22a8 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 16:32:24 +0900 Subject: [PATCH 08/27] =?UTF-8?q?refactor:=20DayItemWrapper=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/Calendar/Calendar.stories.tsx | 4 +-- .../CalendarContext/CalendarProvider.tsx | 12 ++++---- .../DayItemWrapper/DayItemWrapper.tsx | 28 +++---------------- 3 files changed, 13 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx index 661bbed7..c0d1e0a3 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -31,7 +31,7 @@ export const Calendar202309: Story = { month: 9, children: STUDY_LIST_9_ARRAY.map((item, index) => { return ( - + {item.name} ); @@ -51,7 +51,7 @@ export const LimitCountCalendar202309: Story = { onClickTotalDataCount: (date) => window.alert(format.date(new Date(date), '-')), children: STUDY_LIST_9_ARRAY.map((item, index) => { return ( - + {item.name} ); diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index ed334890..fb94fdfb 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -140,21 +140,23 @@ const CalendarProvider = ({ }, [calendarRef, formatChangedWidth]); useEffect(() => { - const calendarDataMap: Record = {}; + const calendarDataObject: Record = {}; Children.forEach(calendarDataChildren, (child) => { const item = child as ReactElement; const { date } = item.props as { date: Date }; - const formatDate = format.date(new Date(date), '-'); - calendarDataMap[formatDate] = calendarDataMap[formatDate] ? [...calendarDataMap[formatDate], item] : [item]; + const formatDate = format.date(date, '-'); + calendarDataObject[formatDate] = calendarDataObject[formatDate] + ? [...calendarDataObject[formatDate], item] + : [item]; }); setCalendarStorage( calendar.getCalendarStorage(year, month).map((item) => { - const formatDate = format.date(new Date(item.date), '-'); + const formatDate = format.date(item.date, '-'); - return { ...item, children: calendarDataMap[formatDate] }; + return { ...item, children: calendarDataObject[formatDate] }; }), ); }, [year, month, calendarDataFormat, calendarDataChildren]); diff --git a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx index 4219b64c..1b979e04 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx @@ -1,32 +1,12 @@ -import type { PropsWithChildren } from 'react'; +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; type Props = { /** - * 달력에 데이터를 렌더링하기 위한 필수 값. "yyyy-mm-dd" 형식으로 이와 일치하는 곳에 자식 컴포넌트가 렌더링 됨. + * 달력에 데이터를 렌더링하기 위한 필수 값. * */ - date: string; - /** - * 남은 데이터의 개수를 보여주기 위한 값. 0보다 크면 Day 오른쪽에 해당 값이 렌더링 됨. - * - */ - restDataCount?: number; - /** - * 남은 데이터의 개수를 클릭하면 발생되는 이벤트. 남은 데이터의 개수가 0보다 클 경우 이벤트를 등록할 수 있음. - * - */ - onClickRestDataCount?: () => void; - /** - * 전체 데이터의 개수를 보여주기 위한 값. 해당 속성을 지정하면 달력의 너비가 750px보다 작아지면 전체 데이터의 개수가 달력에 렌더링 됨. - * - */ - fullDataCount?: number; - /** - * 전체 데이터의 개수를 클릭하면 발생되는 이벤트. 달력의 너비가 750px보다 작아지면 전체 데이터 개수에 이벤트를 등록할 수 있음. - * - */ - onClickFullDataCount?: () => void; -}; + date: Date; +} & ComponentPropsWithoutRef<'div'>; const DayItemWrapper = ({ children }: PropsWithChildren) => { return
{children}
; From 7b948b77a2860e726d01eb1cb8e1bb073d05964c Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 18:49:32 +0900 Subject: [PATCH 09/27] =?UTF-8?q?refactor:=20Calendar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dataLoading을 위한 props 추가 year, month 옵셔널로 변 renderCalendarItems 스타일 추가 DayItemWrapper에 onClick 메서드 추가 --- .../common/Calendar/Calendar/Calendar.tsx | 12 ++++++-- .../CalendarContext/CalendarProvider.tsx | 5 ++++ .../Calendar/Calendar/DayItem/DayItem.tsx | 8 ++++- .../DayItemWrapper/DayItemWrapper.tsx | 16 ++++++++-- .../Calendar/Calendar/DayList/DayList.tsx | 30 +++++++++++++++++-- 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index a2aa1840..2b61b93f 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -14,13 +14,13 @@ type Props = { * * * @default 2023 */ - year: number; + year?: number; /** * 달력의 월을 지정하는 속성. * * * @default 11 */ - month: number; + month?: number; /** * 달력 내 Data 개수를 제한하는 속성. * @@ -32,10 +32,16 @@ type Props = { * * @default 750 */ formatChangedWidth?: number; + /** + * 달력에 렌더링되는 Data의 로딩 상태를 지정하는 속성 + * + */ + dataLoading?: boolean; /** * 달력의 년, 월이 바뀔 때 호출되는 함수. year, month를 매개변수로 받음. * */ + onChangeCalendar?: (year: number, month: number) => void; /** * 달력의 Day의 클릭할 때 호출되는 함수. 해당 Day의 Date 객체를 매개변수로 받음. @@ -60,6 +66,7 @@ const Calendar = ({ limit, formatChangedWidth = 750, children, + dataLoading = false, onChangeCalendar, onClickDay, onClickRestDataCount, @@ -75,6 +82,7 @@ const Calendar = ({ formatChangedWidth={formatChangedWidth} calendarDataChildren={children} calendarRef={calendarRef} + dataLoading={dataLoading} onChangeCalendar={onChangeCalendar} onClickDay={onClickDay} onClickRestDataCount={onClickRestDataCount} diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index fb94fdfb..f8201054 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -13,6 +13,7 @@ type CalendarContext = { limit?: number; calendarStorage: CalendarStorage; calendarDataFormat: 'long' | 'short'; + dataLoading: boolean; isToday: (date: Date) => boolean; shiftMonth: (type: 'next' | 'prev' | 'today') => void; navigateYear: (year: number) => void; @@ -30,6 +31,7 @@ type Props = { formatChangedWidth: number; calendarDataChildren: ReactNode; calendarRef: RefObject; + dataLoading: boolean; onChangeCalendar?: (year: number, month: number) => void; onClickDay?: (date: Date) => void; onClickRestDataCount?: (date: Date) => void; @@ -46,6 +48,7 @@ const CalendarProvider = ({ calendarDataChildren, children, calendarRef, + dataLoading, onChangeCalendar, onClickDay, onClickRestDataCount, @@ -144,6 +147,7 @@ const CalendarProvider = ({ Children.forEach(calendarDataChildren, (child) => { const item = child as ReactElement; + const { date } = item.props as { date: Date }; const formatDate = format.date(date, '-'); @@ -169,6 +173,7 @@ const CalendarProvider = ({ limit, calendarStorage, calendarDataFormat, + dataLoading, isToday, shiftMonth, navigateYear, diff --git a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx index 86fa4430..2864dec8 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItem/DayItem.tsx @@ -46,7 +46,7 @@ const DayItem = ({ data }: Props) => { {children?.length}
) : ( - renderCalendarItems + {renderCalendarItems} )}
); @@ -116,3 +116,9 @@ const TotalRecordCount = styled.div` } } `; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; diff --git a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx index 1b979e04..36c1c474 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx @@ -1,3 +1,6 @@ +/* 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 = { @@ -6,10 +9,19 @@ type Props = { * */ date: Date; + /** + * Calendar Item를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음. + * + */ + onClickCalendarItem?: (date: Date) => void; } & ComponentPropsWithoutRef<'div'>; -const DayItemWrapper = ({ children }: PropsWithChildren) => { - return
{children}
; +const DayItemWrapper = ({ date, onClickCalendarItem, children, ...rest }: PropsWithChildren) => { + return ( +
onClickCalendarItem && onClickCalendarItem(date)} {...rest}> + {children} +
+ ); }; export default DayItemWrapper; diff --git a/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx index c02db897..4ea98326 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx +++ b/frontend/src/components/common/Calendar/Calendar/DayList/DayList.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { styled } from 'styled-components'; +import { css, styled } from 'styled-components'; + +import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; import color from '@Styles/color'; @@ -11,10 +13,21 @@ type Props = { }; const DayList = ({ calendarRef }: Props) => { - const { calendarStorage } = useCalendar(); + const { calendarStorage, dataLoading } = useCalendar(); return ( + {dataLoading && ( + + + + )} {calendarStorage.map((data, index) => ( ))} @@ -44,3 +57,16 @@ const Layout = styled.ul` 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; +`; From 09d5f398ec3f41b263da05907a116d83f255a109 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 18:50:02 +0900 Subject: [PATCH 10/27] =?UTF-8?q?test:=20Calender=EA=B4=80=EB=A0=A8=20mock?= =?UTF-8?q?Data=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/queryHandler.ts | 24 +-- frontend/src/mocks/mockData.ts | 189 +++++++++++++++----- 2 files changed, 153 insertions(+), 60 deletions(-) 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 47411cec..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,7 +394,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-30T13:33:02.810Z', }; }), }, @@ -522,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', }; }), }, @@ -650,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', }; }), }, From 433bdddd0e075a3a4efeafe78720dc246320a925 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 8 Nov 2023 18:50:49 +0900 Subject: [PATCH 11/27] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20MemberCa?= =?UTF-8?q?lendar=20=EB=B6=80=EB=B6=84=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../record/hooks/useMemberCalendarRecord.ts | 96 ++++---- .../useMemberCalendarRecordSearchParams.ts | 64 ----- .../CalendarDayOfWeeks/CalendarDayOfWeeks.tsx | 0 .../MemberRecordCalendar.tsx | 72 ++++++ .../MemberRecordListModal.tsx | 2 +- .../MemberRecordMode/MemberRecordMode.tsx | 2 +- .../calendar/CalendarDay/CalendarDay.tsx | 72 ------ .../MemberRecordCalendar.tsx | 82 ------- .../MemberRecordCalendarControlBar.tsx | 231 ------------------ .../MemberRecordCalendarDayItem.tsx | 167 ------------- .../MemberRecordCalendarDayList.tsx | 55 ----- .../PeriodSelectCalendar.tsx | 2 +- frontend/src/utils/Calendar/Calendar.ts | 6 + 13 files changed, 122 insertions(+), 729 deletions(-) delete mode 100644 frontend/src/components/record/hooks/useMemberCalendarRecordSearchParams.ts rename frontend/src/components/record/member/{calendar => }/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx (100%) create mode 100644 frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx rename frontend/src/components/record/member/{calendar => }/MemberRecordListModal/MemberRecordListModal.tsx (95%) delete mode 100644 frontend/src/components/record/member/calendar/CalendarDay/CalendarDay.tsx delete mode 100644 frontend/src/components/record/member/calendar/MemberRecordCalendar/MemberRecordCalendar.tsx delete mode 100644 frontend/src/components/record/member/calendar/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx delete mode 100644 frontend/src/components/record/member/calendar/MemberRecordCalendarDayItem/MemberRecordCalendarDayItem.tsx delete mode 100644 frontend/src/components/record/member/calendar/MemberRecordCalendarDayList/MemberRecordCalendarDayList.tsx diff --git a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts index 1aaddab9..649c69e7 100644 --- a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts +++ b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts @@ -3,81 +3,83 @@ 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 calendar from '@Utils/Calendar/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 [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 +88,15 @@ 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); + 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]); + prefetchSidesCalendarData(); + }, [year, month]); - return { calendarRecord, calendarData, isLoading }; + return { year, 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/member/calendar/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx b/frontend/src/components/record/member/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx similarity index 100% rename from frontend/src/components/record/member/calendar/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx rename to frontend/src/components/record/member/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx 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..ca20c581 --- /dev/null +++ b/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx @@ -0,0 +1,72 @@ +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 format from '@Utils/format'; + +import MemberRecordListModal from '../MemberRecordListModal/MemberRecordListModal'; + +type Props = { + memberId: string; +}; + +const MemberRecordCalendar = ({ memberId }: Props) => { + const navigate = useNavigate(); + const { openModal } = useModal(); + + const { year, month, calendarData, isLoading, getStudies, updateYearMonth } = useMemberCalendarRecord(memberId); + + const handleClickStudyItem = (studyId: string) => navigate(`${ROUTES_PATH.record}/${studyId}`); + + const handleOpenMemberRecordListModal = (date: Date) => { + 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/CalendarDay/CalendarDay.tsx b/frontend/src/components/record/member/calendar/CalendarDay/CalendarDay.tsx deleted file mode 100644 index 1d09a153..00000000 --- a/frontend/src/components/record/member/calendar/CalendarDay/CalendarDay.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; -import { css, styled } from 'styled-components'; - -import color from '@Styles/color'; - -type Props = { - hasStudy?: boolean; - isToday: boolean; - isCurrentMonthDay: boolean; - dayOfWeek: number; -} & ComponentPropsWithoutRef<'div'>; - -const CalendarDay = ({ - children, - hasStudy = false, - isToday, - isCurrentMonthDay, - dayOfWeek, - ...rest -}: PropsWithChildren) => { - const getDayFontColor = (dayOfWeek: number) => { - if (dayOfWeek === 0) return color.red[600]; - - if (dayOfWeek === 6) return color.blue[600]; - - return color.black; - }; - - return ( - - {children} - - ); -}; - -export default CalendarDay; - -type DayProps = { - $isToday: boolean; - $hasStudy: boolean; - $isCurrentMonthDay: boolean; - $fontColor: string; -}; - -const Day = styled.div` - display: flex; - justify-content: center; - align-items: center; - - border-radius: 50%; - - width: 30px; - height: 30px; - - ${({ $isToday, $hasStudy, $isCurrentMonthDay, $fontColor }) => css` - background-color: ${$isToday && color.neutral[100]}; - opacity: ${$isCurrentMonthDay ? 1 : 0.4}; - color: ${$fontColor}; - cursor: ${$hasStudy && 'pointer'}; - `} - - @media screen and (max-width: 360px) { - margin: 0 auto; - margin-top: 5px; - } -`; 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/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx b/frontend/src/components/record/member/calendar/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx deleted file mode 100644 index 558a964e..00000000 --- a/frontend/src/components/record/member/calendar/MemberRecordCalendarControlBar/MemberRecordCalendarControlBar.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { useState } from 'react'; -import { css, styled } from 'styled-components'; - -import Typography from '@Components/common/Typography/Typography'; - -import useOutsideClick from '@Hooks/common/useOutsideClick'; - -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; -}; - -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); - setIsOpenCalendarNavigation(false); - }; - - return ( - - setIsOpenCalendarNavigation((prev) => !prev)}> - {year}년 {month}월 - - - - handleClickMonthShiftButton('prev')}> - - - handleClickMonthShiftButton('next')}> - - - handleClickMonthShiftButton('today')}>오늘 - - {isOpenCalendarNavigation && ( - - -
{navigationYear}
- - handleNavigationYear('prev')} /> - handleNavigationYear('next')} /> - -
- - {Array.from({ length: 12 }).map((_, index) => ( - handleClickMonthNavigation(index + 1)} - > - {index + 1}월 - - ))} - -
- )} -
- ); -}; - -export default MemberRecordCalendarControlBar; - -const Layout = styled.div` - position: relative; - - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 10px; - - width: fit-content; - - p { - width: 170px; - - padding: 0px 3px; - - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - - border-radius: 4px; - - transition: background-color 0.2s ease; - - cursor: pointer; - - &:hover { - background-color: ${color.neutral[100]}; - } - } - - @media screen and (max-width: 768px) { - font-size: 1.4rem; - } -`; - -const MonthShiftButtonContainer = styled.div` - display: flex; - align-items: center; - gap: 10px; - - margin-left: 20px; - - @media screen and (max-width: 768px) { - margin-left: 0px; - } -`; - -const MonthShiftButton = styled.div` - padding: 8px; - border-radius: 50%; - - border: 1px solid ${color.neutral[200]}; - - cursor: pointer; -`; - -const ShiftTodayButton = styled.div` - padding: 4px 16px; - border-radius: 16px; - - border: 1px solid ${color.neutral[200]}; - - cursor: pointer; -`; - -const CalendarNavigation = styled.div` - position: absolute; - top: 40px; - - background-color: ${color.white}; - - padding: 10px; - border: 1px solid ${color.neutral[100]}; - border-radius: 4px; - - box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; - - z-index: 5; -`; - -const YearNavigation = styled.div` - display: flex; - justify-content: space-between; - - padding: 10px; - - & > div { - font-weight: 700; - } -`; - -const YearNavigationButton = styled.div` - display: flex; - gap: 20px; - - opacity: 0.6; - - svg { - cursor: pointer; - } -`; - -const MonthNavigation = styled.ul` - display: grid; - grid-template-columns: repeat(4, 80px); - row-gap: 5px; - justify-items: center; - - padding: 10px; - - @media screen and (max-width: 768px) { - grid-template-columns: repeat(4, 60px); - } -`; - -type MonthProps = { - $isCurMonth: boolean; -}; - -const Month = styled.li` - padding: 10px 20px; - border-radius: 4px; - - transition: background-color 0.2s ease; - - cursor: pointer; - - ${({ $isCurMonth }) => css` - color: ${$isCurMonth ? color.blue[500] : color.neutral[600]}; - font-weight: ${$isCurMonth ? 500 : 300}; - `} - - &:hover { - background-color: ${color.blue[100]}; - } - - @media screen and (max-width: 768px) { - padding: 4px 8px; - } -`; 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..96a5b46e 100644 --- a/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx +++ b/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx @@ -11,7 +11,7 @@ import ArrowIcon from '@Assets/icons/ArrowIcon'; import format from '@Utils/format'; -import CalendarDayOfWeeks from '../../calendar/CalendarDayOfWeeks/CalendarDayOfWeeks'; +import CalendarDayOfWeeks from '../../CalendarDayOfWeeks/CalendarDayOfWeeks'; const MENU_STYLE = css` & > div { diff --git a/frontend/src/utils/Calendar/Calendar.ts b/frontend/src/utils/Calendar/Calendar.ts index af8e6fea..cb819a34 100644 --- a/frontend/src/utils/Calendar/Calendar.ts +++ b/frontend/src/utils/Calendar/Calendar.ts @@ -17,6 +17,12 @@ const calendar = { ]; }, + getMonthFirstLastDate: (year: number, month: number) => { + const calendarStorage = calendar.getCalendarStorage(year, month); + + return [calendarStorage[0], calendarStorage.at(-1)]; + }, + getPrevMonthLastWeekDays: (year: number, month: number): CalendarStorage => { const prevMonthLastDateObject = new Date(year, month - 1, 0); From 5e0c1cc0a929231913823f5a1eaa6bc93736aee8 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 10:02:48 +0900 Subject: [PATCH 12/27] =?UTF-8?q?feat:=20render=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=85=84,=20=EC=9B=94=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/Calendar/Calendar.stories.tsx | 7 ++- .../common/Calendar/Calendar/Calendar.tsx | 12 ++-- .../CalendarContext/CalendarProvider.tsx | 59 ++++++------------- .../record/hooks/useMemberCalendarRecord.ts | 15 ++++- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx index c0d1e0a3..6e42b671 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx @@ -20,7 +20,12 @@ export default meta; /** * `DefaultCalendar`는 현재 년, 월을 렌더링한 기본적인 Calendar의 스토리입니다. */ -export const DefaultCalendar: Story = {}; +export const DefaultCalendar: Story = { + args: { + year: 2023, + month: 11, + }, +}; /** * `Calendar202309`는 2023년 9월 달력으로 외부에서 데이터를 받는 스토리입니다. diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index 2b61b93f..a62427ad 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -14,13 +14,13 @@ type Props = { * * * @default 2023 */ - year?: number; + year: number; /** * 달력의 월을 지정하는 속성. * * * @default 11 */ - month?: number; + month: number; /** * 달력 내 Data 개수를 제한하는 속성. * @@ -61,8 +61,8 @@ type Props = { }; const Calendar = ({ - year = new Date().getFullYear(), - month = new Date().getMonth() + 1, + year, + month, limit, formatChangedWidth = 750, children, @@ -76,8 +76,8 @@ const Calendar = ({ return ( (null); const CalendarProvider = ({ - initYear, - initMonth, + year, + month, limit, formatChangedWidth, calendarDataChildren, @@ -54,11 +54,8 @@ const CalendarProvider = ({ onClickRestDataCount, onClickTotalDataCount, }: PropsWithChildren) => { - const [year, setYear] = useState(initYear); - const [month, setMonth] = useState(initMonth); - const [navigationYear, setNavigationYear] = useState(initYear); - const [navigationMonth, setNavigationMonth] = useState(initMonth); - const [calendarStorage, setCalendarStorage] = useState(calendar.getCalendarStorage(year, month)); + const [navigationYear, setNavigationYear] = useState(year); + const [navigationMonth, setNavigationMonth] = useState(month); const [calendarDataFormat, setCalendarDataFormat] = useState<'long' | 'short'>('long'); const isToday = (date: Date) => { @@ -95,13 +92,9 @@ const CalendarProvider = ({ newMonth = changedMonth; } - setYear(newYear); - setMonth(newMonth); setNavigationYear(newYear); setNavigationMonth(newMonth); - setCalendarStorage(calendar.getCalendarStorage(newYear, newMonth)); - if (onChangeCalendar) onChangeCalendar(newYear, newMonth); }; @@ -111,20 +104,10 @@ const CalendarProvider = ({ const navigate = (year?: number, month?: number) => { if (year && month) { - setYear(year); - setMonth(month); - - setCalendarStorage(calendar.getCalendarStorage(year, month)); - if (onChangeCalendar) onChangeCalendar(year, month); return; } - setYear(navigationYear); - setMonth(navigationMonth); - - setCalendarStorage(calendar.getCalendarStorage(navigationYear, navigationMonth)); - if (onChangeCalendar) onChangeCalendar(navigationYear, navigationMonth); }; @@ -142,28 +125,24 @@ const CalendarProvider = ({ calendarResizeObserver.observe(calendarRef.current); }, [calendarRef, formatChangedWidth]); - useEffect(() => { - const calendarDataObject: Record = {}; + const calendarDataObject: Record = {}; - Children.forEach(calendarDataChildren, (child) => { - const item = child as ReactElement; + Children.forEach(calendarDataChildren, (child) => { + const item = child as ReactElement; - const { date } = item.props as { date: Date }; + const { date } = item.props as { date: Date }; - const formatDate = format.date(date, '-'); - calendarDataObject[formatDate] = calendarDataObject[formatDate] - ? [...calendarDataObject[formatDate], item] - : [item]; - }); + const formatDate = format.date(date, '-'); + calendarDataObject[formatDate] = calendarDataObject[formatDate] + ? [...calendarDataObject[formatDate], item] + : [item]; + }); - setCalendarStorage( - calendar.getCalendarStorage(year, month).map((item) => { - const formatDate = format.date(item.date, '-'); + const calendarStorage = calendar.getCalendarStorage(year, month).map((item) => { + const formatDate = format.date(item.date, '-'); - return { ...item, children: calendarDataObject[formatDate] }; - }), - ); - }, [year, month, calendarDataFormat, calendarDataChildren]); + return { ...item, children: calendarDataObject[formatDate] }; + }); const initValue = { year, diff --git a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts index 649c69e7..6e055c6c 100644 --- a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts +++ b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts @@ -21,6 +21,9 @@ const useMemberCalendarRecord = (memberId: string) => { const year = Number(searchParams.year); const month = Number(searchParams.month); + const [renderYear, setRenderYear] = useState(null); + const [renderMonth, setRenderMonth] = useState(null); + const [calendarData, setCalendarData] = useState(null); const [startDate, endDate] = calendar.getMonthFirstLastDate(year, month).map((item) => { @@ -89,6 +92,9 @@ const useMemberCalendarRecord = (memberId: string) => { const studyRecords = result.data.studyRecords; + setRenderYear(year); + setRenderMonth(month); + setCalendarData(Object.values(studyRecords).flat()); }, [result]); @@ -96,7 +102,14 @@ const useMemberCalendarRecord = (memberId: string) => { prefetchSidesCalendarData(); }, [year, month]); - return { year, month, calendarData, isLoading, getStudies, updateYearMonth }; + return { + year: renderYear || year, + month: renderMonth || month, + calendarData, + isLoading, + getStudies, + updateYearMonth, + }; }; export default useMemberCalendarRecord; From 4a57bdf4fbf049aa144de35b0855ee14a3af9084 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 10:14:00 +0900 Subject: [PATCH 13/27] =?UTF-8?q?test:=20DatePicker=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatePicker/DatePicker.stories.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx 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..023b66d8 --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import DatePicker from './DatePIcker'; + +type Story = StoryObj; + +/** + * `DatePicker`는 일정과 같이 day에 대한 정보를 제공하는 달력 컴포넌트입니다. + */ +const meta: Meta = { + title: 'COMPONENTS/DatePicker', + component: DatePicker, +}; + +export default meta; + +/** + * `DefaultDatePicker`는 현재 년, 월을 렌더링한 기본적인 DatePicker의 스토리입니다. + */ +export const DefaultDatePicker: Story = {}; From df24596334ed7c49262ae20ecd2a3683374b533c Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 13:31:10 +0900 Subject: [PATCH 14/27] =?UTF-8?q?feat:=20DatePicker=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatePicker/DatePicker.stories.tsx | 6 +- .../common/Calendar/DatePicker/DatePicker.tsx | 320 ++++++++++++++++++ 2 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index 023b66d8..fbfefb21 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -1,11 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react'; -import DatePicker from './DatePIcker'; +import DatePicker from './DatePicker'; type Story = StoryObj; /** - * `DatePicker`는 일정과 같이 day에 대한 정보를 제공하는 달력 컴포넌트입니다. + * `DatePicker`는 날짜를 선택할 수 있는 달력 컴포넌트입니다. */ const meta: Meta = { title: 'COMPONENTS/DatePicker', @@ -15,6 +15,6 @@ const meta: Meta = { export default meta; /** - * `DefaultDatePicker`는 현재 년, 월을 렌더링한 기본적인 DatePicker의 스토리입니다. + * `DefaultDatePicker`는 기본적인 DatePicker의 스토리입니다. */ export const DefaultDatePicker: Story = {}; 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..71639a7f --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -0,0 +1,320 @@ +import { useState } from 'react'; +import { styled, css } from 'styled-components'; + +import Menu from '@Components/common/Menu/Menu'; + +import color from '@Styles/color'; + +import ArrowIcon from '@Assets/icons/ArrowIcon'; + +import calendar from '@Utils/Calendar/Calendar'; +import format from '@Utils/format'; + +import DayOfWeeks from '../Calendar/DayOfWeeks/DayOfWeeks'; + +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; +`; + +type Props = { + startDate?: Date | null; + endDate?: Date | null; +}; + +const DatePicker = ({ startDate, endDate }: Props) => { + const [start, setStart] = useState(startDate); + const [end, setEnd] = useState(endDate); + + const today = new Date(); + + const [year, setYear] = useState(start ? start.getFullYear() : today.getFullYear()); + const [month, setMonth] = useState(start ? start.getMonth() + 1 : today.getMonth() + 1); + const [hoveredDay, setHoveredDay] = useState(null); + + const calendarStorage = calendar.getCalendarStorage(year, month); + + const handleMonthShift = (type: 'next' | 'prev' | 'today') => { + if (type === 'today') { + const today = new Date(); + + setYear(today.getFullYear()); + setMonth(today.getMonth() + 1); + + return; + } + + const changedMonth = month + (type === 'next' ? +1 : -1); + + if (changedMonth === 0) { + setYear((prev) => prev - 1); + setMonth(12); + return; + } + + if (changedMonth === 13) { + setYear((prev) => prev + 1); + setMonth(1); + return; + } + + setMonth(changedMonth); + }; + + const handleYearShift = (year: number) => setYear(year); + + const handleNavigationMonth = (month: number) => { + setMonth(month); + }; + + const getDayBackgroundColor = (date: Date, fullDate: string) => { + if (start && format.date(start, '-') === fullDate) return color.blue[200]; + + if (end && format.date(end, '-') === 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 || !start) return false; + + if (hoveredDay > start) { + if (start <= date && hoveredDay >= date) return true; + + return false; + } else { + if (start >= date && hoveredDay <= date) return true; + + return false; + } + }; + + const isIncludeSelectDate = (date: Date) => { + if (!start || !end) return false; + + if (new Date(start) < date && new Date(end) >= date) return true; + + return false; + }; + + const updateHoverDays = (date: Date) => { + if (!start) return; + if (start && end) return; + + setHoveredDay(date); + }; + + const updateStartEndDate = (date: Date) => { + setHoveredDay(date); + + let newStartDate: null | Date = null; + let newEndDate: null | Date = null; + + if (!start) newStartDate = date; + + if (start && !end && new Date(start) > date) { + newStartDate = date; + newEndDate = start; + } + + if (start && !end && new Date(start) < date) { + newStartDate = start; + newEndDate = date; + } + + if (start && end) newStartDate = date; + + setStart(newStartDate); + setEnd(newEndDate); + }; + + 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')} /> + + + + + {calendarStorage.map(({ day, state, date }, index) => ( + updateStartEndDate(date)} + onMouseEnter={() => updateHoverDays(date)} + $backgroundColor={getDayBackgroundColor(date, format.date(date, '-'))} + > + {day} + + ))} + + + ); +}; + +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; + + 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` + position: relative; + top: 2px; + + 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); + + margin-top: 10px; +`; + +type DayProps = { + $isCurrentMonthDay: boolean; + $backgroundColor: string; +}; + +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 }) => css` + opacity: ${$isCurrentMonthDay ? 1 : 0.4}; + background-color: ${$backgroundColor}; + `} +`; From e1928fbcce6f752f7d26e26ba529687e3d931c09 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 14:32:06 +0900 Subject: [PATCH 15/27] =?UTF-8?q?refactor:=20DatePicker=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?Provider=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/Calendar/Calendar.tsx | 2 +- .../CalendarContext/CalendarProvider.tsx | 2 +- .../DatePicker/ControlBar/ControlBar.tsx | 141 ++++++++ .../common/Calendar/DatePicker/DatePicker.tsx | 307 +----------------- .../DatePickerContext/DatePickerProvider.tsx | 166 ++++++++++ .../Calendar/DatePicker/DayList/DayList.tsx | 60 ++++ .../DayOfWeeks/DayOfWeeks.tsx | 0 7 files changed, 383 insertions(+), 295 deletions(-) create mode 100644 frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx create mode 100644 frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx create mode 100644 frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx rename frontend/src/components/common/Calendar/{Calendar => common}/DayOfWeeks/DayOfWeeks.tsx (100%) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index a62427ad..deb32df1 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -6,7 +6,7 @@ import CalendarProvider from './CalendarContext/CalendarProvider'; import ControlBar from './ControlBar/ControlBar'; import DayItemWrapper from './DayItemWrapper/DayItemWrapper'; import DayList from './DayList/DayList'; -import DayOfWeeks from './DayOfWeeks/DayOfWeeks'; +import DayOfWeeks from '../common/DayOfWeeks/DayOfWeeks'; type Props = { /** diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index 59814323..30026a97 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -171,7 +171,7 @@ export default CalendarProvider; export const useCalendar = () => { const value = useContext(CalendarContext); - if (!value) throw new Error('calendar가 적절하지 않는 곳에서 호출되었습니다.'); + if (!value) throw new Error('적절하지 않는 곳에서 useCalendar를 호출했습니다.'); return value; }; 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..1b860f7f --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx @@ -0,0 +1,141 @@ +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` + position: relative; + top: 2px; + + 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.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index 71639a7f..6eeaa65b 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -1,210 +1,26 @@ -import { useState } from 'react'; -import { styled, css } from 'styled-components'; - -import Menu from '@Components/common/Menu/Menu'; +import { styled } from 'styled-components'; import color from '@Styles/color'; -import ArrowIcon from '@Assets/icons/ArrowIcon'; - -import calendar from '@Utils/Calendar/Calendar'; -import format from '@Utils/format'; - -import DayOfWeeks from '../Calendar/DayOfWeeks/DayOfWeeks'; - -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; -`; +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; + startDate?: Date; + endDate?: Date; }; const DatePicker = ({ startDate, endDate }: Props) => { - const [start, setStart] = useState(startDate); - const [end, setEnd] = useState(endDate); - - const today = new Date(); - - const [year, setYear] = useState(start ? start.getFullYear() : today.getFullYear()); - const [month, setMonth] = useState(start ? start.getMonth() + 1 : today.getMonth() + 1); - const [hoveredDay, setHoveredDay] = useState(null); - - const calendarStorage = calendar.getCalendarStorage(year, month); - - const handleMonthShift = (type: 'next' | 'prev' | 'today') => { - if (type === 'today') { - const today = new Date(); - - setYear(today.getFullYear()); - setMonth(today.getMonth() + 1); - - return; - } - - const changedMonth = month + (type === 'next' ? +1 : -1); - - if (changedMonth === 0) { - setYear((prev) => prev - 1); - setMonth(12); - return; - } - - if (changedMonth === 13) { - setYear((prev) => prev + 1); - setMonth(1); - return; - } - - setMonth(changedMonth); - }; - - const handleYearShift = (year: number) => setYear(year); - - const handleNavigationMonth = (month: number) => { - setMonth(month); - }; - - const getDayBackgroundColor = (date: Date, fullDate: string) => { - if (start && format.date(start, '-') === fullDate) return color.blue[200]; - - if (end && format.date(end, '-') === 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 || !start) return false; - - if (hoveredDay > start) { - if (start <= date && hoveredDay >= date) return true; - - return false; - } else { - if (start >= date && hoveredDay <= date) return true; - - return false; - } - }; - - const isIncludeSelectDate = (date: Date) => { - if (!start || !end) return false; - - if (new Date(start) < date && new Date(end) >= date) return true; - - return false; - }; - - const updateHoverDays = (date: Date) => { - if (!start) return; - if (start && end) return; - - setHoveredDay(date); - }; - - const updateStartEndDate = (date: Date) => { - setHoveredDay(date); - - let newStartDate: null | Date = null; - let newEndDate: null | Date = null; - - if (!start) newStartDate = date; - - if (start && !end && new Date(start) > date) { - newStartDate = date; - newEndDate = start; - } - - if (start && !end && new Date(start) < date) { - newStartDate = start; - newEndDate = date; - } - - if (start && end) newStartDate = date; - - setStart(newStartDate); - setEnd(newEndDate); - }; - 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')} /> - - - - - {calendarStorage.map(({ day, state, date }, index) => ( - updateStartEndDate(date)} - onMouseEnter={() => updateHoverDays(date)} - $backgroundColor={getDayBackgroundColor(date, format.date(date, '-'))} - > - {day} - - ))} - - + + + + + + + ); }; @@ -223,98 +39,3 @@ const Layout = styled.div` 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` - position: relative; - top: 2px; - - 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); - - margin-top: 10px; -`; - -type DayProps = { - $isCurrentMonthDay: boolean; - $backgroundColor: string; -}; - -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 }) => css` - opacity: ${$isCurrentMonthDay ? 1 : 0.4}; - background-color: ${$backgroundColor}; - `} -`; 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..e1bb21da --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -0,0 +1,166 @@ +import type { PropsWithChildren } from 'react'; +import { createContext, useContext, useState } from 'react'; + +import color from '@Styles/color'; + +import type { CalendarStorage } from '@Utils/Calendar/Calendar'; +import calendar from '@Utils/Calendar/Calendar'; +import format from '@Utils/format'; + +type DatePickerContext = { + startDate?: Date; + endDate?: Date; + year: number; + month: number; + calendarStorage: CalendarStorage; + 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; +}; + +const DatePickerContext = createContext(null); + +type Props = { + initStartDate?: Date; + initEndDate?: Date; +}; + +const DatePickerProvider = ({ initStartDate, initEndDate, children }: PropsWithChildren) => { + const [startDate, setStart] = useState(initStartDate); + const [endDate, setEnd] = useState(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(); + + const calendarStorage = calendar.getCalendarStorage(year, month); + + const handleMonthShift = (type: 'next' | 'prev' | 'today') => { + if (type === 'today') { + const today = new Date(); + + setYear(today.getFullYear()); + setMonth(today.getMonth() + 1); + + return; + } + + const changedMonth = month + (type === 'next' ? +1 : -1); + + if (changedMonth === 0) { + setYear((prev) => prev - 1); + setMonth(12); + return; + } + + if (changedMonth === 13) { + setYear((prev) => prev + 1); + setMonth(1); + return; + } + + setMonth(changedMonth); + }; + + 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 (!startDate) return; + if (startDate && endDate) return; + + setHoveredDay(date); + }; + + const updateStartEndDate = (date: Date) => { + setHoveredDay(date); + + let newStartDate: undefined | Date = undefined; + let newEndDate: undefined | Date = undefined; + + 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); + }; + + const initValue = { + startDate, + endDate, + year, + month, + calendarStorage, + handleMonthShift, + handleNavigationYear, + handleNavigationMonth, + getDayBackgroundColor, + updateHoverDays, + updateStartEndDate, + }; + + 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..151787ad --- /dev/null +++ b/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx @@ -0,0 +1,60 @@ +import { css, styled } from 'styled-components'; + +import { useDatePicker } from '../DatePickerContext/DatePickerProvider'; + +const DayList = () => { + const { calendarStorage, getDayBackgroundColor, updateHoverDays, updateStartEndDate } = useDatePicker(); + + return ( + + {calendarStorage.map(({ day, state, date }, index) => ( + updateStartEndDate(date)} + onMouseEnter={() => updateHoverDays(date)} + $backgroundColor={getDayBackgroundColor(date)} + > + {day} + + ))} + + ); +}; + +export default DayList; + +const Layout = styled.ul` + display: grid; + row-gap: 5px; + grid-template-columns: repeat(7, 1fr); + + margin-top: 10px; +`; + +type DayProps = { + $isCurrentMonthDay: boolean; + $backgroundColor: string; +}; + +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 }) => css` + opacity: ${$isCurrentMonthDay ? 1 : 0.4}; + background-color: ${$backgroundColor}; + `} +`; diff --git a/frontend/src/components/common/Calendar/Calendar/DayOfWeeks/DayOfWeeks.tsx b/frontend/src/components/common/Calendar/common/DayOfWeeks/DayOfWeeks.tsx similarity index 100% rename from frontend/src/components/common/Calendar/Calendar/DayOfWeeks/DayOfWeeks.tsx rename to frontend/src/components/common/Calendar/common/DayOfWeeks/DayOfWeeks.tsx From fb516360cb59f7c9337ebad4a49433839e02d2e0 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 14:35:41 +0900 Subject: [PATCH 16/27] =?UTF-8?q?test:=20StartEndDatePicker=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/DatePicker/DatePicker.stories.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index fbfefb21..c24ebfda 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -18,3 +18,13 @@ 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'), + }, +}; From 6a83e631965d9bab3aff7933850828aebb30440c Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 14:51:39 +0900 Subject: [PATCH 17/27] =?UTF-8?q?feat:=20onChangeDate=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/Calendar/Calendar.tsx | 1 - .../DatePicker/DatePicker.stories.tsx | 12 +++++++ .../common/Calendar/DatePicker/DatePicker.tsx | 17 +++++++-- .../DatePickerContext/DatePickerProvider.tsx | 35 +++++++++++-------- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index deb32df1..8cedb706 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -41,7 +41,6 @@ type Props = { * 달력의 년, 월이 바뀔 때 호출되는 함수. year, month를 매개변수로 받음. * */ - onChangeCalendar?: (year: number, month: number) => void; /** * 달력의 Day의 클릭할 때 호출되는 함수. 해당 Day의 Date 객체를 매개변수로 받음. diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index c24ebfda..0d8747ec 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import type { Meta, StoryObj } from '@storybook/react'; import DatePicker from './DatePicker'; @@ -28,3 +29,14 @@ export const StartEndDatePicker: Story = { endDate: new Date('2023-11-09'), }, }; + +/** + * `OnChangeDatePicker`는 startDate와 endDate를 onChangeDate 속성을 통해 받아올 수 있는 DatePicker의 스토리입니다. + */ +export const OnChangeDatePicker: Story = { + args: { + onChangeDate: (startDate, endDate) => { + window.alert(`${startDate || ''}, ${endDate || ''}`); + }, + }, +}; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index 6eeaa65b..6b592c83 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -8,13 +8,26 @@ import DayList from './DayList/DayList'; import DayOfWeeks from '../common/DayOfWeeks/DayOfWeeks'; type Props = { + /** + * 시작일을 지정하는 속성. + * + */ startDate?: Date; + /** + * 마지막일을 지정하는 속성. + * + */ endDate?: Date; + /** + * startDate, endDate가 바뀔 때 호출되는 함수. startDate, endDate를 매개변수로 받음. + * + */ + onChangeDate?: (startDate?: Date, endDate?: Date) => void; }; -const DatePicker = ({ startDate, endDate }: Props) => { +const DatePicker = ({ startDate, endDate, onChangeDate }: Props) => { return ( - + diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index e1bb21da..742c1ba9 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -26,9 +26,10 @@ const DatePickerContext = createContext(null); type Props = { initStartDate?: Date; initEndDate?: Date; + onChangeDate?: (startDate?: Date, endDate?: Date) => void; }; -const DatePickerProvider = ({ initStartDate, initEndDate, children }: PropsWithChildren) => { +const DatePickerProvider = ({ initStartDate, initEndDate, children, onChangeDate }: PropsWithChildren) => { const [startDate, setStart] = useState(initStartDate); const [endDate, setEnd] = useState(initEndDate); @@ -41,30 +42,34 @@ const DatePickerProvider = ({ initStartDate, initEndDate, children }: PropsWithC const calendarStorage = calendar.getCalendarStorage(year, month); const handleMonthShift = (type: 'next' | 'prev' | 'today') => { + let newYear = year; + let newMonth = month; + if (type === 'today') { const today = new Date(); - setYear(today.getFullYear()); - setMonth(today.getMonth() + 1); - - return; + newYear = today.getFullYear(); + newMonth = today.getMonth() + 1; } const changedMonth = month + (type === 'next' ? +1 : -1); - if (changedMonth === 0) { - setYear((prev) => prev - 1); - setMonth(12); - return; + if (type !== 'today' && changedMonth === 0) { + newYear -= 1; + newMonth = 12; } - if (changedMonth === 13) { - setYear((prev) => prev + 1); - setMonth(1); - return; + if (type !== 'today' && changedMonth === 13) { + newYear += 1; + newMonth = 1; } - setMonth(changedMonth); + if (type !== 'today' && changedMonth > 0 && changedMonth < 13) { + newMonth = changedMonth; + } + + setYear(newYear); + setMonth(newMonth); }; const handleNavigationYear = (year: number) => setYear(year); @@ -136,6 +141,8 @@ const DatePickerProvider = ({ initStartDate, initEndDate, children }: PropsWithC setStart(newStartDate); setEnd(newEndDate); + + if (onChangeDate) onChangeDate(newStartDate, newEndDate); }; const initValue = { From 476cc60e154b32f9d514961406bef9e3df984f0e Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 16:04:41 +0900 Subject: [PATCH 18/27] =?UTF-8?q?feat:=20DatePicker=20mode=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatePicker/DatePicker.stories.tsx | 9 +++ .../common/Calendar/DatePicker/DatePicker.tsx | 9 ++- .../DatePickerContext/DatePickerProvider.tsx | 52 ++++++++----- .../Calendar/DatePicker/DayList/DayList.tsx | 77 ++++++++++++++----- 4 files changed, 109 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index 0d8747ec..0f59012e 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -40,3 +40,12 @@ export const OnChangeDatePicker: Story = { }, }, }; + +/** + * `DoubleDatePicker`는 두 개의 DatePicker 달력을 보여주는 스토리입니다. + */ +export const DoubleDatePicker: Story = { + args: { + mode: 'double', + }, +}; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index 6b592c83..329a9386 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -18,6 +18,11 @@ type Props = { * */ endDate?: Date; + /** + * 달력의 개수를 지정하는 속성 + * + */ + mode?: 'single' | 'double'; /** * startDate, endDate가 바뀔 때 호출되는 함수. startDate, endDate를 매개변수로 받음. * @@ -25,9 +30,9 @@ type Props = { onChangeDate?: (startDate?: Date, endDate?: Date) => void; }; -const DatePicker = ({ startDate, endDate, onChangeDate }: Props) => { +const DatePicker = ({ startDate, endDate, mode = 'single', onChangeDate }: Props) => { return ( - + diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index 742c1ba9..b6353316 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -13,6 +13,8 @@ type DatePickerContext = { 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; @@ -26,10 +28,11 @@ const DatePickerContext = createContext(null); type Props = { initStartDate?: Date; initEndDate?: Date; + mode: 'single' | 'double'; onChangeDate?: (startDate?: Date, endDate?: Date) => void; }; -const DatePickerProvider = ({ initStartDate, initEndDate, children, onChangeDate }: PropsWithChildren) => { +const DatePickerProvider = ({ initStartDate, initEndDate, mode, children, onChangeDate }: PropsWithChildren) => { const [startDate, setStart] = useState(initStartDate); const [endDate, setEnd] = useState(initEndDate); @@ -41,32 +44,43 @@ const DatePickerProvider = ({ initStartDate, initEndDate, children, onChangeDate const calendarStorage = calendar.getCalendarStorage(year, month); - const handleMonthShift = (type: 'next' | 'prev' | 'today') => { - let newYear = year; - let newMonth = 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(); - newYear = today.getFullYear(); - newMonth = today.getMonth() + 1; - } + const newYear = today.getFullYear(); + const newMonth = today.getMonth() + 1; - const changedMonth = month + (type === 'next' ? +1 : -1); + setYear(newYear); + setMonth(newMonth); - if (type !== 'today' && changedMonth === 0) { - newYear -= 1; - newMonth = 12; + return; } - if (type !== 'today' && changedMonth === 13) { - newYear += 1; - newMonth = 1; - } + const getMonth = () => { + let number = 0; - if (type !== 'today' && changedMonth > 0 && changedMonth < 13) { - newMonth = changedMonth; - } + 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); @@ -150,7 +164,9 @@ const DatePickerProvider = ({ initStartDate, initEndDate, children, onChangeDate endDate, year, month, + mode, calendarStorage, + nextCalendarInformation, handleMonthShift, handleNavigationYear, handleNavigationMonth, diff --git a/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx b/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx index 151787ad..6dd265da 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DayList/DayList.tsx @@ -3,22 +3,53 @@ import { css, styled } from 'styled-components'; import { useDatePicker } from '../DatePickerContext/DatePickerProvider'; const DayList = () => { - const { calendarStorage, getDayBackgroundColor, updateHoverDays, updateStartEndDate } = useDatePicker(); + const { calendarStorage, nextCalendarInformation, getDayBackgroundColor, updateHoverDays, updateStartEndDate } = + useDatePicker(); return ( - - {calendarStorage.map(({ day, state, date }, index) => ( - updateStartEndDate(date)} - onMouseEnter={() => updateHoverDays(date)} - $backgroundColor={getDayBackgroundColor(date)} - > - {day} - - ))} - + <> + + {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} + + ))} + + + )} + ); }; @@ -28,13 +59,12 @@ const Layout = styled.ul` display: grid; row-gap: 5px; grid-template-columns: repeat(7, 1fr); - - margin-top: 10px; `; type DayProps = { $isCurrentMonthDay: boolean; $backgroundColor: string; + $isTransparent?: boolean; }; const Day = styled.li` @@ -53,8 +83,19 @@ const Day = styled.li` transition: background-color 0.1s ease; - ${({ $isCurrentMonthDay, $backgroundColor }) => css` - opacity: ${$isCurrentMonthDay ? 1 : 0.4}; + ${({ $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; +`; From ae79e7f1d110a33edf9164a3a8d33242deb4ab8a Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 16:19:21 +0900 Subject: [PATCH 19/27] =?UTF-8?q?feat:=20DatePicker=20hasButton=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConfirmCancelButton.tsx | 45 +++++++++++++++++++ .../DatePicker/DatePicker.stories.tsx | 11 +++++ .../common/Calendar/DatePicker/DatePicker.tsx | 36 ++++++++++++++- .../DatePickerContext/DatePickerProvider.tsx | 16 ++++++- 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/common/Calendar/DatePicker/ConfirmCancelButton/ConfirmCancelButton.tsx 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/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index 0f59012e..711d7de1 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -49,3 +49,14 @@ export const DoubleDatePicker: Story = { mode: 'double', }, }; + +/** + * `ButtonDatePicker`는 확인, 취소 버튼이 있는 DatePicker 스토리입니다. + */ +export const ButtonDatePicker: Story = { + args: { + hasButton: true, + onClickConfirm: (startDate, endDate) => console.log(startDate, endDate), + onClickCancel: () => window.alert('취소'), + }, +}; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index 329a9386..98dee54e 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -2,6 +2,7 @@ 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'; @@ -23,20 +24,51 @@ type Props = { * */ mode?: 'single' | 'double'; + /** + * Date 선택 후 확인, 취소 버튼을 통해 startDate, endDate를 반환할 수 있는 버튼을 지정하는 속성. + * + */ + hasButton?: boolean; /** * startDate, endDate가 바뀔 때 호출되는 함수. startDate, endDate를 매개변수로 받음. * */ onChangeDate?: (startDate?: Date, endDate?: Date) => void; + /** + * Date 선택 후 확인버튼을 누를 때 호출되는 함수. startDate, endDate를 매개변수로 받음. + * + */ + onClickConfirm?: (startDate?: Date, endDate?: Date) => void; + /** + * Date 선택 후 취소버튼을 누를 때 호출되는 함수. startDate, endDate를 매개변수로 받음. + * + */ + onClickCancel?: () => void; }; -const DatePicker = ({ startDate, endDate, mode = 'single', onChangeDate }: Props) => { +const DatePicker = ({ + startDate, + endDate, + mode = 'single', + hasButton = false, + onClickCancel, + onClickConfirm, + onChangeDate, +}: Props) => { return ( - + + {hasButton && } ); diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index b6353316..42bf987d 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -21,6 +21,8 @@ type DatePickerContext = { getDayBackgroundColor: (date: Date) => string; updateHoverDays: (date: Date) => void; updateStartEndDate: (date: Date) => void; + onClickConfirm?: (startDate?: Date, endDate?: Date) => void; + onClickCancel?: () => void; }; const DatePickerContext = createContext(null); @@ -30,9 +32,19 @@ type Props = { initEndDate?: Date; mode: 'single' | 'double'; onChangeDate?: (startDate?: Date, endDate?: Date) => void; + onClickConfirm?: (startDate?: Date, endDate?: Date) => void; + onClickCancel?: () => void; }; -const DatePickerProvider = ({ initStartDate, initEndDate, mode, children, onChangeDate }: PropsWithChildren) => { +const DatePickerProvider = ({ + initStartDate, + initEndDate, + mode, + children, + onChangeDate, + onClickConfirm, + onClickCancel, +}: PropsWithChildren) => { const [startDate, setStart] = useState(initStartDate); const [endDate, setEnd] = useState(initEndDate); @@ -173,6 +185,8 @@ const DatePickerProvider = ({ initStartDate, initEndDate, mode, children, onChan getDayBackgroundColor, updateHoverDays, updateStartEndDate, + onClickConfirm, + onClickCancel, }; return {children}; From bad2088ec57d60f2896da2c652733bc5239b84dc Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 9 Nov 2023 16:33:34 +0900 Subject: [PATCH 20/27] =?UTF-8?q?feat:=20DatePicker=20isOnlyOneDay=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/Calendar/Calendar.tsx | 3 +-- .../Calendar/DatePicker/DatePicker.stories.tsx | 10 ++++++++++ .../common/Calendar/DatePicker/DatePicker.tsx | 13 ++++++++++++- .../DatePickerContext/DatePickerProvider.tsx | 10 ++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index 8cedb706..918ee38e 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -12,13 +12,11 @@ type Props = { /** * 달력의 년도를 지정하는 속성. * - * * @default 2023 */ year: number; /** * 달력의 월을 지정하는 속성. * - * * @default 11 */ month: number; /** @@ -35,6 +33,7 @@ type Props = { /** * 달력에 렌더링되는 Data의 로딩 상태를 지정하는 속성 * + * * @default false */ dataLoading?: boolean; /** diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index 711d7de1..1c9a62f3 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -60,3 +60,13 @@ export const ButtonDatePicker: Story = { onClickCancel: () => window.alert('취소'), }, }; + +/** + * `OnlyOneDayDatePicker`는 특정 하루만 선택할 수 있는 DatePicker 스토리입니다. + */ +export const OnlyOneDayDatePicker: Story = { + args: { + isOnlyOneDay: true, + onChangeDate: (date) => console.log(date), + }, +}; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index 98dee54e..221ee8cd 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -12,6 +12,7 @@ type Props = { /** * 시작일을 지정하는 속성. * + * */ startDate?: Date; /** @@ -22,13 +23,21 @@ type Props = { /** * 달력의 개수를 지정하는 속성 * + * * @default "single" */ mode?: 'single' | 'double'; /** * Date 선택 후 확인, 취소 버튼을 통해 startDate, endDate를 반환할 수 있는 버튼을 지정하는 속성. * + * * @default false */ hasButton?: boolean; + /** + * 하루를 선택할지 혹은 기간을 선택할지를 지정하는 속성. + * + * * @default false + */ + isOnlyOneDay?: boolean; /** * startDate, endDate가 바뀔 때 호출되는 함수. startDate, endDate를 매개변수로 받음. * @@ -51,6 +60,7 @@ const DatePicker = ({ endDate, mode = 'single', hasButton = false, + isOnlyOneDay = false, onClickCancel, onClickConfirm, onChangeDate, @@ -59,8 +69,9 @@ const DatePicker = ({ diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index 42bf987d..5e1710d3 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -31,6 +31,7 @@ type Props = { initStartDate?: Date; initEndDate?: Date; mode: 'single' | 'double'; + isOnlyOneDay: boolean; onChangeDate?: (startDate?: Date, endDate?: Date) => void; onClickConfirm?: (startDate?: Date, endDate?: Date) => void; onClickCancel?: () => void; @@ -41,6 +42,7 @@ const DatePickerProvider = ({ initEndDate, mode, children, + isOnlyOneDay, onChangeDate, onClickConfirm, onClickCancel, @@ -139,6 +141,8 @@ const DatePickerProvider = ({ }; const updateHoverDays = (date: Date) => { + if (isOnlyOneDay) return; + if (!startDate) return; if (startDate && endDate) return; @@ -146,6 +150,12 @@ const DatePickerProvider = ({ }; const updateStartEndDate = (date: Date) => { + if (isOnlyOneDay) { + setStart(date); + if (onChangeDate) onChangeDate(date); + return; + } + setHoveredDay(date); let newStartDate: undefined | Date = undefined; From 151f86af9c0fa2328a5b72540ce08b1fa8688941 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 10 Nov 2023 16:28:34 +0900 Subject: [PATCH 21/27] =?UTF-8?q?refactor:=20DatePicker=20startDate,=20end?= =?UTF-8?q?Date=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatePicker/ControlBar/ControlBar.tsx | 3 --- .../common/Calendar/DatePicker/DatePicker.tsx | 10 ++++----- .../DatePickerContext/DatePickerProvider.tsx | 22 +++++++++---------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx b/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx index 1b860f7f..58100097 100644 --- a/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/ControlBar/ControlBar.tsx @@ -125,9 +125,6 @@ const ShiftButton = styled.div` `; const TodayButton = styled.div` - position: relative; - top: 2px; - cursor: pointer; `; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index 221ee8cd..f33d8108 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -14,12 +14,12 @@ type Props = { * * */ - startDate?: Date; + startDate: Date | null; /** * 마지막일을 지정하는 속성. * */ - endDate?: Date; + endDate: Date | null; /** * 달력의 개수를 지정하는 속성 * @@ -42,12 +42,12 @@ type Props = { * startDate, endDate가 바뀔 때 호출되는 함수. startDate, endDate를 매개변수로 받음. * */ - onChangeDate?: (startDate?: Date, endDate?: Date) => void; + onChangeDate?: (startDate: Date | null, endDate: Date | null) => void; /** * Date 선택 후 확인버튼을 누를 때 호출되는 함수. startDate, endDate를 매개변수로 받음. * */ - onClickConfirm?: (startDate?: Date, endDate?: Date) => void; + onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; /** * Date 선택 후 취소버튼을 누를 때 호출되는 함수. startDate, endDate를 매개변수로 받음. * @@ -97,6 +97,4 @@ const Layout = styled.div` border-radius: 4px; box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; - - z-index: 5; `; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index 5e1710d3..0357c73c 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -8,8 +8,8 @@ import calendar from '@Utils/Calendar/Calendar'; import format from '@Utils/format'; type DatePickerContext = { - startDate?: Date; - endDate?: Date; + startDate: Date | null; + endDate: Date | null; year: number; month: number; calendarStorage: CalendarStorage; @@ -21,19 +21,19 @@ type DatePickerContext = { getDayBackgroundColor: (date: Date) => string; updateHoverDays: (date: Date) => void; updateStartEndDate: (date: Date) => void; - onClickConfirm?: (startDate?: Date, endDate?: Date) => void; + onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; onClickCancel?: () => void; }; const DatePickerContext = createContext(null); type Props = { - initStartDate?: Date; - initEndDate?: Date; + initStartDate: Date | null; + initEndDate: Date | null; mode: 'single' | 'double'; isOnlyOneDay: boolean; - onChangeDate?: (startDate?: Date, endDate?: Date) => void; - onClickConfirm?: (startDate?: Date, endDate?: Date) => void; + onChangeDate?: (startDate: Date | null, endDate: Date | null) => void; + onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; onClickCancel?: () => void; }; @@ -54,7 +54,7 @@ const DatePickerProvider = ({ const [year, setYear] = useState(startDate ? startDate.getFullYear() : today.getFullYear()); const [month, setMonth] = useState(startDate ? startDate.getMonth() + 1 : today.getMonth() + 1); - const [hoveredDay, setHoveredDay] = useState(); + const [hoveredDay, setHoveredDay] = useState(null); const calendarStorage = calendar.getCalendarStorage(year, month); @@ -152,14 +152,14 @@ const DatePickerProvider = ({ const updateStartEndDate = (date: Date) => { if (isOnlyOneDay) { setStart(date); - if (onChangeDate) onChangeDate(date); + if (onChangeDate) onChangeDate(date, endDate); return; } setHoveredDay(date); - let newStartDate: undefined | Date = undefined; - let newEndDate: undefined | Date = undefined; + let newStartDate: null | Date = null; + let newEndDate: null | Date = null; if (!startDate) newStartDate = date; From 37ece7cd3111fd85146cbd5ff4a5a5891e901457 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 10 Nov 2023 16:28:46 +0900 Subject: [PATCH 22/27] =?UTF-8?q?refactor:=20DatePicker=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/index.ts | 4 +- .../contexts/MemberRecordPeriodProvider.tsx | 71 +----- .../record/hooks/useMemberListRecord.ts | 13 +- .../PeriodSelectCalendar.tsx | 216 +----------------- .../PeriodSelectionBar/PeriodSelectionBar.tsx | 6 +- 5 files changed, 31 insertions(+), 279 deletions(-) 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/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/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/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx b/frontend/src/components/record/member/period/PeriodSelectCalendar/PeriodSelectCalendar.tsx index 96a5b46e..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 '../../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, '-')}
) : ( '날짜를 선택해주세요.' From 964e8d8800157d45c2985b05ee90bf62b02b848e Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 10 Nov 2023 17:59:46 +0900 Subject: [PATCH 23/27] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=EC=9D=B4=20=EC=97=86=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EB=AA=A8=EB=8B=AC=20=EB=8C=80=EC=8B=A0=20=EB=85=B8?= =?UTF-8?q?=ED=8B=B0=EB=A1=9C=20=EC=95=8C=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarDayOfWeeks/CalendarDayOfWeeks.tsx | 50 ------------------- .../MemberRecordCalendar.tsx | 13 +++++ 2 files changed, 13 insertions(+), 50 deletions(-) delete mode 100644 frontend/src/components/record/member/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx diff --git a/frontend/src/components/record/member/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx b/frontend/src/components/record/member/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx deleted file mode 100644 index 9617bd51..00000000 --- a/frontend/src/components/record/member/CalendarDayOfWeeks/CalendarDayOfWeeks.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { css, styled } from 'styled-components'; - -import color from '@Styles/color'; - -const DAY_COLOR = { - 일: color.red[600], - 월: color.black, - 화: color.black, - 수: color.black, - 목: color.black, - 금: color.black, - 토: color.blue[600], -} as const; - -const DAY_OF_WEEKS = ['일', '월', '화', '수', '목', '금', '토'] as const; - -const CalendarDayOfWeeks = ({ position = 'left' }: { position?: 'left' | 'center' }) => { - return ( - - {DAY_OF_WEEKS.map((dayOfWeek) => ( - - {dayOfWeek} - - ))} - - ); -}; - -export default CalendarDayOfWeeks; - -const Layout = styled.ul` - display: flex; -`; - -type DayOfWeekProps = { - $dayOfWeek: (typeof DAY_OF_WEEKS)[number]; - position: 'left' | 'center'; -}; - -const DayOfWeek = styled.li` - flex: 1; - - color: ${color.black}; - - ${({ $dayOfWeek, position }) => css` - color: ${position === 'left' ? DAY_COLOR[$dayOfWeek] : color.black}; - margin-left: ${position === 'left' && '5px'}; - text-align: ${position === 'center' && 'center'}; - `} -`; diff --git a/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx b/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx index ca20c581..3d6f023c 100644 --- a/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx +++ b/frontend/src/components/record/member/MemberRecordCalendar/MemberRecordCalendar.tsx @@ -9,6 +9,7 @@ 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'; @@ -21,12 +22,24 @@ type Props = { 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( Date: Fri, 10 Nov 2023 18:00:50 +0900 Subject: [PATCH 24/27] =?UTF-8?q?refactor:=20DatePicker=20Props=20?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20isOnlyOneDay?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20endDate=20=EB=AC=B4=ED=9A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Calendar/DatePicker/DatePicker.stories.tsx | 4 +++- .../components/common/Calendar/DatePicker/DatePicker.tsx | 9 ++++----- .../DatePicker/DatePickerContext/DatePickerProvider.tsx | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx index 1c9a62f3..75363e9f 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.stories.tsx @@ -55,7 +55,7 @@ export const DoubleDatePicker: Story = { */ export const ButtonDatePicker: Story = { args: { - hasButton: true, + showButtons: true, onClickConfirm: (startDate, endDate) => console.log(startDate, endDate), onClickCancel: () => window.alert('취소'), }, @@ -68,5 +68,7 @@ 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 index f33d8108..e1b93b62 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -12,7 +12,6 @@ type Props = { /** * 시작일을 지정하는 속성. * - * */ startDate: Date | null; /** @@ -31,9 +30,9 @@ type Props = { * * * @default false */ - hasButton?: boolean; + showButtons?: boolean; /** - * 하루를 선택할지 혹은 기간을 선택할지를 지정하는 속성. + * 하루를 선택할지 혹은 기간을 선택할지를 지정하는 속성. 해당 속성을 true로 할 경우 endDate 속성은 무시됨. * * * @default false */ @@ -59,7 +58,7 @@ const DatePicker = ({ startDate, endDate, mode = 'single', - hasButton = false, + showButtons = false, isOnlyOneDay = false, onClickCancel, onClickConfirm, @@ -79,7 +78,7 @@ const DatePicker = ({ - {hasButton && } + {showButtons && }
); diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index 0357c73c..3b181c15 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -48,7 +48,7 @@ const DatePickerProvider = ({ onClickCancel, }: PropsWithChildren) => { const [startDate, setStart] = useState(initStartDate); - const [endDate, setEnd] = useState(initEndDate); + const [endDate, setEnd] = useState(isOnlyOneDay ? null : initEndDate); const today = new Date(); From 97c832084ddde46b1e067ed04fa1f6371d60806e Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sat, 11 Nov 2023 14:47:03 +0900 Subject: [PATCH 25/27] =?UTF-8?q?refactor:=20DayItemWarpper=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20CalendarItem=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Calendar/Calendar/Calendar.tsx | 4 ++-- .../DayItemWrapper.tsx => CalendarItem/CalendarItem.tsx} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/components/common/Calendar/Calendar/{DayItemWrapper/DayItemWrapper.tsx => CalendarItem/CalendarItem.tsx} (82%) diff --git a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx index 918ee38e..417c000b 100644 --- a/frontend/src/components/common/Calendar/Calendar/Calendar.tsx +++ b/frontend/src/components/common/Calendar/Calendar/Calendar.tsx @@ -3,8 +3,8 @@ 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 DayItemWrapper from './DayItemWrapper/DayItemWrapper'; import DayList from './DayList/DayList'; import DayOfWeeks from '../common/DayOfWeeks/DayOfWeeks'; @@ -97,7 +97,7 @@ const Calendar = ({ ); }; -Calendar.Item = DayItemWrapper; +Calendar.Item = CalendarItem; export default Calendar; diff --git a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarItem/CalendarItem.tsx similarity index 82% rename from frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx rename to frontend/src/components/common/Calendar/Calendar/CalendarItem/CalendarItem.tsx index 36c1c474..88610a1d 100644 --- a/frontend/src/components/common/Calendar/Calendar/DayItemWrapper/DayItemWrapper.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarItem/CalendarItem.tsx @@ -16,7 +16,7 @@ type Props = { onClickCalendarItem?: (date: Date) => void; } & ComponentPropsWithoutRef<'div'>; -const DayItemWrapper = ({ date, onClickCalendarItem, children, ...rest }: PropsWithChildren) => { +const CalendarItem = ({ date, onClickCalendarItem, children, ...rest }: PropsWithChildren) => { return (
onClickCalendarItem && onClickCalendarItem(date)} {...rest}> {children} @@ -24,4 +24,4 @@ const DayItemWrapper = ({ date, onClickCalendarItem, children, ...rest }: PropsW ); }; -export default DayItemWrapper; +export default CalendarItem; From 36793cd85c7eeedb5f2f5eb9697bedc9ebf56a56 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sat, 11 Nov 2023 15:30:34 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refctor:=20calendar=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarContext/CalendarProvider.tsx | 4 +- .../DatePickerContext/DatePickerProvider.tsx | 4 +- .../record/hooks/useMemberCalendarRecord.ts | 2 +- frontend/src/hooks/common/useCalendar.ts | 68 ----- frontend/src/utils/Calendar/Calendar.ts | 243 ------------------ frontend/src/utils/calendar.ts | 102 ++++---- 6 files changed, 52 insertions(+), 371 deletions(-) delete mode 100644 frontend/src/hooks/common/useCalendar.ts delete mode 100644 frontend/src/utils/Calendar/Calendar.ts diff --git a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx index 30026a97..4c435f20 100644 --- a/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx +++ b/frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx @@ -1,8 +1,8 @@ import type { PropsWithChildren, ReactElement, ReactNode, RefObject } from 'react'; import { Children, createContext, useContext, useEffect, useState } from 'react'; -import type { CalendarStorage } from '@Utils/Calendar/Calendar'; -import calendar from '@Utils/Calendar/Calendar'; +import type { CalendarStorage } from '@Utils/calendar'; +import calendar from '@Utils/calendar'; import format from '@Utils/format'; type CalendarContext = { diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index 3b181c15..d96fee51 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -3,8 +3,8 @@ import { createContext, useContext, useState } from 'react'; import color from '@Styles/color'; -import type { CalendarStorage } from '@Utils/Calendar/Calendar'; -import calendar from '@Utils/Calendar/Calendar'; +import type { CalendarStorage } from '@Utils/calendar'; +import calendar from '@Utils/calendar'; import format from '@Utils/format'; type DatePickerContext = { diff --git a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts index 6e055c6c..3d7731e2 100644 --- a/frontend/src/components/record/hooks/useMemberCalendarRecord.ts +++ b/frontend/src/components/record/hooks/useMemberCalendarRecord.ts @@ -5,7 +5,7 @@ import useCacheFetch from '@Hooks/api/useCacheFetch'; import usePreFetch from '@Hooks/api/usePreFetch'; import useSearchParams from '@Hooks/common/useSearchParams'; -import calendar from '@Utils/Calendar/Calendar'; +import calendar from '@Utils/calendar'; import format from '@Utils/format'; import { requestGetMemberCalendarRecord } from '@Apis/index'; 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/utils/Calendar/Calendar.ts b/frontend/src/utils/Calendar/Calendar.ts deleted file mode 100644 index cb819a34..00000000 --- a/frontend/src/utils/Calendar/Calendar.ts +++ /dev/null @@ -1,243 +0,0 @@ -import type { ReactElement } from 'react'; - -export type CalendarStorage = { - day: number; - dayOfWeek: number; - date: Date; - state: 'prev' | 'next' | 'cur'; - children?: ReactElement[]; -}[]; - -const calendar = { - 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 calendarStorage = calendar.getCalendarStorage(year, month); - - return [calendarStorage[0], calendarStorage.at(-1)]; - }, - - getPrevMonthLastWeekDays: (year: number, month: number): CalendarStorage => { - const prevMonthLastDateObject = new Date(year, month - 1, 0); - - const prevYear = prevMonthLastDateObject.getFullYear(); // 이전 달의 년도 - const prevMonth = prevMonthLastDateObject.getMonth(); // 이전 달 - const prevLastDayOfWeek = prevMonthLastDateObject.getDay(); // 이전 달의 마지막 요일 - const prevLastDay = prevMonthLastDateObject.getDate(); // 이전 달의 마지막 일 - - if (prevLastDayOfWeek === 6) return []; // 이전 달의 마지막 요일이 토요일인 경우 - - return Array.from({ length: prevLastDayOfWeek + 1 }).map((_, index) => { - const day = prevLastDay - prevLastDayOfWeek + index; - const dayOfWeek = index; - - return { - day, - dayOfWeek, - date: new Date(prevYear, prevMonth, day), - state: 'prev', - }; - }); - }, - - 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: curLastDay }).map((_, index) => { - const day = index + 1; // 일은 index에 1을 더한 값 - const dayOfWeek = (curFirstDayOfWeek + index) % 7; // 첫 번째 요일과 index를 더한 값을 7로 나눈 값의 나머지 - - return { - day, - dayOfWeek, - date: new Date(year, month - 1, day), - state: 'cur', - }; - }); - }, - - 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 (nextFirstDayOfWeek === 0) return []; // 다음 달의 첫 번재 날이 일요일인 경우 - - return Array.from({ length: 7 - nextFirstDayOfWeek }).map((_, index) => { - const day = nextFirstDay + index; - const dayOfWeek = nextFirstDayOfWeek + index; - - return { - day, - dayOfWeek, - date: new Date(nextYear, nextMonth, day), - state: 'next', - }; - }); - }, -}; - -export default calendar; - -// class Calendar { -// private year; - -// private month; - -// private navigationYear; - -// private navigationMonth; - -// constructor(year: number, month: number) { -// this.year = year; -// this.month = month; -// this.navigationYear = year; -// this.navigationMonth = month; -// } - -// getCalendarStorage = () => { -// return [ -// ...this.getPrevMonthLastWeekDays(this.year, this.month), -// ...this.getCurMonthDays(this.year, this.month), -// ...this.getNextMonthFirstWeekDays(this.year, this.month), -// ]; -// }; - -// getYear = () => this.year; - -// getMonth = () => this.month; - -// isToday = (date: Date) => { -// const today = format.date(new Date()); -// const inputDate = format.date(date); - -// return today === inputDate; -// }; - -// shiftMonth = (type: 'next' | 'prev' | 'today') => { -// let newYear = this.year; -// let newMonth = this.month; - -// if (type === 'today') { -// const today = new Date(); - -// newYear = today.getFullYear(); -// newMonth = today.getMonth() + 1; -// } - -// const changedMonth = this.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; -// } - -// this.year = newYear; -// this.navigationYear = newYear; -// this.month = newMonth; -// this.navigationMonth = newMonth; -// }; - -// navigateYear = (year: number) => (this.navigationYear = year); - -// navigateMonth = (month: number) => (this.navigationMonth = month); - -// navigate = () => { -// this.year = this.navigationYear; -// this.month = this.navigationMonth; -// }; - -// getNavigationYear = () => this.navigationYear; - -// getNavigationMonth = () => this.navigationMonth; - -// private getPrevMonthLastWeekDays = (year: number, month: number): CalendarStorage => { -// const prevMonthLastDateObject = new Date(year, month - 1, 0); - -// const prevYear = prevMonthLastDateObject.getFullYear(); // 이전 달의 년도 -// const prevMonth = prevMonthLastDateObject.getMonth(); // 이전 달 -// const prevLastDayOfWeek = prevMonthLastDateObject.getDay(); // 이전 달의 마지막 요일 -// const prevLastDay = prevMonthLastDateObject.getDate(); // 이전 달의 마지막 일 - -// if (prevLastDayOfWeek === 6) return []; // 이전 달의 마지막 요일이 토요일인 경우 - -// return Array.from({ length: prevLastDayOfWeek + 1 }).map((_, index) => { -// const day = prevLastDay - prevLastDayOfWeek + index; -// const dayOfWeek = index; - -// return { -// day, -// dayOfWeek, -// date: new Date(prevYear, prevMonth, day), -// state: 'prev', -// }; -// }); -// }; - -// private 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: curLastDay }).map((_, index) => { -// const day = index + 1; // 일은 index에 1을 더한 값 -// const dayOfWeek = (curFirstDayOfWeek + index) % 7; // 첫 번째 요일과 index를 더한 값을 7로 나눈 값의 나머지 - -// return { -// day, -// dayOfWeek, -// date: new Date(year, month - 1, day), -// state: 'cur', -// }; -// }); -// }; - -// private 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 (nextFirstDayOfWeek === 0) return []; // 다음 달의 첫 번재 날이 일요일인 경우 - -// return Array.from({ length: 7 - nextFirstDayOfWeek }).map((_, index) => { -// const day = nextFirstDay + index; -// const dayOfWeek = nextFirstDayOfWeek + index; - -// return { -// day, -// dayOfWeek, -// date: new Date(nextYear, nextMonth, day), -// state: 'next', -// }; -// }); -// }; -// } - -// export default Calendar; 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, From 2ae6316676f348c71226a055a0180950c8c3e676 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 12 Nov 2023 14:03:49 +0900 Subject: [PATCH 27/27] =?UTF-8?q?refactor:=20DatePicker=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20getDayBackgroundColor=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Calendar/DatePicker/DatePicker.tsx | 2 +- .../DatePicker/DatePickerContext/DatePickerProvider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx index e1b93b62..90bdfcb5 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePicker.tsx @@ -48,7 +48,7 @@ type Props = { */ onClickConfirm?: (startDate: Date | null, endDate: Date | null) => void; /** - * Date 선택 후 취소버튼을 누를 때 호출되는 함수. startDate, endDate를 매개변수로 받음. + * Date 선택 후 취소버튼을 누를 때 호출되는 함수. * */ onClickCancel?: () => void; diff --git a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx index d96fee51..86fe6ee1 100644 --- a/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx +++ b/frontend/src/components/common/Calendar/DatePicker/DatePickerContext/DatePickerProvider.tsx @@ -113,7 +113,7 @@ const DatePickerProvider = ({ if (isSoonSelectedDate(date) || isIncludeSelectDate(date)) return color.blue[100]; - if (fullDate === format.date(today)) return color.neutral[100]; + if (fullDate === format.date(today, '-')) return color.neutral[100]; return 'transparent'; };