25
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

うつ病を抱えるエンジニアが ChatGPT+Laravel+SQLite で「自分を追い込まない体調管理ツール」を作ってみた話(途中経過)

Last updated at Posted at 2025-12-14

こちらは『LITALICO Advent Calendar 2025』15日目の記事です

はじめに

この記事は、 去年のアドベントカレンダーの記事 のゆるい続編です。

去年のアドベントカレンダーでは、

うつ病で働けなくなった状態から、就労移行支援を経て LITALICO に入社し、エンジニアとして再スタートを切るまでの経緯を、「ここまで戻ってくるまでの話」として書きました。

あれから1年が経ち、ありがたいことに私は今もエンジニアとして働けています。
もちろん体調がずっと完璧だったわけではなく、しんどい日もたくさんありました。

今年はその後の話として、

  • どうやって体調と付き合いながら働き続けているか
  • その中で ChatGPT と小さな自作ツールがどんな役割を果たしているか

をまとめてみようと思います。

タイトルに「(途中経過)」とつけたのは、これが「うまくいきました!」という完成レポートではなく、まだ試行錯誤の途中だけどここまで来た、という中間報告だからです。

長期就労の過程で見えた体調の波と課題

去年の記事の最後で、「長期就労を目標にしたい」と書きました。
それから1年、結果だけ見ると

  • 大きな休職はしていない
  • 同じチームでちゃんと仕事を続けられている

という意味で、1年目クリアはできたと思います。
その過程は全自動で順調だったわけではなくて、

  • 季節や天候の変化でメンタルが揺れやすい時期がある
  • 食事や睡眠の乱れが、後からじわじわ効いてくる

のような「ゆるやかに崩れていく」感覚を何度も経験しました。

去年までに学んだ

  • 無理をしない
  • 自分の状態を周りに共有する
  • 休む選択肢を持っておく

といった基本は土台として大変役に立ちましたが、それだけでは「ゆるやかな崩れ」をキャッチして調整するところまではなかなか手が回りませんでした。

そこで、自分ひとりで全部覚えて管理しなくていいようにしよう、という発想で、ChatGPTと一緒に体調管理の仕組みづくりを始めました。

ChatGPTを活用した体調ログの記録と課題

まずは技術の話に行く前に、「ChatGPTをどう使っているか」を説明します。

ここから先の内容は、医師や専門家による医療行為・カウンセリングではありません。
もし真似をする場合は、ご自身の体調や状況に十分注意したうえで、自己責任でお願いいたします。
必要に応じて、医療機関や専門家にも相談してください。

この1年で、ChatGPT にお願いしている主な役割はこんな感じです。

  • 食事や体調のログのフォーマットを一緒に決める
  • 「今日こんな感じだった」と投げると、
    • 良い/悪いで評価しない
    • 「こういうパターンがありそう」と事実ベースで整理してくれる
  • しんどいときに、考えすぎているところを CBT(認知行動療法)っぽい質問で整理してくれる

ここで大事にしているのは、「もっと頑張ろう!」みたいな励ましを求めているわけではなくて、

  • 今の状態を淡々と一緒に眺めてくれる相棒

として使っている、ということです。

人に相談するときって「忙しいかな」「重いかな」と気を遣ってしまうことが多いですが、ChatGPT に対しては事実だけポンと置いてコメントをもらう、という距離感を取れるのが自分にはちょうどよかったです。

ただ、これはこれで便利なんですが、

  • データの蓄積ができない
  • 会話ベースだと、ログとして後から振り返りづらい
  • 今日どういう行動をしたかのサマリーがChatGPTでうまく出せない(ここはプロンプトでなんとかできそうではある)

という弱点もあります。

そこで、ログのための小さいWebアプリを作ることにしました。

体調の波を支える職場のサポートと文化

ここまで書くと、「体調管理が全部 ChatGPT のおかげでうまくいっている」ように見えるかもしれませんが、実際には職場やチームにかなり助けてもらっています。

たとえば、季節の変わり目などでメンタルが不安定になりやすい時期には、

  • 1on1で「最近ちょっとしんどいです」と素直に共有すると、マネージャーやリーダーがタスクや人員の調整をしてくれました

