トップ ブログ 健康管理のために Next.js × OpenAI × Vercel で体重管理アプリを作ってみた

健康管理のために Next.js × OpenAI × Vercel で体重管理アプリを作ってみた

本記事は、NRIエンジニアによって2023年12月3日にQiitaに投稿された記事です。

はじめまして。突然ですが、システムエンジニアにとって体は資本ですよね。
システムエンジニアという職業上、デスクワークが多く運動不足になりやすいですし、リリース時期ともなると忙しく睡眠不足に陥ってしまうこともあると思います。
ですがもちろん体調が悪いと良いパフォーマンスを発揮できませんし、普段から健康管理をすることが重要になってきます。

私は最近 Azure やフロントエンドにおける技術支援を主に担当しておりますが、今年私の下に新人が入社してきました。その新人にも同じように業務を教えていくのですが、私の下で働くからには一日でも長く健康に仕事をしてほしいと思い、興味のある技術(Next.js や Open AI)を用いて「体重管理アプリ」を作ろうと決意しました。

ということでNRI OpenStandia Advent Calendar 2023の3日目はその奮闘記になります。大まかな開発の流れは以下の通りです。

  1. Next.jsでアプリを開発し、Vercelにデプロイする

  2. Azureで動作するようアプリを改修し、App Serviceに移行する

本記事はその中で前半の Vercel にデプロイするまでとなります。Azure に移行する記事はNRI OpenStandia Advent Calendar 2023の 19 日目に公開予定です。

本記事で記載している実装はポイントを絞っていますので、ソースコードの完成系を閲覧したい場合はm2-sakai/weight-management-appをご参照ください。

完成したアプリ

まず、完成したアプリは以下となります。

ソースコードは以下の GitHub に入れてますので、ご興味あればご覧ください。

機能・技術スタック

本アプリでは以下の機能を実装しています。

機能 説明 使用技術 バージョン
カレンダー機能

毎日の体重を数値で入力・管理できる

FullCalender

6.1.9

グラフ機能

毎日の体重の増減をグラフで見ることができる

react-chartjs-2

5.2.0

チャット機能

優れた AI と対話ができる

OpenAI API

v1

認証機能

各ユーザーごとにパーソナライズされる

NextAuth.js

5.0.0-beta.3

また、その他開発に関わる技術スタックは以下の通りです。

種別 技術スタック バージョン / プラン
フロントエンドの実装

Next.js / TypeScript

14.0.1 / 5

CSS

tailwind css / chakra-ui

3.3.0 / 2.8.2

ホスティングサービス

Vercel

Hobby プラン

DB

Vercel Postgres

0.5.1

API通信

axios

1.6.2

バリデーション

zod

3.22.4

準備

ここから実装の説明に移ります。と、いきたいところですが、私は Next.js における開発が未経験で何から始めれば良いか調べるところからのスタートだったため、まずはNext.js の公式チュートリアルに取り組むことで理解を深めました。

このチュートリアルですが、Chapter 1~16 と用意されており、コンテンツも非常に充実しています。Next.js を学ぼうと思っている方、まずはここから始めるのが良いかと思います。

実装

満を持して実装です。まず以下のコマンドで Next.js のプロジェクトの大枠を作成します。

npx create-next-app@latest

コマンド実行後、対話型による各種設定を行います。今回はほとんどデフォルトの設定としましたが、どのような設定があるかは公式ドキュメントのcreate-next-appに記載されているので、そちらをご参照ください。

全ての設定が完了すると Next.js のプロジェクトの大枠が完成したので、一度 GitHub に push しておきます。

Vercel にデプロイ

Next.js のテンプレートが完成したところで、Vercel にデプロイしてホスティングされるか確認してみます。

Vercel は Hobby、Pro、Enterprise と 3 つの料金プランがあります。今回は商用目的でなく個人開発用ですので、Hobby プランでアカウントを作成します。

アカウントの作成は GitHub 連携、GitLab 連携、Bitbucket 連携、Email と様々な手段がありますが、先程 push した GitHub のアカウントと連携して作成すると、push したリポジトリで Vercel のプロジェクトがすぐに作成できるため、おすすめです。

プロジェクトを作成すると、ビルド ⇒ デプロイ と CI/CD が実行され、xxxx.vercel.appでホスティングされます。

この後は機能を実装し、該当ブランチに push をすれば自動的にデプロイされます。楽チンですね。

DB の設定

Vercel でホスティングできたことを確認できたため、次はユーザー情報や体重を管理するための DB を用意します。
Vercel Hobby プランには 2 種類(Key-Value、PostgreSQL)の DB が用意されています。今回は単純な SQL でデータを操作するため PostgreSQL を使用します。ホスティングも簡単に実現できるのに DB もあるなんて嬉しいですね。

Vercel の DB の使い方に関しては、Next.js のチュートリアルの Chapter 6Chapter 7をご覧頂くのが一番良いかと思います。

DB を作成すると、接続するための接続情報(URL やホスト、ユーザ、パスワード等)が発行されます。
間違ってもその情報は GitHub に push しないようにしましょう。Vercel で環境変数を設定する画面があるので、そちらに登録するようにしてください。

