본문 바로가기

React/MyDiary

Ant Design의 Calendar를 사용하여 일기를 렌더링 하는 달력 개발

구현 목표

개발을 완료한 Calendar 컴포넌트

MyDiary 프로젝트에서 일기를 작성한 이후 달력에 표시하기 위해서 Calendar 컴포넌트를 사용하였다.

Calendar 컴포넌트는 달력을 렌더링하고 각 datecell은 React Node를 가진다.

다시말해 일(day)마다 React 컴포넌트를 설계할 수 있다는 말이다.

 

여기서 처음 구현목표는 datecell을 클릭하면 그 날에 작성한 일기를 Modal로 렌더링 시켜주는 것을 목표로 하였고 구현 후에는 해당 날짜의 작성한 모든 일기를 List로 렌더링 하도록 변경했다.

구현 방법

Ant Design의 Calendar 컴포넌트는 달력에 형태를 하고 있다.

구현 목표는 datecell을 onClick 했을때 그 날짜의 해당하는 일기를 Modal로 렌더링하는 것을 목표로한다.

 

Calendar - Ant Design

You selected date: 2017-01-25

ant.design

먼저 Ant Design API 문서에는 dateCellRender라는 props를 전달 받아 렌더링 한다고 설명이 되어있다.

dateCellRender의 타입은 function(date: moment): ReactNode 이다.

즉, dateCellRender의 props는 function이여야 하고 해당 함수의 매개변수는 moment 객체를 받는다. 라는 뜻이다.

또한 이 함수가 반환하는 것은 ReactNode여야 한다.

 

API 설명이 조금 불친절하다. 매개변수가 moment 객체라는 정보 뿐이라서 어떻게 코딩을 해야할지 감이 잡히질 않았다.

그래서 console.log를 출력시켜 보았다.

자신이 현재 화면으로 보고 있는 달력의 과거일부터 미래 일 순으로 쭉 moment로 들어온다.

(GIF 첨부 예정)

다행히도 예시코드가 있어 참고할 수 있었다. moment가 들어올 때 해당 moment에 맞는 ReatNode를 반환 해주면된다.

구현 코드

import { Calendar } from "antd";
import dateCellRender from "./dateCellRender";
import locale from "antd/es/calendar/locale/ko_KR";

return(
    <div>
    	<Calendar
            locale={locale}
            dateCellRender={dateCellRender}
            onSelect={onSelectDateCell}
          />

          { /* <DiaryModal /> */ }
          <DiaryList selectedDiary={selectDiary} />
    </div>
);

 

Ant Design에서 Calenar 컴포넌트를 import 해준다.

정말 간단하다. Calendar 컴포넌트만 렌더링 해줘도 손쉽게 달력 구현이 가능하다.

locale은 달력의 년, 월, 요일 등을 한국화 시키는 것이다.

 

가장 중요한 코드는 dateCellRender와 onSelect이다.

dateCellRender는 구현 목표에서도 말했듯이 datecell에 어떻게 렌더링 하는지 기술하는 props이고 onSelect는 달력의 datecell을 선택(클릭, 키보드 조작)했을때 이벤트를 어떻게 처리할지 기술하는 props이다.

 

dateCellRender props 처리

먼저 dateCellRender는 ReactNode가 들어가야 하는 만큼 따로 파일을 분리해서 코딩했다.

 

( dateCellRender.tsx )

import React from "react";
import moment from "moment";
import useSWR from "swr";
import fetcher from "../../utils/fetcher";
import {
  Cloud,
  Lightning,
  Rain,
  Snow,
  Sun,
} from "../../utils/styles/weather_styledIcon";
import {
  Happy,
  Normal,
  Sad,
  Unhappy,
} from "../../utils/styles/emotion_styledIcon";

