[Typescript] Calendar 만들기

JunSeok·2023년 8월 13일
0
post-thumbnail

React-Calendar와 같은 Calendar 라이브러리가 있지만, date를 다루는 date-fns 자바스크립트 라이브러리를 사용해서 내가 원하는대로 만들어봤다.
date-fns는 npm 다운로드 수가 1600만(8월 13일 기준)이 넘어가고 179개의 버전이 있는만큼 공신력있고 꾸준히 업데이트를 하고 있는 라이브러리다. 공식문서가 매우 잘 되어 있고, 있을 법한 함수를 모두 찾아 사용할 수 있다.

구조

전체적인 구조는 Header | Days | Cells Component로 나누고, 이 3개의 Component를 감싸는 Calendar Component가 있다.
Calendar Component

Calendar Componenet

Calendar Component에서는 Calendar에 필요한 변수와 함수를 선언하고 Header | Days | Cells Component를 감싼다.

  • 필요 변수
    -현재 몇 월인지 체크할 currentMonth
    -내가 선택한 날짜 저장할 selectedDate
  • 필요 함수
    -선택한 날짜 저장할 함수 onDateClick
    -지난달, 다음달로 이동할 함수 preMonth, nextMonth
    -현재 날짜로 이동할 함수 goToday
    -options(날짜를 클릭하면 띄울 modal)
import { addMonths, format, subMonths } from "date-fns"

import CalendarCells from "./CalendarCells"
import CalendarDays from "./CalendarDays"
import CalendarHeader from "./CalendarHeader"
import { useState } from "react"

const MoneyBookCalendar = () => {
    const [currentMonth, setCurrentMonth] = useState(new Date())
    const [selectedDate, setSelectedDate] = useState("")
    const onDateClick = (day: Date) => setSelectedDate(format(day, "yyyy-MM-dd"))
    const preMonth = () => setCurrentMonth(subMonths(currentMonth, 1))
    const nextMonth = () => setCurrentMonth(addMonths(currentMonth, 1))
    const goToday = () => setCurrentMonth(new Date())
    const [click, setModal] = useState(false)
    const clickModal = () => setModal(!click)
    return (
        <section className="bg-white min-w-[62.5rem] lg:ml-[14rem] ml-[3.5rem] flex justify-center items-center pb-10" onClick={() => setModal(false)}>
            <div className="flex flex-col items-center justify-center w-full max-w-[65rem] lg:px-10 px-10" onClick={(e) => e.stopPropagation()}>
                <div className="w-full gap-3 mt-10 mb-5 text-4xl font-semibold">
                    Calendar
                </div>
                <CalendarHeader currentMonth={currentMonth} preMonth={preMonth} nextMonth={nextMonth} goToday={goToday} />
                <CalendarDays />
                <CalendarCells click={click} clickModal={clickModal} currentMonth={currentMonth} onDateClick={onDateClick} selectedDate={selectedDate} />
            </div>
        </section>
    )
}

export default MoneyBookCalendar

Header Component

Header Component에서는 현재달을 보여주고, 지난달, 오늘, 다음달로 넘어갈 수 있는 기능을 넣는다.

import { HiOutlineArrowCircleLeft, HiOutlineArrowCircleRight } from 'react-icons/hi'

import format from 'date-fns/format'

type Props = {
    currentMonth: Date,
    preMonth: () => void,
    nextMonth: () => void,
    goToday: () => void
}

const CalendarHeader = ({ currentMonth, preMonth, nextMonth, goToday }: Props) => {
    return (
        <div className='flex items-center justify-between w-full gap-3 p-5 text-3xl font-semibold border rounded rounded-b-none'>
            <div className='gap-3'>
                <span className='mr-3'>
                    {format(currentMonth, 'yyyy')}</span>
                <span>
                    {format(currentMonth, 'M')}</span>
            </div>
            <div className='flex items-center gap-2 cursor-pointer'>
                <HiOutlineArrowCircleLeft onClick={preMonth} />
                <button className='text-lg' onClick={goToday}>오늘</button>
                <HiOutlineArrowCircleRight onClick={nextMonth} />
            </div>
        </div>
    )
}