Next.js から DB を操作するには、@vercel/postgresを利用します。以下のコマンドでライブラリをインストールすることで利用可能です。

npm i @vercel/postgre

SQL を用いたデータフェッチの実装については各機能の実装で後述します。

カレンダー機能の実装 

それでは各機能の実装に移ります。まずは毎日の体重を入力、管理できるカレンダー機能の実装です。カレンダーを画面に描画するため、今回はFullCalenderを使用します。

FullCalender は JavaScript で開発された、カスタマイズ可能で使いやすいイベントカレンダーのライブラリです。様々なイベントの表示、追加、編集、削除など、カレンダーに関連する機能を提供しており、アプリケーションに動的なカレンダー機能を追加することができます。

以下のコマンドでライブラリをインストールします。

npm i @fullcalendar/core @fullcalendar/daygrid @fullcalendar/interaction @fullcalendar/react @fullcalendar/timegrid

本アプリのカレンダー機能で実現したいことは以下の通りです。

  • カレンダー初期表示に、サインインしているユーザーの 3 ヶ月分(当月 + 前後 1 ヶ月)の体重を DB から取得し、カレンダーにイベントとして表示する
  • 月を移動した場合、取得していない月の体重を DB から取得し、カレンダーにイベントとして表示する
  • カレンダーの日付は選択でき、選択した日付の体重及び BMI がカレンダーの下に表示される
  • 選択した日付を再度クリックするとモーダルが表示され、体重が入力できる ⇒ 入力情報は DB に保存される

上記を実現するために実装したコードが以下となります。ユーザ情報をセッションから取得する部分については認証機能が完成したら修正するので一旦固定値としています。

また、このカレンダーはインタラクティブにイベントを管理、体重を表示しているためClient Componentsにしており、体重を DB に登録・取得する部分はServer Actionsを用いています。

ソースコードを表示(カレンダーページ)

ソースコードを表示(モーダル)

ソースコードを表示(データ取得)

FullCalender では、node_modules配下にあるグローバル CSS を読み込みますが、Next.js ではそのグローバル CSS が読み込まれない仕様のため(参考)、next-transpile-modulesをインストールしてnext.config.jsに設定する必要があります。

npm i next-transpile-modules

next.config.js

const nextConfig = {
  transpilePackages: ['@fullcalendar/common', '@fullcalendar/daygrid', '@fullcalendar/react'],
};

module.exports = nextConfig;

グラフ機能の実装

続いて、毎日の体重の増減を確認できるグラフ機能の実装です。今回は DB から取得した情報をグラフとして表示するために、react-chartjs-2を使用します。

react-chartjs-2 は React アプリケーションで使用するための Chart.js の React ラッパーライブラリです。Chart.js は JavaScript で描画されるクライアントサイドのグラフ描画ライブラリであり、react-chartjs-2 を使用すると React プロジェクトで簡単にインタラクティブで美しいチャートを組み込むことができます。

以下でライブラリをインストールします。

npm i react-chartjs-2 chart.js chartjs-plugin-annotation

今回グラフ機能で実現したいことは以下となります。

  • グラフの範囲は 1 週間、1 ヶ月、3 ヶ月、1 年の 4 つの内から選択でき、選択した範囲のグラフが表示される
  • 選択したグラフの範囲に対応した体重を DB から取得し、折れ線グラフで表示される
  • ユーザの目標体重を表示し、体重の変遷と目標までの差分を視覚的に把握できる

上記を実現するために実装したコードが以下となります。ユーザー情報をセッションから取得する部分については認証機能が完成したら修正するので一旦固定値としています。

グラフ部分はカレンダー機能と同じくClient Componentsとしており、体重を DB から取得する部分はServer Actionsを用いています。

ソースコードを表示(グラフページ)
ソースコードを表示(タブ)

ソースコードを表示(データ取得)

react-chart-js2 では、option のmaintainAspectRatioがデフォルトでtrueになっているため、何も option を設定しない場合は表示するキャンバスのアスペクト比を維持しようとします。

もしブラウザのサイズ変更をしても表示幅を固定したい場合は option にmaintainAspectRatio: falseに設定した上でグラフコンポーネントにheightwidthを設定することで表示幅を固定にできます。

const options = {
  maintainAspectRatio: false,
};
return (
  <Line options="{graphOptions}" data="{data}" width="{200}" height="{200}" />
);

また、表示をレスポンシブにしたくない場合は option にresponsive: false(デフォルトはtrue)を加えることでレスポンシブ設定を無効にすることができます。

チャット機能の実装

続いて、優れた AI と対話ができるチャット機能です。今回はチャット機能を実現するにあたり、OpenAI APIを使用します。

OpenAI API は、OpenAI が提供するプログラムやサービスと他のソフトウェアを連携させるためのインターフェースです。具体的には、OpenAI API は自然言語処理タスクにおいて高度な言語モデルである GPT を利用するための手段を提供しています。

