React | moment.js로 달력 구현하기

샘샘·2023년 7월 15일
0
post-thumbnail

가계부 프로젝트에서 제일 중요한 것이 한 달 사용 내역을 한 눈에 확인할 수 있는 캘린더라고 생각한다

date-picker같은 라이브러리를 사용해서 구현할 수도 있지만 첫 프로젝트이고 가계부에서 캘린더는 정말 정말 메인 기능이라고 생각하기 때문에 직접 만들어보고 싶었다

라이브러리는 단순히 date를 pick! 해야 할 때 쓰기로 하고, 일단 내역 확인에서는 직접 만들어보기로 했다


import React, { useState } from 'react';
import moment, { Moment } from 'moment';
import { QueryObserverResult, UseQueryResult, useQuery } from 'react-query';
import '../../styles/components/_Calender.scss';
import { useParams } from 'react-router';
import { isAfter, isSameDay } from 'date-fns';
import CalendarModal from './CalendarModal';
import accounts from '../../api/accounts';

interface CalendarProps {
  today: Moment;
  incomeType: (type: string) => string;
  expenditureType: (type: string) => string;
  getAccountRefetch: QueryObserverResult['refetch'];
}

// 월별, 일별 수입/지출 총 금액 조회
interface TotalStatus {
  day: string;
  expenditure_sum: number;
  income_sum: number;
}

interface AccountTotalResponseDto {
  accountTotalResponseDtoList: TotalStatus[];
  expenditure_sum: number;
  income_sum: number;
}

