Zenn
🤖

初心者がWebアプリを作ってみた(2)

2025/04/12に公開

はじめに

Webアプリを作ることに関しては初心者が、ネット情報で基礎的な知識は得たので、次に何するかってことでアプリ構築を行ってみました。
今回は2回目ですが、1回目は以下を見てくださいね。
https://zenn.dev/maedan/articles/125304d4aef4d1

カレンダーのクリックに反応しよう

予約の登録をするにしても、カレンダーをクリックして、といった操作感だと思いますので、クリックの処理を実装しましょう。
fullCalendarの機能として、カレンダー内のクリックに対応してくれてますので、設定を追記してクリック後の処理を書く事で対応できます。
まずは、クリックした際のイベントです。

「interactionPlugin」を使用します。もともとある設定に追加して下さい。

plugins={[timeGridPlugin,interactionPlugin ]}

複数時間を選択したいので、

selectable={true}

次に時間を選択した場合に、とりあえず選択した時間を表示するようにしましょう。

select={(info) => {
    alert('Clicked select ' + info.startStr + ' to ' + info.endStr);
}}

これで、カレンダー上で時間を選択すると、アラートとしてその時間範囲を表示します。

イベントの無い場所をクリックすることで、アラートが表示されます。
イベントをクリックした場合が必要な場合には、以下を記述下さい

eventClick={(info) => {
    alert('Clicked eventClick ' + info.event.title);
}}


イベント名称を取得して表示しています。

イベントの情報をバックエンドに持っていこう

フロントエンド側はだいたい出来ましたね。
現状では、イベントの登録をevents.jsに書き込んでます。本体であればデータベースとかって話になるのでしょうが、それは後程に実装するとして、このファイルをサーバーに置いて、複数人でアクセスしても同じイベント情報が見れるようにしましょう。

バックエンド側の構築

バックエンド側は、作成したカレンダの実装とは、まったく別フォルダで管理します。
ここで、フォルダを整理整頓しましょう。
現状では、以下の構成でしょうか。フォルダ名は、ここでは安易に「Sample02」となっています。

まずは、この安易なフォルダ名を「Client」として、「Server」フォルダを作成します。
以下の感じですね。

Serverフォルダはまだ空っぽです。
ファイル名の色や、右側に書いてある●やMは気にしないで下さい。私の環境ではgitでの管理になっているので、こういったものが表示されています。

events.jsファイルを移動します

予約状況を保持しているevents.jsファイルはバックエンド側にあって欲しいので、ファイル自体を移動させてしまいましょう。内容はデータのみなので、jsファイルでなく、JSONファイルの方が適切ですね。名前も変更しておきましょう。ついでに、Serverフォルダに「server.js」ファイルも作って下さい。ここにバックエンド側の処理を記述します。

events.jsonファイルの中身には、とりあえず何か入れておきましょう

[
    {
      "title":"会議",
      "start":"2025-03-15T10:00:00",
      "end":"2025-03-15T12:00:00"
    },
    {
      "title":"イベント",
      "start":"2025-03-18T10:00:00",
      "end":"2025-03-18T16:00:00"
    },
    {
      "title":"イベント2",
      "start":"2025-03-20T10:00:00",
      "end":"2025-03-20T16:00:00"
    }
  ]

Node.jsを使用します

バックエンド側はNode.jsを使用して作ります。環境にNode.jsがインストールされている事を確認してください。

node -v  
v23.10.0

バックエンドの実装

フロンドエンドの実装である、Client/App.jsと通信して、events.jsonの内容を返すような処理を記述します。
いろいろとライブラリを使用しますので、まずは、これらのインストールが必要です。

npm install express body-parser path cors fs

server.js

// server/server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const cors = require('cors');
const fs = require('fs');
const app = express();
const port = 5000;

app.use(cors());
app.use(bodyParser.json());