export default CalendarHeader

Days Component

Days Component는 요일을 표시한다.

const CalendarDays = () => {
    const date = ['일', '월', '화', '수', '목', '금', '토']
    return (
        <div className="flex items-center justify-between w-full p-1 border border-t-0 border-collapse rounded-t-none">
            {date.map((day: string) => (
                <div className="w-full" key={day}>
                    <div className="w-[calc(100% / 7)] text-end mr-4">
                        {day}
                    </div>
                </div>
            ))}
        </div>
    )
}

export default CalendarDays

Cells Component

Cells Component는 30개 또는 31개의 day를 그린다.

Cells Component의 핵심컨셉은 startDate부터 시작해서 7개의 day를 가지고 있는 week 배열을 만든 뒤, 배열 변수 rows에 담아 위에서 아래로 그려나가는 것이다.

  • 필요 변수
    -달의 시작일이 있는 주의 시작일 startDate
    -달의 말일이 있는 주의 말인 endDate
    -달의 시작일 monthStart
    -달의 말일 monthEnd
    -startDate를 초기값으로 가지고 endDate까지 변하는day변수
    -day를 담을 배열 변수 days
    -days 배열을 담을 배열 변수 rows
    -화면에 보일 string 타입의 날짜 formattedDate
    -map함수에 사용할 key값 num
import { addDays, endOfMonth, endOfWeek, format, startOfMonth, startOfWeek } from "date-fns"

import CalendarCellItem from "./CalendarCellItem"
import CalendarModal from "./CalendarModal"

interface Props {
    currentMonth: Date,
    onDateClick: (day: Date) => void,
    selectedDate: string,
    clickModal: () => void,
    click: boolean
}

const CalendarCells = ({ currentMonth, onDateClick, selectedDate, clickModal, click }: Props) => {
    const monthStart = startOfMonth(currentMonth)
    const monthEnd = endOfMonth(monthStart)
    const startDate = startOfWeek(monthStart)
    const endDate = endOfWeek(monthEnd)
    const today = new Date()
    const rows = []
    let days = []
    let day = startDate
    let formattedDate = ''
    let num = 0;
    while (day <= endDate) {
        for (let i = 0; i < 7; i++) {
            formattedDate = format(day, 'yyyy-MM-dd')
            const cloneDay = day
            num++
          	// days 배열에 day 추가
            days.push(
              	// 현재 day값이 현재 달과 다르면 회색으로, 같으면 흰색으로 style 설정
                <div className={`w-full h-[10.5rem] relative gap-1 border rounded pb-[12%] text-end ${format(currentMonth, 'M') !== format(day, 'M') ? 'bg-gray-100 text-gray-300' : 'cursor-pointer'}`} key={num} onClick={() => {
                    clickModal()
                    onDateClick(cloneDay)
                }} >
					// 현재 날짜는 text를 빨간색으로 표시
                    <span className={`p-1 flex flex-col px-2 ${format(today, 'yyyy-MM-dd') === format(day, 'yyyy-MM-dd') && "text-red-600 font-bold"}`}>
                        {format(day, 'd')}
                    </span>
                    <CalendarCellItem day={day} />
                    {click && formattedDate === selectedDate && format(currentMonth, 'M') === format(day, 'M') && <CalendarModal select={selectedDate} date={day} num={num} />}
                </div>
            )
			// day를 다음날로 업데이트  
            day = addDays(day, 1)
        }
		// 7일을 채운 days 배열을 rows배열에 추가
        rows.push(
            <div className="flex w-full gap-1" key={num}>
                {days}
            </div>
        )
		// days 배열 초기화
        days = []
    }

    return (
        <div className="flex flex-col items-center justify-between w-full gap-1 p-1 border border-t-0 border-collapse rounded rounded-t-none">
            {rows}
        </div>
    )
}

export default CalendarCells

코드 보기

프론트엔드 코드 깃헙

profile
최선을 다한다는 것은 할 수 있는 한 가장 핵심을 향한다는 것

0개의 댓글