function Calendar({
  today,
  incomeType,
  expenditureType,
  getAccountRefetch,
}: CalendarProps): JSX.Element {
  // 현재 가계부의 id 조회
  const { id } = useParams<{ id?: string }>();

  // 현재 조회하고 있는 달
  const currentMonth = today.format('YYYY-MM');

  // 월별, 일별 수입/지출 총 금액 조회
  const {
    data: getTotalMonthDate,
    refetch: getTotalMonthDateRefetch,
  }: UseQueryResult<AccountTotalResponseDto> = useQuery(
    ['getTotalMonthDate', id, currentMonth],
    () => accounts.getTotalMonthDate(id as string, currentMonth)
  );

  const monthTotal = getTotalMonthDate?.accountTotalResponseDtoList;

react-query로 받아온 데이터를 일별 수입 / 지출에 표시해주기 위해 monthTotal이라는 변수에 할당해주었다

  // 숫자 형식화 함수
  const formatNumber = (value: number) => {
    if (value >= 100000000) {
      const stringValue = value.toString();
      const remainingDigits = stringValue.substring(0, stringValue.length - 8);
      return `${remainingDigits}억원`;
    }
    if (value >= 100000) {
      const stringValue = value.toString();
      const firstTwoDigits = stringValue.substring(0, stringValue.length - 4);
      return `${firstTwoDigits}만원`;
    }
    if (value >= 10000 && value < 100000) {
      const stringValue = value.toString();
      const firstDigit = stringValue[0];
      const secondDigit = stringValue[1];
      return `${firstDigit}${secondDigit}천원`;
    }
    if (value >= 1000 && value < 10000) {
      const firstDigit = Math.floor(value / 1000);
      return `${firstDigit}천원`;
    }
    return `${value.toLocaleString().split('.')[0]}`;
  };

큰 금액을 입력하다 보면 한정된 캘린더의 크기를 넘어서 표시되는 경우가 있었다
👉 억단위까지 테스트....!! (실제로 억 이상 입력하는 유저가 있었음)
숫자 형식화 함수를 만들어 금액 단위를 나누어서 표현하게 했다

  // 날짜 클릭 시 상세 모달
  const [calendarModal, setCalendarModal] = useState<boolean>(false);
  const [selectedDate, setSelectedDate] = useState<string>('');
  // 모달 오픈/클로즈 애니메이션
  const [modalAnimation, setModalAnimation] = useState('');

  const calendarModalOpen = (date: string): void => {
    if (isSameDay(new Date(date), new Date())) {
      setSelectedDate(date);
      setCalendarModal(true);
      setModalAnimation('modalAnimation');
    }
    if (!isAfter(new Date(date), new Date())) {
      setSelectedDate(date);
      setCalendarModal(true);
      setModalAnimation('modalAnimation');
    }
  };

  const firstWeek = today.clone().startOf('month').week();
  const lastWeek =
    today.clone().endOf('month').week() === 1
      ? 53
      : today.clone().endOf('month').week();

이번달의 첫째 주와 마지막 주가 올 해의 몇번째 주인지 확인

  const calendarArr = () => {
    let result: JSX.Element[] = [];
    let currentweek = firstWeek;

    const generateTableCells = (week: number): JSX.Element[] =>
      Array(7)
        .fill(0)
        .map((_, index) => {
          const days = today
            .clone()
            .startOf('year')
            .week(week)
            .startOf('week')
            .add(index, 'day');

날짜 생성을 위해 일주일의 날짜를 가지는 배열을 생성한다

          const matchingData = monthTotal?.find(
            (item) => item.day === days.format('YYYY-MM-DD')
          );

          if (moment().format('YYYYMMDD') === days.format('YYYYMMDD')) {
            return (
              <button
                type="button"
                className="td"
                key={days.format('YYYY-MM-DD')}
                style={{ color: 'black' }}
                onClick={() => calendarModalOpen(days.format('YYYY-MM-DD'))}
              >
                <span className="today" data-text={days.format('D')}>
                  {days.format('D')}
                </span>
                <div className="accountPriceHeight">
                  {matchingData?.expenditure_sum ? (
                    <span className="accountPrice expenditure">
                      -{formatNumber(matchingData.expenditure_sum)}
                    </span>
                  ) : (
                    ''
                  )}
                  {matchingData?.income_sum ? (
                    <span className="accountPrice income">
                      +{formatNumber(matchingData.income_sum)}
                    </span>
                  ) : (
                    ''
                  )}
                </div>
              </button>
            );
          }

해당 날짜가 오늘이라면 css를 다르게 줘야하기 때문에 if문을 두개로 만들었다
날짜를 클릭하면 일일 상세 내역이 나타나야 하기 때문에 key에는 고유한 값인 해당 날짜를 할당해주었다

그리고 날짜 밑에 react-query로 가져온 해당 날짜의 수입과 지출 금액을 각각 나타냈다


          if (days.format('MM') !== today.format('MM')) {
            return (
              <button
                type="button"
                className="notThisMonthBox"
                key={days.format('YYYY-MM-DD')}
              >
                <span>{days.format('D')}</span>
              </button>
            );
          }

그리고 달력을 주 단위로 만들었기 때문에 이번달이 아니더라도 같은 주에 속해있으면

👉 예) 7월 달력에 6월이 나와있으면 보기 싫으니까!

.notThisMonthBox {
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-direction: column;
  width: 100%;
  height: 56.5px;
  border-radius: 0px;
  padding-bottom: 10px;
  cursor: default;
  & span {
    color: white;
  }
}

아예 글자가 보이지 않고 클릭도 할 수 없게 해주었다

          return (
            <button
              type="button"
              className="td"
              key={days.format('YYYY-MM-DD')}
              onClick={() => calendarModalOpen(days.format('YYYY-MM-DD'))}
            >
              <span className="eachDate">{days.format('D')}</span>
              <div className="accountPriceHeight">
                {matchingData?.expenditure_sum ? (
                  <span className="accountPrice expenditure">
                    -{formatNumber(matchingData.expenditure_sum)}
                  </span>
                ) : (
                  ''
                )}
                {matchingData?.income_sum ? (
                  <span className="accountPrice income">
                    +{formatNumber(matchingData.income_sum)}
                  </span>
                ) : (
                  ''
                )}
              </div>
            </button>
          );
        });

    for (currentweek; currentweek <= lastWeek; currentweek += 1) {
      result = result.concat(
        <tr className="tr" key={currentweek}>
          {generateTableCells(currentweek)}
        </tr>
      );
    }

    return result;
  };

이렇게 만든 각 주들의 배열을 for문을 통해 마지막주까지 반복하면서 result배열에 tr(행)로 추가해줬다

  return (
    <div className="calBackground">
      {calendarModal && (
        <CalendarModal
          setCalendarModal={setCalendarModal}
          selectedDate={selectedDate}
          incomeType={incomeType}
          expenditureType={expenditureType}
          getAccountRefetch={getAccountRefetch}
          getTotalMonthDateRefetch={getTotalMonthDateRefetch}
          modalAnimation={modalAnimation}
          setModalAnimation={setModalAnimation}
        />
      )}
      <div className="days">
        <p className="sun"></p>
        <p></p>
        <p></p>
        <p></p>
        <p></p>
        <p></p>
        <p className="sat"></p>
      </div>
      <table className="dates">
        <tbody className="date">{calendarArr()}</tbody>
      </table>
    </div>
  );
}

export default Calendar;

달력의 윗부분에는 요일을 표시해서 주말만 색을 다르게 하고, 그 밑에 table로 위에서 만들어준 캘린더를 출력해줬다

profile
회계팀 출신 FE개발자 👉콘테크 회사에서 웹개발을 하고 있습니다

0개의 댓글