社内イベントなどによる、どうしても長時間になりがちな会議では、

  • 運営の方が事前に不安な点をヒアリングしてくれて、体調不良でも退出しやすい工夫や、休憩時間の確保を考えてくれました

「体調に波がある前提で仕事を組み立てていい」という感覚を持てたのは、この職場の文化のおかげだと思っています。
去年のアドベントカレンダー記事も社内の方がたくさん読んでくださって、いいねやリアクションをもらえたことが、「このテーマを書いていいんだ」と思えるきっかけになりました。

体調管理を支えるミニWebアプリの最初の版(v0)

ここからは、Qiitaらしく少し技術寄りの話も書いておきます。
いちばん小さい一歩として作った初期版(以下 v0 と書きます)のミニ Web アプリについてです。

普段は日々の業務で精いっぱいで、体力的にも技術的にもあまり余裕はありません。なので v0 の構成はかなりシンプルに割り切りました。

どんなものを作ったか

作ったのは、ものすごくシンプルに言うと

「1食=1行で記録するための、食事レコーディング用ミニWebアプリ」

です。

v0でやりたいことは、欲張らずに以下の4つに絞りました。

  1. 1食=1行で、食べたもの/カロリー/気分/空腹度/満腹度を入力できる
  2. 今日の合計カロリーと件数がぱっと見で分かる
  3. 入力ミスしても、あとから編集・削除ができる
  4. その日のログをまとめて ChatGPT に渡して一言コメントをもらえる