const DateCellRender = (value: moment.Moment) => {
  const { data } = useSWR(`/api/diary/`, fetcher);

  const getDayInfo = () => {
    const diaryData = data && data.diaryData;
    let calendarData;
    let diaryMoment;

    for (let i in diaryData) {
      diaryMoment = moment(diaryData[i].createdAt).format("YYYY-MM-DD");

      if (diaryMoment === value.format("YYYY-MM-DD")) {
        calendarData = [
          {
            weather: diaryData[i].weather,
            emotion: diaryData[i].emotion,
            content: diaryData[i].content,
          },
        ];
      }
    }

    return calendarData || [];
  };

  const WeatherRander = ({ weather }: { weather: string }) => {
    switch (weather) {
      case "sun":
        return <Sun size="30px" weather="none" />;
      case "cloud":
        return (
          <Cloud size="30px" weather="none" />
        );
      case "rain":
        return <Rain size="30px" weather="none" />;
      case "snow":
        return <Snow size="30px" weather="none" />;
      case "lightning":
        return (
          <Lightning size="30px" weather="none" />
        );
      default:
        return <div></div>;
    }
  };

  const EmotioinRender = ({ emotion }: { emotion: string }) => {
    switch (emotion) {
      case "happy":
        return <Happy size="30px" emotion="none" />;
      case "normal":
        return <Normal size="30px" emotion="none" />;
      case "unhappy":
        return <Unhappy size="30px" emotion="none" />;
      case "sad":
        return <Sad size="30px" emotion="none" />;
      default:
        return <div></div>;
    }
  };

  const dayInfo = getDayInfo();

  return (
    <div>
      {dayInfo.map((info) => (
        <div>
          <WeatherRander weather={info.weather} />
          <EmotioinRender emotion={info.emotion} />
        </div>
      ))}
    </div>
  );
};

export default DateCellRender;

 

dateCellRender에 props로 제공되는 함수이다. 핵심 함수는 getDayInfo() 함수이다.

 

const getDayInfo = () => {
    const diaryData = data && data.diaryData;
    let calendarData;
    let diaryMoment;

    for (let i in diaryData) {
      diaryMoment = moment(diaryData[i].createdAt).format("YYYY-MM-DD");

      if (diaryMoment === value.format("YYYY-MM-DD")) {
        calendarData = [
          {
            weather: diaryData[i].weather,
            emotion: diaryData[i].emotion,
            content: diaryData[i].content,
          },
        ];
      }
    }

    return calendarData || [];
  };

 

dateCellRender의 props로 설정한 함수는 매개변수로 moment를 날짜순으로 들어온다고 위에서 부터 말했다.

이 때 들어온 moment와 작성한 일기의 작성일이 서로 동일한 것을 가져와야 한다.

변수 diaryData는 사용자가 작성한 모든 일기를 가지고 있고 순차적으로 들어오는 moment는 value 이다.

그렇다면 현재 들어온 moment의 날짜와 diaryData의 일기의 날짜와 동일한 것을 반환 해줘야한다.

 

moment는 시간, 분, 초까지 표현한다. 개발에 필요한 데이터는 년, 월, 일이면 된다. 따라서 포맷을 바꾸어 비교해주기 위해서 format("YYYY-MM-DD")로 변환 해준뒤 if문을 통해서 비교한다.

 

if (diaryMoment === value.format("YYYY-MM-DD"))

 

이 판별식에서 참이 나온 데이터는 현재 들어온 moment와 동일한 년, 월, 일이기 때문에 dateCellRender로 반환해주어야 한다.

좀 더 상세히 말하면 해당 데이터를 ReactNode로 렌더링할 수 있게 변환 후 반환 해주어야 한다.

그렇기에 calendarData라는 Array를 통해서 moment에 해당하는 일기들을 모아주고 렌더링할 return문으로 가게 된다.

 

const dayInfo = getDayInfo();

return (
    <div>
      {dayInfo.map((info) => (
        <div>
          <WeatherRander weather={info.weather} />
          <EmotioinRender emotion={info.emotion} />
        </div>
      ))}
    </div>
  );

 