const eventsFilePath = path.join(__dirname, 'events.json');
app.get('/api/events', (req, res) => {

  fs.readFile(eventsFilePath, (err, data) => {
    if (err) {
      console.error('Error reading file:', err);
      res.status(500).json({ message: 'Failed to load reservations' });
    } else {
      try {
        const reservations = JSON.parse(data);
        res.status(200).json(reservations);
      } catch (parseError) {
        console.error('JSON parse error:', parseError);
        res.status(500).json({ message: 'Failed to parse reservations' });
      }
    }
  });
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

先ほど移動した「event.json」ファイルは、

fs.readFile(eventsFilePath, (err, data)

で読み出しています。
フロントエンド側(React側)から聞かれた際には「app.get」関数が呼ばれて、この中でevents.jsonファイルを読み出して、返します。

フロントエンド側の修正

次にApp.jsにて、Reactの機能である「useState」「useEffect 」を使用します。
この機能を使用するために、先頭に使うことを宣言します。

import React, { useState, useEffect } from 'react';

events情報はバックエンド側から取得できるので、ファイルから読み込みの処理は削除します。

import events from './events.js';

を削除するか、コメントとして下さい

フロントエンドのClient/app.jsの実装は、events.jsonを読みだしてイベントとしているので、これを修正して、バックエンドと通信して、イベント情報を取得することとしましょう。

const [events, setEvents] = useState([]);

  useEffect(() => {
    const apiUrl = 'http://localhost:5000/api/events';

    fetch(apiUrl)
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        setEvents(data);
      })
      .catch(error => {
        alert('イベントデータの読み込みに失敗しました。');
      });
  }, []);

最初の

const [events, setEvents] = useState([]);

は、events変数と、events変数を変更する際に使用する、setEvents関数を定義しています。右辺はevents変数の初期値を代入しています。
次のuseEffectでは、イベントを登録しています。
最後の

}, []);

の[ ]が第2引数になっているんですが、ここに空配列を入れることでブラウザに最初に表示された際に起動する処理となります。
useState/useEffectはReactを勉強すると必ず出てくるHooksという機能ですが、難しい事は考えずにこういうもんだ、という認識で使用しています。
「fetch」で指定している「apiUrl」は、バックエンド側のServer.jsの事です。この命令で、Server.js側の「app.get」が読み出されます。先ほど実装した通り、「app.get」関数内ではevents.jsonファイルを読み込んで返しているので、fetch関数で、events.jsonの中身が返される訳です。
サーバーサイドから返答があると、then以降の処理となります。ここも並列処理とか面倒な説明がありますが、こういうもんです、って理解しましょう。
thenが2つ続いているのは、順番に処理されていきます。途中でなんかエラーが起こったら、「catch」に飛ばされます。

起動のしかた

まずはバックエンド側を起動させておきましょう。ターミナル、コマンドプロンプト等でServerフォルダに移動して、

node server.js

次にフロントエンド側の起動です。こちらはClientフォルダに移動する必要がありますので、さきほどServerを起動したターミナル、コマンドプロンプトとは別のターミナル、コマンドプロンプトを起動して、

npm start

で起動します。動作を確認してみてください。

ここまでの実装

Client/App.js

//import logo from './logo.svg';
import './App.css';

import React from 'react';
import FullCalendar from '@fullcalendar/react';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
//import events from './events.js';
import { useState, useEffect } from 'react';

function App() {
  const [events, setEvents] = useState([]);

  useEffect(() => {
    const apiUrl = 'http://localhost:5000/api/events';

    fetch(apiUrl)
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        setEvents(data);
      })
      .catch(error => {
        alert('イベントデータの読み込みに失敗しました。');
      });
  }, []);

  return (
    <div className="App">
      <FullCalendar
        plugins={[timeGridPlugin,interactionPlugin ]}
        initialView="timeGridWeek" // 初期表示のモードを設定する
        headerToolbar={{
          left: 'prev,next today',
          center: 'title',
          right: ''
        }}
        locale="ja"
        allDaySlot={false} // all-day の表示を非表示にする
        //hiddenDays={[0, 6]} // 日曜日と土曜日を非表示
        buttonText={{today: '今日'}}
        slotMinTime="09:00:00"
        slotMaxTime="17:30:00"
        eventClick={(info) => {
          alert('Clicked eventClick ' + info.event.title);
        }}
        dateClick={(info) => {
          alert('Clicked dateClick ' + info.dateStr);
        }}
        select={(info) => {
          alert('Clicked select ' + info.startStr + ' to ' + info.endStr);
        }}
        selectable={true}
        events={events}
      />
    </div>
  );
}
export default App;

Server/server.js

const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const cors = require('cors');
const fs = require('fs');
const app = express();
const port = 5000;

app.use(cors());
app.use(bodyParser.json());

const eventsFilePath = path.join(__dirname, 'events.json');
app.get('/api/events', (req, res) => {

  fs.readFile(eventsFilePath, (err, data) => {
    if (err) {
      res.status(500).json({ message: 'Failed to load reservations' });
    } else {
      try {
        const reservations = JSON.parse(data);
        res.status(200).json(reservations);
      } catch (parseError) {
        res.status(500).json({ message: 'Failed to parse reservations' });
      }
    }
  });
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

最後に

今回でバックエンド側を作りました。急に難しくなった感じですが、詳細は後々慣れていくので、まずは動くものを作ってデバックする経験が必要だと思ってるので、このまま突き進みます。
次回は、予約画面の作って、カレンダーに予約状況を反映させるところを説明しますね。

フロントエンドとバックエンドの実装を行ったので、これでフルスタックエンジニアの仲間入りです。

Discussion

ログインするとコメントできます