技術スタックと設計の割り切り

  • フレームワーク:Laravel
    → 一番触り慣れている

  • 実行環境:Docker 1コンテナ
    → ベースイメージ:php:8.3-cli
    php artisan serve --host=0.0.0.0 --port=8000 でそのまま起動
    → nginx / Apache は v0では入れていません

  • DB:SQLite
    → 1コンテナ構成でも扱いやすく、初期セットアップも軽い

  • フロントエンド
    → Laravel Blade + 手書きCSS(public/css/base.css
    → SPAやCSSフレームワーク(Tailwindなど)は、将来のv1以降で検討

インフラとしては、将来的にAWS Lightsailのコンテナサービスで動かすことも見据えていますが、とりあえず v0 はローカルDocker上で完結するようにしました。

DB設計:meal_entries テーブル

1食=1行でログを持つので、v0ではテーブルは1枚だけです。

1行には、「日時・区分・食べたもの・カロリー・気分・体力・空腹度・満腹度・メモ・タグ」を持たせています。
Laravel側では、このテーブルに対応する MealEntry モデルを生やして、SoftDeletes も有効にしています。

画面 1ページ完結の「今日の記録+入力フォーム」

v0は、画面もとことんシンプルです。

/ にアクセスすると、

  • 左側に入力フォーム
  • 右側に「今日の記録一覧」と合計カロリー

が出るようにしています。

入力フォーム(左)

  • 区分(朝/昼/夜/間食)
  • 食べたもの(テキスト)
  • カロリー(わかれば)
  • 気分メモ
  • 体力(1〜5)
  • 食前の空腹度(1〜5)
  • 食後の満腹度(1〜5)
  • メモ/タグ

今日の記録一覧(右)

  • 1行ごとに
    • 時刻
    • 区分
    • 食べたもの
    • kcal
    • 気分
    • 空腹/満腹
    • 編集/削除ボタン

を表示しています。

CSSフレームワークは入れていませんが、

  • カード風レイアウト
  • バッジやボタンのちょっとしたスタイル
  • テーブルの背景色を交互に変える

くらいは base.css に詰めて、
最低限スクショを貼っても恥ずかしくないくらいの見た目にはしました。

image.png

AIコメントのためのサービス:DailyFeedbackService

日次のコメント生成は、DailyFeedbackService というサービスクラスに閉じ込めました。
インターフェースはこんな感じです。

class DailyFeedbackService
{
    public function generate(Collection $entries, Carbon $date): ?string
    {
        if ($entries->isEmpty()) {
            return null;
        }

        if (!config('app.ai_feedback_enabled')) {
            return '※openAI接続はオフです';
        }

        try {
            $client = OpenAI::client(config('services.openai.key'));


            $systemPrompt = $this->buildSystemPrompt();

            $payload = $this->buildPayload($entries, $date);
            $prompt = $this->buildPrompt($payload);

            $response = $client->chat()->create([
                'model' => env('OPENAI_MODEL', 'gpt-4.1-mini'),
                'messages' => [
                    ['role' => 'system', 'content' => $systemPrompt],
                    ['role' => 'user', 'content' => $prompt],
                ],
                'max_tokens' => 200,
            ]);
        } catch (\OpenAI\Exceptions\RateLimitException $e) {
            Log::warning('DailyFeedbackService rate limit', [
                'message' => $e->getMessage(),
            ]);
            return 'openAI API error';

        } catch (\Throwable $e) {
            Log::error('DailyFeedbackService error', [
                'message' => $e->getMessage(),
            ]);
            return 'openAI API error';
        }

        return $response->choices[0]->message->content ?? null;
    }

    protected function buildSystemPrompt(): string
    {
        return view('prompts.daily_feedback_system')->render();
    }

    protected function buildPayload(Collection $entries, Carbon $date): array
    {
        return [
            'date' => $date->toDateString(),
            'total_calorie' => $entries->sum('calorie_kcal'),
            'count' => $entries->count(),
            'entries' => $entries->map(function (MealEntry $e) {
                return [
                    't'  => $e->jst_datetime?->format('H:i'),          // time
                    'k'  => $e->meal_type?->value,                     // kind
                    'f'  => $e->food,
                    'c'  => $e->calorie_kcal,
                    'm'  => $e->mood,                                  // mood
                    'e'  => $e->energy_level_1_5,                      // energy
                    'h'  => $e->hunger_1_5,
                    'fu' => $e->fullness_1_5,
                ];
            })->all(),
        ];
    }

    protected function buildPrompt(array $payload): string
    {
        // Bladeテンプレートを使ってプロンプト文字列を組み立てる
        return view('prompts.daily_feedback', [
            'payload' => $payload,
        ])->render();
    }
}

ポイントは、次の3つです。

役割ごとにメソッドを分ける

buildPayload():Eloquentのコレクションを、「AIに渡しやすい配列」に変換する
buildSystemPrompt():カウンセラーの性格や禁止ワードをまとめた system プロンプトを返す
buildPrompt():その日のログを埋め込んだ user プロンプトを作る

Blade でプロンプトをテンプレート化する

resources/views/prompts/daily_feedback_system.blade.php
resources/views/prompts/daily_feedback_user.blade.php
に分けておき、view(...)->render() で文字列として取り出しています。
文章のトーンを調整するときに、PHPのコードを触らなくていいのが地味に便利でした。

フラグと例外で安全側に倒す

.env に AI_FEEDBACK_ENABLED フラグを持たせて、普段は false にしておきます。
フラグが OFF のときや API エラーのときはシンプルなエラーメッセージだけ出すようにし、画面上は500エラーにならないようにしました。
コントローラ側では、このサービスを呼んで $feedback としてビューに渡すだけにしておき、

  • 実際にAPIを叩くかどうか
  • どのモデル(例:gpt-4.1-mini)を使うか
  • どんなプロンプトでコメントしてもらうか

といった「中身のロジック」は DailyFeedbackService の中だけで完結するようにしました。

APIを叩くかどうかは .env のフラグで切り替え可能にしていて、

AI_FEEDBACK_ENABLED=false
OPENAI_API_KEY=sk-xxxx...
OPENAI_MODEL=gpt-4.1-mini
  • 開発中:false(APIを呼ばない)
  • 本番運用:true(コメントを生成)

という感じで使い分ける予定です。

プロンプト設計とトークン節約の工夫

日次コメントでは、1日ぶんのログをまるごと送るので、
そのまま日本語テキストにするとトークン量がかなり重くなります。

そのため、今回の v0 では次のような工夫をしています。

  • system と user を分離
    • system には「CBT-OB を参考にしたカウンセラーとして振る舞うこと」だけを書く
    • user には「その日のログ」と「出力フォーマットの指定」だけを書く
  • ログは JSON っぽい形で渡す
    • entries の各要素は {t: 時刻, k: meal_type, f: 食べたもの, c: kcal, m: 気分, e: 体力, h: 空腹度, fu: 満腹度} のような短いキーにして、
    • 「キーの意味」は user プロンプト側で1行だけ説明する形にしました。
  • 出力の長さを決めておく
    • 「今日のまとめ」「気づき」「次の日を少し楽にするヒント」の3ブロックに絞り、
    • それぞれ 1〜3 行におさめてもらうように指定しています。
    • max_tokens も 200 程度に設定しました。

これくらい絞っても、実際に返ってくる文章は

「今日はこういう一日だったね」「こういう選択ができていたね」

といった、こちらが欲しかったニュアンスを十分含んでくれていて、
「トークンをケチりながらフレンドリーなカウンセラーっぽさを保つ」バランスは取れたかなと思います。

v0導入後の変化と得られた手応え

v0はまだ完成版ではなく、機能としても最低限です。
それでも、実際に動かし始めるといくつか良い変化がありました。

1. 「ちゃんとできた/できてない」から少し距離が取れた

市販のレコーディングアプリだと、目標カロリーや体重のグラフに意識が持っていかれてしまって、

  • 「今日はオーバーした」
  • 「グラフが悪化してる」

といったところに目が行きがちでした。

自分で作ったこのツールは、

  • 目標値を出さない
  • グラフもまだ描かない
  • 今日はここまでで「X kcal、Y食」という事実だけが出てくる
  • AIが客観的なコメントをくれる

ので、「良い/悪い」の評価よりも、「今日はこうだったんだな」と現状を眺める感覚に少しずつ寄せていけている気がします。
実際、「今日は食事のタイミングが夜に固まっていたね」「眠気が強い日にどうしていたかが見えてきたね」 といったコメントが返ってくることで、「その日の自分の行動パターンを眺める」感覚が少しずつ育ってきているように感じます。

2. 「ログを探すストレス」が減った

ChatGPTとの会話ログだけで食事や体調を管理しようとすると、

  • どのチャットで何を話したか見つかりづらい
  • いつどんな食事をしたか見えづらい
  • ChatGPTが過去の会話から観察するのが苦手

というのもあり、振り返りが大変です。

1食=1行でDBに溜める形にしたことで、

  • ある日の食事だけをぱっと一覧できる
  • 将来的に日次・週次での振り返りもしやすそう
  • ChatGPTにコメントしてほしい領域を絞りやすそう

という手応えが出てきました。

3. 「まだ育てていける余白」がある

v0はあくまで土台であって、

  • 体調管理のため記録したい内容は他にも残っている
  • やってみたいSPA化もまだ
  • 可視化もまだ

という、やっていないことだらけの状態です。
自作ツールに着手したおかげで、技術的に試してみたいことをここでいろいろ挑戦できる場所ができました。

おわりに

去年の記事では、うつ病で働けなくなった自分がどうにか今の職場までたどり着いたという、「ここまで戻ってくるまでの話」を書きました。

今年はその続きとして、体調と付き合いながらどうやって働き続けるかを少し具体的にするため、

  • ChatGPT という相棒を持ち、
  • 自分仕様のミニ Web アプリを作って、
  • 体調管理の一部を外部に預けながらやっていく

という「途中経過」を紹介しました。

まだまだ道の途中ですし、この記事を書いている今も、来年の自分がどうなっているかは分かりません。
それでも、

  • 去年:働けるようになった
  • 今年:ChatGPT や小さなツールの力も借りながら、1年なんとか働き続けられた

という成果があったことは、ひとまずここに残しておきたいと思います。

もし似たような状況で「自分仕様の記録環境」を模索している方がいれば、
どこか参考になったり、「自分もゆっくりやっていけばいいか」と思ってもらえたらうれしいです。

もしまたQiitaを書く機会があれば、

  • v0 がどう育ったか
  • AIコメントや可視化がどこまで役に立ったか
  • そして、まだ元気に働けているか

を、また途中経過として書けたらいいなと思います。

25
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?