이제 렌더링만 해주면 된다.

dayInfo에는 위 getDayInfo()에서 처리한 데이터를 반환받아 저장하고 있다.

dayInfo.map을 이용해서 저장된 Array를 순차적으로 WeatherRander, EmotioinRender 컴포넌트를 통해서 SVG화하여 렌더링 해준다.

onSelect 이벤트 처리

Modal을 렌더링 했던 코드

const [DateCellModal, setDateCellModal] = useState(false);

const onSelectDateCell = (value: moment.Moment) => {
    const diaryData: Array<diaryType> = data && data.diaryData;

    const filterData = diaryData && diaryData.filter((data: any) => {
      return (
        moment(data.createdAt).format("YYYY-MM-DD") === value.format("YYYY-MM-DD")
      );
    });

    setSelectDiary(filterData);
    //filterData[0] && setDateCellModal(true);
  };

const DiaryModal = () => (
    <Modal
      visible={DateCellModal}
      title={selectDiary?.title}
      onOk={handleOk}
      onCancel={handleCancel}
      footer={[
        <Button key="back" onClick={handleCancel}>
          수정
        </Button>,
        <Button key="submit" type="primary" onClick={handleCancel}>
          확인
        </Button>,
      ]}
    >
      {selectDiary?.contents}
    </Modal>
  );
  
  return(
  	<Calendar
        locale={locale}
        dateCellRender={dateCellRender}
        onSelect={onSelectDateCell}
      />
  
  	<DiaryModal />
  )

 

onSelect와 연결된 onSelectDateCell 함수는 사용자가 달력의 특정 dateCell을 선택하면 그 날짜에 해당하는 moment를 반환한다.

따라서 사용자가 선택한 날짜와 일기의 작성일과 동일한 일기를 필터 해주면 되는 것이다.

그후 필터된 일기를 setState를 해주고 Modal을 열어주면 된다.

이때 Modal에 렌더링 되는 데이터는 필터한 데이터를 저장한 state를 렌더링 해주면 된다.

 

하지만 여기에는 치명적인 문제가 있다. 해당 날짜에 일기를 딱 한개 밖에 렌더링하지 못한다.

일기는 하루에 하나만 쓴다는 나의 착각이었다. 따라서 여러개의 일기를 보여주는 List로 렌더링 하기를 결정했다.

 

List로 렌더링 하는 코드

const [selectDiary, setSelectDiary] = useState<Array<diaryType>>([]);

const onSelectDateCell = (value: moment.Moment) => {
    const diaryData: Array<diaryType> = data && data.diaryData;

    const filterData = diaryData && diaryData.filter((data: any) => {
      return (
        moment(data.createdAt).format("YYYY-MM-DD") === value.format("YYYY-MM-DD")
      );
    });

    setSelectDiary(filterData);
    //filterData[0] && setDateCellModal(true);
  };
  
  return(
  	<Calendar
        locale={locale}
        dateCellRender={dateCellRender}
        onSelect={onSelectDateCell}
      />	
  
       <DiaryList selectedDiary={selectDiary} />
  )

 

List도 Modal과 다르지 않게 흐름은 비슷하다 단지 렌더링 하는 방식만 다른 것 뿐이다.

Modal과 마찬가지로 사용자가 선택한 날짜와 일기의 작성일과 동일한 일기를 필터 해준다.

Modal과의 차이점은 선택한 날짜의 일기를 단한개만 setState를 한다면 List는 전부 setState를 한다.

그 후 DiaryList라는 일기를 렌더링하는 컴포넌트에 selectedDiary라는 props으로 보내주면 된다.

 

DiaryList 컴포넌트의 JSX는 아래를 참고하면 된다.

 

GitHub - lexky82/mydiary

Contribute to lexky82/mydiary development by creating an account on GitHub.

github.com