yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

バックエンドで生成したJSONを元にReactで月表示カレンダーを描画する実装

Reactで作ってるWebサービスにFullcalendarなどライブラリを使わずスクラッチでカレンダーを作ったので実装書いてみる。

前にVueでカレンダー作ったときみたいにフロントエンド側でゴリゴリ書いてこうと思ったんだけどちょっと今回は要件的にその実装方針だと辛かったんでロジックをバックエンドに寄せたらフロントで頑張るよりは書くの楽だった。

前にVueで実装したもの

github.com

バックエンドのロジック

今回はRailsで作った

週表示など月表示以外もいろんなパターンの実装作るんで共通処理を切り出すモジュールを用意

# カレンダー描画用json生成に使うモジュール
module CalendarContent
  BIG_MOON = [1, 3, 5, 7, 8, 10, 12]
  WEEK_DAYS = 7

  def last_month(month)
    month == 1 ? 12 : month - 1
  end

  def next_month(month)
    month == 12 ? 1 : month + 1
  end

  def display_last_year(year, month)
    (month == 1) ? (year - 1) : year
  end

  def month_end_date(month)
    BIG_MOON.include?(month) ? 31 : month === 2 ?  28 : 30
  end

  # 指定した年、年の最終日の曜日
  def month_end_wday(year, month)
    end_date = month_end_date(month)
    Date.new(year, month, end_date).wday
  end

  # 引数で指定した月の初日の曜日
  def month_first_wday(year, month)
    Date.new(year, month, 1).wday
  end

  # 引数で指定した月の最終日の曜日
  def month_end_wday(year, month)
    end_date = month_end_date(month)
    Date.new(year, month, end_date).wday
  end
end

上記のモジュールを読み込んでカレンダー描画用のJSON生成。日付描画の部分だけ書いたけど実際はJSONの中に表示テキストやURLなど埋め込んでいく。

で、その時複数モデル絡んでくるのでどこか特定のモデルに書くのは違和感があったのでServiceクラスに切り出して書いてみた。

include CalendarContent

class MonthCalendarService
  def initialize(year, month)
    @target_year = year
    @target_month = month
  end

  WEEK_DAYS = 7

  SUN = 0
  MON = 1
  TUE = 2
  WED = 3
  THU = 4
  FRI = 5
  SAT = 6

  # 日曜始まり月表示カレンダーの行数
  def week_count(year, month)
    if month == 1
      last_month = 12
      last_year = year - 1
    else
      last_month = month - 1
      last_year = year
    end
    ((month_first_wday(year, month) + month_end_date(month)).quo(WEEK_DAYS).to_f).ceil
  end

  # [{date: 27, text: '', url: ''},{date: 28, text: '', url: ''}]
  def content_json
    result = []
    # カレンダー行数
    week_count_num = week_count(@target_year, @target_month)
    # 表示月の最終日
    current_month_end_date = month_end_date(@target_month)
    # 表示される先月の年と月
    display_last_year = display_last_year(@target_year, @target_month)
    last_month = last_month(@target_month)
    # 表示される最終日付と曜日
    display_last_month_end_date = month_end_date(last_month)
    display_last_month_end_date_wday = month_end_wday(display_last_year, last_month)
    # 先月の最終日時が土曜の場合は先月の日付表示不要
    if display_last_month_end_date_wday == SAT
      display_last_month_start_date = nil
    else
      display_last_month_start_date = display_last_month_end_date - display_last_month_end_date_wday
    end
    current_date = 0
    week_count_num.times do |row|
      week_days_array = []
      # カレンダー1行目
      # 前月を表示する場合
      if row == 0 && display_last_month_start_date.present?
        (display_last_month_start_date..display_last_month_end_date).each do |num|
          week_days_array.push({date: num})
        end
        (WEEK_DAYS - display_last_month_end_date_wday - 1).times do |num|
          current_date = num + 1
          week_days_array.push({date: current_date})
        end
        result.push(week_days_array)
      else
        WEEK_DAYS.times do |num|
          if current_date == current_month_end_date
            current_date = 1
          else
            current_date = current_date + 1
          end
          week_days_array.push({date: current_date})
        end
        result.push(week_days_array)
      end
    end
    result
  end
end

上記のcontent_jsonメソッドを実行すると以下のようなJSONが生成される(実際はdate以外のkeyもあり

[
    [
        {
            "date": 26
        },
        {
            "date": 27
        },
        {
            "date": 28
        },
        {
            "date": 29
        },
        {
            "date": 30
        },
        {
            "date": 1
        },
        {
            "date": 2
        }
    ],
    [
        {
            "date": 3
        },
        {
            "date": 4
        },
        {
            "date": 5
        },
        {
            "date": 6
        },
        {
            "date": 7
        },
        {
            "date": 8
        },
        {
            "date": 9
        }
    ],
    [
        {
            "date": 10
        },
        {
            "date": 11
        },
        {
            "date": 12
        },
        {
            "date": 13
        },
        {
            "date": 14
        },
        {
            "date": 15
        },
        {
            "date": 16
        }
    ],
    [
        {
            "date": 17
        },
        {
            "date": 18
        },
        {
            "date": 19
        },
        {
            "date": 20
        },
        {
            "date": 21
        },
        {
            "date": 22
        },
        {
            "date": 23
        }
    ],
    [
        {
            "date": 24
        },
        {
            "date": 25
        },
        {
            "date": 26
        },
        {
            "date": 27
        },
        {
            "date": 28
        },
        {
            "date": 29
        },
        {
            "date": 30
        }
    ],
    [
        {
            "date": 31
        },
        {
            "date": 1
        },
        {
            "date": 2
        },
        {
            "date": 3
        },
        {
            "date": 4
        },
        {
            "date": 5
        },
        {
            "date": 6
        }
    ]
]

あとはこれをReactに渡してカレンダーを作る

interface

export interface MonthCalendarContentJson {
  date: number
}

state

const [calendarContentArray, setCalendarContentArray] = useState<Array<Array<MonthCalendarContentJson>>>([[]])

カレンダー表示部分

<table className={calendarStyles.calendar}>
  <thead>
    <tr>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  {calendarContentArray.map((array, i) => {
    return (
      <tbody key={i}>
        <tr>
          {array.map((a, i) => {
            return (
              <td key={i}>
                {a.date}
              </td>
            )
          })}
        </tr>
      </tbody>
    )
  })}            
</table>

できたもの。CSSでtdにpaddingを追加している

CSSは略