まず、OpenAI API を使用するために、API キーを取得します(Open AI へのアカウント作成は省略します)。
公式ドキュメント - API keysから、「Create new secret key」を押下することで簡単に作成ができます。

DB の接続情報と同様、API Key は間違っても GitHub に push しないようにしましょう。

ただ、API キーを取得しただけでは API は実行できません。OpenAI API は実行毎に料金が発生するためです。従って事前にSettings > Billingから、クレジットカードの登録とチャージ(最低 $5)をしておきます。

これで API を実行する準備は整いました。今回チャット機能で実現したいことは以下となります。

  • ユーザーがチャットを入力し、送信すると AI(今回は「体重管理マン」と命名)がレスポンスを返す
  • レスポンスを生成している時間は「・・・」の文字が表示される
  • レスポンスは 1 文字 1 文字コマ送りに表示される

上記を実現するための実装したコードが以下となります。GPT のモデルはgpt-3.5-turbo-0613を使用しています。

ソースコードを表示(チャットページ)
ソースコードを表示(チャット)
ソースコードを表示(送信フォーム)

ソースコードを表示(API実行)

今回はCreate chat completionの API のみ使用しましたが、OpenAI API の他 API の使用方法については以下のリファレンスをご参照ください。

https://platform.openai.com/docs/api-reference

認証機能の実装

最後に認証機能の実装です。認証機能はNext.js のチュートリアルの Chapter 15にあるようにNextAuth.jsを用います。

NextAuth.js はセッションの管理、サインインとサインアウト、および他の認証に関する多くの複雑な部分を抽象化し、Next.js アプリケーションでの認証に対する統一されたソリューションを提供することで実装プロセスを簡素化してくれます。

まず、NextAuth.js の設定オプションを定義する必要があるため、以下のようにauth.config.tsを実装します。ここでは以下のような設定を入れています。

  • pagesオプションにsignIn: '/signin'とすることで、ユーザーは NextAuth.js のデフォルトページではなく、カスタムサインインページ(/signin)にリダイレクトされます。
  • サインインしていないユーザーはコンテンツにアクセスできず、サインインした後はトップページ(/top)にリダイレクトされます。

auth.config.ts

import type { NextAuthConfig } from 'next-auth';

export const authConfig = {
  providers: [],
  pages: {
    signIn: '/signin',
  },
  callbacks: {
    authorized({
      auth,
      request: { nextUrl },
    }: {
      auth: any;
      request: {
        nextUrl: any;
      };
    }) {
      const isSignedIn = !!auth?.user;
      const isOnTop = nextUrl.pathname.startsWith('/top');
      if (isOnTop) {
        if (isSignedIn) return true;
        return false;
      } else if (isSignedIn) {
        return Response.redirect(new URL('/top', nextUrl));
      }
      return true;
    },
  },
} satisfies NextAuthConfig;

続いて、上記の設定オプションを適用するためにmiddleware.tsファイルにインポートします。

middleware.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.png).*)'],
};

続いて、auth.tsファイルを作成し、Credential Providerを追加します。今回はユーザー名及びパスワードによる認証を行います。
ユーザー情報は DB で管理されているため、入力された情報を元に DB から情報を取得し検証を行います。

auth.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { User } from './app/types/User';
import bcrypt from 'bcrypt';

async function getUser(email: string): Promise<User | undefined> {
  try {
    const user = await sql<User>`SELECT * from wm_users where email=${email}`;
    return user.rows[0];
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);

        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;
          const passwordsMatch = await bcrypt.compare(password, user.password);
          if (passwordsMatch) {
            return user;
          }
        }
        console.log('Invalid credentials');
        return null;
      },
    }),
  ],
});

最後にこの認証機能を呼び出すためのサインインフォームや、サインアップフォームを作成します。作ったフォームは以下となります。

ソースコードを表示(サインインフォーム)

ソースコードを表示(サインアップフォーム)

ソースコードを表示(認証関連のDB処理)

以上で認証機能ができました。最後に、各機能でユーザー情報をセッションから取得する部分がありましたので、以下のような関数を実装して各機能を修正します。

ソースコードを表示(ユーザー情報取得)

以上で、全ての機能の実装が完了し、体重管理アプリが完成しまし

おわりに

いかがでしたでしょうか。この記事では Next.js を用いて 0 からアプリケーションを作った奮闘記を紹介しました。
正直 Next.js を適切に使いこなせたかは微妙なところですが、1つ動くものができたことは良かったと思います。現在はライブラリが充実しているため、以前は 0 から実装しなければいけなかった機能もライブラリを用いて比較的容易に開発することができます。嬉しい世の中です。今回使用しなかった機能も多数あるため、今後より一層開発経験を積んでいきたいと思います。

参考文献

今回も非常に多くの文献を参考にさせていただきました。心より感謝申し上げます。

関連OSS

  • Next.js
    サポート対象

    Next.js

    ねくすとじぇーえす。Reactの機能を拡張するためのJavaScriptフレームワークです。

お気軽にお問い合わせください
オープンソースに関するさまざまな課題、OpenStandiaがまるごと解決します。
下記コンテンツも
あわせてご確認ください。