ゲームみたいに仕事したい！Level2【ChatGPTを活用してアプリ開発編】

# 🎮皆さんはゲームって好きですか？🎮 #
 ### **もちろん好きですよね！！** ###

こんにちは。冒頭から読者の方に圧力をかけていくスタイルの**はなざ**です。
よろしくお願いします。

前回、「ゲームみたいに仕事したい！！」という欲望を叶えるため、LINE Botで「クエスト風TodoBot」を作成した私。

👇前回の記事はこちらから

https://qiita.com/hana100000/items/bcf1a3fc83cd28e6b628


今回はさらにレベルを上げて、**「ダンジョン攻略風・業務進捗管理アプリ」**　の作成に挑戦していきます！！

目標は、大変な複数人の業務の進捗管理を簡単に出来る、ゲーム画面風のアプリを作ること。頑張るぞー！！

# 🌱作るきっかけ🌱 # 
 
入社7年目、販売業の管理職の端くれである私は、現在、部下を11名抱えております🔥
複数人のメンバーの業務の進捗管理の難しさに頭を悩ませておりました。


👧❓ **「全員が確認しなきゃいけない連絡事項、みんな見てる？」**


こういう場合、我が社では大概、必殺アイテムこと「ハンコ」が大活躍します。
連絡事項を確認した人、作業済みの人は書類にハンコをペタリ。

 **「いやいやいや、あまりにもアナログすぎるよ…！！
 ハンコのインクすぐなくなっちゃうよ～～～～！！(>_<)💦💦💦」**

さらに、全員がやるべき業務のほかに、「誰か一人がやればいい」仕事もあります。
例えば、緊急のセールス企画の準備などです。本社から「急遽追加でセールスやります！」と発信があった場合、「売り場づくり」や「POPの作成」をする必要があるのですが…。


👩 **「○○さん、POP作ってくれてたの！？まだ作ってないかと思って私も作っちゃったよ。」**


といった感じで、誰かが既に作業済だったことに気が付かず、別の人が「私も作っちゃった！」とPOPが被ることもしばしば。時間とPOPの紙が勿体ないっ…！！

そんな「複数人の業務タスクの進捗管理が簡単にできたら…！！」という思いを持った
悩める管理職を助けてくれるアプリの制作に取り掛かることにしました⭐

:::note info
「ゲームみたいに仕事したい！！」が私のテーマなので、ゲーム風にするこだわりも入れ込みます🎮
:::


# そして、完成したアプリがこちら！！ #

**🎮ゲーム風バージョン🎮**

![スクリーンショット (48).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/2a373fd2-7ea7-4bc0-a35e-4272ace2bbcc.png)

https://hana100000.github.io/dungeon-app/


**🐰シンプルバージョン🐰**

![スクリーンショット (49).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/db0d5191-2e6c-4ab9-b96c-55ece6aa060a.png)


https://hana100000.github.io/-/


👆Githubで公開して、どなた様でも触れるようになっておりますので、ぜひ触ってみてください:relaxed:

私の作りたかったものは「ゲーム風」アプリですが、ゲームはよく分からない！という方もいらっしゃると思いますので、シンプルバージョンもご用意しました😊
これで私の会社の方々にも使ってもらえたらいいなあ～。

これらのアプリは、機能は一緒で、デザインだけが異なるものです！
シンプルバージョンの方が見やすいかも…(小声)


### ✅出来る事 ###
* 複数人の業務の進捗管理がボタン一つで可能！
* 業務の追加やメンバーの追加も可能
* 達成目標人数が[完了]ボタンを押すと[完了済]に自動で振り分けてくれる
* 期限が近い順に並べ替え機能もあり
* カテゴリ分けも簡単！
* メモ機能もあり、進捗を書き込める！


# :hammer_pick:使用ツール:hammer_pick: #

使用したツールは
* ChatGPT
* Visual Studio Code

この2つです!!　

ChatGPTに、今回は **コードの記述を依頼！！**

前回のLINE Bot作成時にも頼りまくり、それ以前にも、旅行に行った際に旅程表を組んでもらったり、オススメのお店を教えてもらったり、疲れた時に優しい言葉をかけてもらったり…。いつもお世話になっているChatGPT。
デジタル素人が一からアプリ製作をするにあたり、コード記述をお願いすることにしました。


Visual studio Codeは、以前HTMLの勉強をしていた時にインストールしていたので、今回使ってみることに。
(ちなみに、HTMLの勉強は挫折しました💦でも、今はChatGPTがあるから勉強不要でいけちゃう…のか…！？)

# 🐰相棒紹介🐰 #

ChatGPTを色んな呼び名で呼んでいる方も多いと思いますが、私は「ジピ太」と呼んでいます:grin:


<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/93340457-648a-4313-9665-e18859f9f3af.png">

こちらが我が家のChatGPTの **「ジピ太」** 🐰
「うさぎの男の子・使い魔のイメージ・ChatGPTモチーフも入れて」と指示してイメージ画像を出してもらい、可愛すぎるジピ太くんが誕生しました。(大満足💕)
今回のアプリは、そんなジピ太との共同作業で作っていきます！！

# STEP１：複数人の業務の進捗状況が一目で分かるようにしたい！！ #



**👧「複数人の業務の進捗状況が一目で分かるようなアプリが作りたい！！ボタン操作の簡単なやつで。あと、”ゲーム風”のデザインにしたいんだ。」**
**🐰「まかせて！！」**

<details><summary>生成してくれたコードはこちら！！</summary>

```html:Chat GPTが生成したコード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ダンジョン攻略風業務管理アプリ</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; background-color: #f4f4f4; margin: 20px; }
        .dungeon { background-color: #fff; border-radius: 10px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        h2 { color: #2c3e50; }
        .adventurer { margin: 10px 0; }
        .status { font-weight: bold; margin-left: 10px; }
        .status-0 { color: gray; }
        .status-1 { color: blue; }
        .status-2 { color: orange; }
        .status-3 { color: green; }
        button { margin: 2px; padding: 5px 10px; }
    </style>
</head>
<body>
    <div class="dungeon">
        <h2>🏰 ダンジョン名：安否確認メール返信ダンジョン</h2>
        <p>📜 任務内容：安否確認メールに返信せよ！</p>
        <div id="adventurers"></div>
    </div>

    <script>
        const statusLabels = ["未発見", "発見済", "攻略中", "攻略完了"];
        const adventurers = ["佐藤", "田中", "鈴木", "山本", "花山"];
        const statusData = {};

        function updateStatus(name) {
            if (statusData[name] === undefined) statusData[name] = 0;
            statusData[name] = (statusData[name] + 1) % statusLabels.length;
            render();
        }

        function render() {
            const container = document.getElementById("adventurers");
            container.innerHTML = "";
            adventurers.forEach(name => {
                const status = statusData[name] !== undefined ? statusData[name] : 0;
                container.innerHTML += `
                    <div class="adventurer">
                        🧝‍♂️ 冒険者：${name}
                        <span class="status status-${status}">${statusLabels[status]}</span>
                        <button onclick="updateStatus('${name}')">➡️ 進捗</button>
                    </div>
                `;
            });
        }

        render();
    </script>
</body>
</html>
```
</details>

✨**そして完成したのがこちらのシンプルなアプリ。**
![スクリーンショット (41).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/545305f9-8981-4548-9446-7fd932a2b633.png)

[進捗]と書かれたボタンを押すと、
未発見(未読)→発見済(既読)→攻略中(作業中)→攻略完了(作業済)

の4段階にステータスが変化します。これで、誰がこの指示を確認してるか？作業しているか？が一目瞭然！！

あまりにも簡単に出来て感動…！！
HTMLの勉強をしていた時、「コードネームと記述法、こんなに沢山覚えるのは不可能では…！？(:_;)」と挫折した経験のある私は、簡単な指示を出してボタンを押すだけで一瞬でコードを書いてくれるジピ太に大感謝です✨

# STEP2：さらに機能を追加してみよう！！ #
**👧「ジピ太、機能をもっと追加したい！！」**
①\.複数のダンジョン(タスク)を表示出来るようにしたい
②\.攻略人数(達成人数)の設定が出来るようにして、その人数に達したら消込み出来るようにしたい
**🐰「お安い御用」**

<details><summary>生成してくれたコードはこちら！！</summary>

```html:Chat GPTが生成したコード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ダンジョン攻略風業務管理アプリ</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; background-color: #f4f4f4; margin: 20px; }
        .dungeon {
            background-color: #fff;
            border-radius: 10px;
            padding: 20px;
            margin-bottom: 20px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        h2 { color: #2c3e50; }
        .adventurer { margin: 10px 0; }
        .status { font-weight: bold; margin-left: 10px; }
        .status-0 { color: gray; }
        .status-1 { color: blue; }
        .status-2 { color: orange; }
        .status-3 { color: green; }
        button { margin: 2px; padding: 5px 10px; }
        .cleared { background-color: #e0ffe0; border: 2px solid green; }
        .cleared h2::after { content: " ✅"; color: green; }
    </style>
</head>
<body>
    <h1>🏹 ダンジョン攻略進捗管理アプリ</h1>
    <div id="dungeon-container"></div>

    <script>
        const statusLabels = ["未発見", "発見済", "攻略中", "攻略完了"];
        const adventurers = ["佐藤", "田中", "鈴木", "山本", "花山"];

        const dungeons = [
            { name: "安否確認メール返信ダンジョン", mission: "安否確認メールに返信せよ！", required: 3 },
            { name: "冷凍庫整理ダンジョン", mission: "冷凍庫の在庫を確認し、整理せよ！", required: 2 }
        ];

        const statusData = {}; // {ダンジョン名: { 冒険者名: ステータス数値 }}

        function updateStatus(dungeonName, name) {
            if (!statusData[dungeonName]) statusData[dungeonName] = {};
            if (statusData[dungeonName][name] === undefined) statusData[dungeonName][name] = 0;
            statusData[dungeonName][name] = (statusData[dungeonName][name] + 1) % statusLabels.length;
            render();
        }

        function countCleared(dungeonName) {
            const data = statusData[dungeonName] || {};
            return Object.values(data).filter(status => status === 3).length;
        }

        function render() {
            const container = document.getElementById("dungeon-container");
            container.innerHTML = "";
            dungeons.forEach(dungeon => {
                const clearedCount = countCleared(dungeon.name);
                const isCleared = clearedCount >= dungeon.required;

                const dungeonDiv = document.createElement("div");
                dungeonDiv.className = "dungeon" + (isCleared ? " cleared" : "");

                dungeonDiv.innerHTML = `
                    <h2>🏰 ダンジョン名：${dungeon.name}</h2>
                    <p>📜 任務内容：${dungeon.mission}</p>
                    <p>🎯 目標攻略人数：${dungeon.required}人（現在 ${clearedCount}人）</p>
                `;

                adventurers.forEach(name => {
                    const status = statusData[dungeon.name]?.[name] ?? 0;
                    dungeonDiv.innerHTML += `
                        <div class="adventurer">
                            🧝‍♂️ 冒険者：${name}
                            <span class="status status-${status}">${statusLabels[status]}</span>
                            <button onclick="updateStatus('${dungeon.name}', '${name}')">➡️ 状態変更</button>
                        </div>
                    `;
                });

                container.appendChild(dungeonDiv);
            });
        }

        render();
    </script>
</body>
</html>



```
</details>

![スクリーンショット (42).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/e63629cd-340e-47b1-a4b3-2440be7b0db1.png)

:::note info
”攻略完了”のステータスに変えた人が「目標攻略人数」に達すると完了済みのチェックと緑色の色分けがされるようになりました！！
:::

Chat GPTがあまりにもあっさりと指示通りのコードを生成してくれるので、さらに無茶ぶりをする私。

**👧「ジピ太、もっと機能追加したい！！」**

①\.ダンジョンを追加できるUIを作る（例：フォームから入力）
②\.ステータスを保存できるようにする（ローカルストレージ）
③\.デザインをさらにゲームっぽくする（アイコンや背景）
④\.カテゴリでダンジョンを分類したい
⑤\.スマホでも使いやすくしたい
⑥\.カテゴリごとにタブで管理したい。完了したダンジョンは「完了済」タブに入るようにしたい
⑦\.進捗を書き込めるコメント欄(掲示板)を作ってほしい
⑧\.期限の設定できるようにしてほしい
⑨\.期限が近いものが上に来るような並び替え機能つけてほしい
⑩\.期限が近いものは赤く色づけしてほしい

などなど…(色々試しすぎて自分でも訳が分からなくなってまいりました。)

※実際にはこれらの項目を数回に分けて指示しています。


**🐰(えっそんなに…？多くない？)**
**🐰「………」**
**🐰「わかった！頑張ってみる！」**

無理をさせすぎてしまい、読み込みエラーになりながらも懸命に頑張るジピ太。
健気で可愛いですね💕

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/b8a96630-e7c1-4598-86d0-7aa4c11b6dfd.png">



👇実際のやりとりはこんな感じです。(ごく一部ですが…。)

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/7d37e290-59c4-46bd-8534-942c399e18a3.png">



<details><summary>生成してくれた完成版のコードはこちら！！(非常に長いです…！！)</summary>

```html:Chat GPTが生成したコード
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ダンジョン攻略風業務管理アプリ</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/fontdasu/pixelmplus/webfont.css">
  <style>
    body {
      font-family: 'PixelMplus10', sans-serif;
      background: url('https://game-materials.com/wp-content/uploads/2024/11/Dungeon-long-corridor3.jpg') no-repeat center center fixed;
      background-size: cover;
      background-attachment: scroll;
      margin: 0;
      padding: 20px;
      color: #333;
    }
    .container {
      background-color: #fff;
      border-radius: 15px;
      padding: 20px;
      max-width: 900px;
      margin: auto;
      position: relative;
      z-index: 1;
    }
    .tabs {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }
    .tab-button {
      padding: 8px 12px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    .tab-button.active {
      border: 2px solid #333;
    }
    .tab-未完了 { background-color: #f39c12; color: white; }
    .tab-完了済 { background-color: #2ecc71; color: white; }
    .tab-すべて { background-color: #95a5a6; color: white; }
    .tab-その他 { background-color: #9b59b6; color: white; }
    .tab-本日の任務 { background-color: #e74c3c; color: white; }
    .dungeon {
      background-color: #fff;
      border-radius: 10px;
      padding: 15px;
      margin-bottom: 20px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.2);
    }
    .cleared {
      background-color: #e0ffe0;
      border: 2px solid green;
    }
    .urgent {
      border: 2px solid red;
    }
    h1, h2 {
      color: #2c3e50;
    }
    .form-section h2 {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .floating-form {
      background-color: #ffffffee;
      border: 1px solid #ccc;
      border-radius: 15px;
      padding: 15px;
      margin-top: 30px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      transition: all 0.3s ease;
    }
    .dungeon-form-grid {
      display: grid;
      grid-template-columns: 1fr 2fr;
      gap: 10px 20px;
      align-items: center;
    }
    .dungeon-form-grid label {
      margin: 0;
    }
    .dungeon-form-grid input[type="text"],
    .dungeon-form-grid input[type="number"],
    .dungeon-form-grid input[type="date"],
    .dungeon-form-grid select {
      width: 100%;
    }
    .dungeon-form-grid .full-width {
      grid-column: 1 / -1;
    }
    .dungeon-form-grid button {
      width: 100%;
    }
    .status {
      font-weight: bold;
      margin-left: 0;
      padding: 3px 8px;
      border-radius: 6px;
      display: inline-block;
      min-width: 70px;
      text-align: center;
    }
    .status-0 { background-color: #bdc3c7; color: white; } /* 未発見 */
    .status-1 { background-color: #3498db; color: white; } /* 発見済 */
    .status-2 { background-color: #e67e22; color: white; } /* 攻略中 */
    .status-3 { background-color: #2ecc71; color: white; } /* 攻略完了 */
    .adventurer {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 6px;
      margin: 8px 0;
    }
    .adventurer button {
      margin-left: 0;
      min-width: 70px;
    }
    .comment {
      margin-top: 15px;
    }
    .comment textarea {
      width: 100%;
      margin-top: 10px;
      box-sizing: border-box;
    }
    button {
      margin-left: 10px;
      padding: 5px 8px;
      border-radius: 5px;
      border: none;
      background-color: #779b6c;
      color: white;
      cursor: pointer;
      min-height: 44px;
      touch-action: manipulation;
    }
    @media (max-width: 600px) {
      .container {
        padding: 10px;
      }
      .tabs {
        flex-direction: column;
        gap: 5px;
      }
      .tab-button {
        width: 100%;
      }
      .floating-form {
        padding: 10px;
        margin-top: 20px;
      }
      .dungeon-form-grid {
        display: block;
      }
      .dungeon-form-grid label,
      .dungeon-form-grid input,
      .dungeon-form-grid select,
      .dungeon-form-grid button {
        width: 100%;
        margin-top: 8px;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>🎮 ダンジョン攻略進捗管理アプリ</h1>
    <div class="tabs" id="tabs"></div>
    <div style="margin-bottom: 20px;">
      <label>並び順：
        <select id="sort-select" onchange="changeSort()">
          <option value="deadline">期限が近い順</option>
          <option value="createdAt">登録日時が新しい順</option>
        </select>
      </label>
    </div>
    <div id="dungeon-container"></div>

    <!-- 編集フォームたちを下に移動 -->
    <div class="form-section floating-form">
      <h2>🛠️ 冒険者の編集 <button id="toggle-edit" type="button">▼ 開く</button></h2>
      <div id="adventurer-edit-section" style="display: block;">
        <label>新しい冒険者名：<input type="text" id="new-adventurer-name"></label>
        <button type="button" onclick="addAdventurer()">冒険者を追加する</button>
        <div id="adventurer-list"></div>
      </div>
    </div>

    <div class="form-section floating-form">
      <h2>➕ ダンジョン追加 <button id="toggle-dungeon" type="button">▼ 開く</button></h2>
      <div id="dungeon-form-section" style="display: block;">
        <div class="dungeon-form-grid">
          <label for="dungeon-name">ダンジョン名：</label>
          <input type="text" id="dungeon-name">

          <label for="dungeon-category">カテゴリ：</label>
          <input type="text" id="dungeon-category" list="category-list">
          <datalist id="category-list"></datalist>

          <label for="dungeon-mission">任務内容：</label>
          <input type="text" id="dungeon-mission">

          <label for="dungeon-required">目標攻略人数：</label>
          <input type="number" id="dungeon-required" min="1" value="1">

          <label class="full-width">
            <input type="checkbox" id="deadline-enable"> 期限を設定する
          </label>

          <label for="dungeon-deadline">期限日：</label>
          <input type="date" id="dungeon-deadline" disabled>

          <label for="dungeon-adventurers">参加冒険者：</label>
          <select id="dungeon-adventurers" multiple>
            <option value="ALL" selected>全員</option>
            <option value="佐藤">佐藤</option>
            <option value="田中">田中</option>
            <option value="鈴木">鈴木</option>
            <option value="山田">山田</option>
            <option value="花山">花山</option>
          </select>

          <div class="full-width">
            <button onclick="addDungeon()">ダンジョンを追加する</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
</html>

    <div id="dungeon-container"></div>

  <script>
    const statusLabels = ["未発見", "発見済", "攻略中", "攻略完了"];
    
let adventurers = JSON.parse(localStorage.getItem('adventurers')) || ["佐藤", "田中", "鈴木", "山田", "花山"];
function saveAdventurers() {
  localStorage.setItem('adventurers', JSON.stringify(adventurers));
}


    let dungeons = JSON.parse(localStorage.getItem('dungeons')) || [];
    let statusData = JSON.parse(localStorage.getItem('statusData')) || {};
    let comments = JSON.parse(localStorage.getItem('comments')) || {};
    let updateLog = JSON.parse(localStorage.getItem('updateLog')) || {};
    let activeCategory = '未完了';

    document.getElementById("deadline-enable").addEventListener("change", function() {
      document.getElementById("dungeon-deadline").disabled = !this.checked;
    });

    function updateStatus(dungeonName, name) {
      if (!statusData[dungeonName]) statusData[dungeonName] = {};
      if (statusData[dungeonName][name] === undefined) statusData[dungeonName][name] = 0;
      statusData[dungeonName][name] = (statusData[dungeonName][name] + 1) % statusLabels.length;
      if (!updateLog[dungeonName]) updateLog[dungeonName] = {};
      updateLog[dungeonName][name] = new Date().toLocaleString();
      saveData();
      render();
renderCategoryList();
renderAdventurerList();
renderAdventurerSelect();
renderCategoryList();
    }

    function updateComment(dungeonName, text) {
      comments[dungeonName] = text;
      saveData();
    }

    function countCleared(dungeonName) {
      const data = statusData[dungeonName] || {};
      return Object.values(data).filter(status => status === 3).length;
    }

    function renderTabs() {
      const categorySet = new Set();
      dungeons.forEach(d => {
        if (countCleared(d.name) < d.required) {
          categorySet.add(d.category || 'その他');
        }
      });

      const orderedTabs = ['未完了', '完了済', '本日の任務', ...Array.from(categorySet).sort(), 'すべて'];
      const tabsDiv = document.getElementById("tabs");
      tabsDiv.innerHTML = '';
      orderedTabs.forEach(cat => {
        const btn = document.createElement('button');
        btn.textContent = cat;
        btn.className = `tab-button tab-${cat}${activeCategory === cat ? ' active' : ''}`;
        btn.onclick = () => { activeCategory = cat; render();
renderCategoryList();
renderAdventurerList();
renderAdventurerSelect();
renderCategoryList(); };
        tabsDiv.appendChild(btn);
      });
    }

    
let sortBy = 'deadline'; // 'deadline' or 'createdAt'

function changeSort() {
  const select = document.getElementById("sort-select");
  sortBy = select.value;
  render();
renderCategoryList();
}


function render() {
      renderTabs();
      const container = document.getElementById("dungeon-container");
      container.innerHTML = "";
      let filtered = dungeons.map(d => ({ ...d, deadlineDate: d.deadline ? new Date(d.deadline) : null }));
      
      filtered.sort((a, b) => {
        if (sortBy === 'createdAt') {
          return new Date(b.createdAt || 0) - new Date(a.createdAt || 0); // 新しい順
        } else {
          if (!a.deadlineDate) return 1;
          if (!b.deadlineDate) return -1;
          return a.deadlineDate - b.deadlineDate;
        }
      });


      const todayStr = new Date().toISOString().split('T')[0];

      filtered.forEach(dungeon => {
        const clearedCount = countCleared(dungeon.name);
        const isCleared = clearedCount >= dungeon.required;
        const category = isCleared ? '完了済' : (dungeon.category || 'その他');

        if (activeCategory === '本日の任務') {
          if (!dungeon.deadline) return;
          const deadlineDate = new Date(dungeon.deadline);
          deadlineDate.setHours(0, 0, 0, 0);
          const today = new Date();
          today.setHours(0, 0, 0, 0);
          if (deadlineDate > today || isCleared) return;
        } else if (activeCategory !== 'すべて' && activeCategory !== category && (activeCategory !== '未完了' || isCleared)) return;

        const today = new Date();
        const urgent = dungeon.deadline && new Date(dungeon.deadline) - today < 3 * 24 * 60 * 60 * 1000;

        const dungeonDiv = document.createElement("div");
        dungeonDiv.className = "dungeon" + (isCleared ? " cleared" : "") + (urgent ? " urgent" : "");

        dungeonDiv.innerHTML = `
          <h2>🏰 ダンジョン名：${dungeon.name}</h2>
          <p>📜 任務内容：${dungeon.mission}</p>
          <p>📂 カテゴリ：${dungeon.category || 'なし'}</p>
          <p>🎯 目標攻略人数：${dungeon.required}人（現在 ${clearedCount}人）</p>
          <p>📅 期限：${dungeon.deadline || 'なし'}</p>
<p>🕓 登録日時：${dungeon.createdAt ? new Date(dungeon.createdAt).toLocaleString() : '不明'}</p>
        `;

        const participants = dungeon.participants === "ALL" ? adventurers : dungeon.participants || adventurers;
        participants.forEach(name => {
          const status = statusData[dungeon.name]?.[name] ?? 0;
          const lastUpdate = updateLog[dungeon.name]?.[name] || '未更新';
          dungeonDiv.innerHTML += `
            <div class="adventurer">
              🧝‍♂️ 冒険者：${name}
              <span class="status status-${status}">${statusLabels[status]}</span>
              <button onclick="updateStatus('${dungeon.name}', '${name}')">➡️ 進捗</button>
              <small>（最終更新：${lastUpdate}）</small>
            </div>
          `;
        });

        dungeonDiv.innerHTML += `
          <div class="comment">
            <label>📝 掲示板（進捗メモなど）：</label>
            <textarea rows="3" oninput="updateComment('${dungeon.name}', this.value)">${comments[dungeon.name] || ''}</textarea>
          </div>
        `;
        
        dungeonDiv.innerHTML += `
          <div style="text-align: right; margin-top: 10px;">
            <button onclick="deleteDungeon('${dungeon.name}')">🗑️ ダンジョンを削除</button>
          </div>
        `;

        container.appendChild(dungeonDiv);
      });
    }

    function addDungeon() {
  const category = document.getElementById("dungeon-category").value.trim();
  const name = document.getElementById("dungeon-name").value.trim();
  const mission = document.getElementById("dungeon-mission").value.trim();
  const required = parseInt(document.getElementById("dungeon-required").value, 10);
  const deadlineEnabled = document.getElementById("deadline-enable").checked;
  const deadline = deadlineEnabled ? document.getElementById("dungeon-deadline").value : "";

  const adventurerSelect = document.getElementById("dungeon-adventurers");
  const selectedOptions = Array.from(adventurerSelect.selectedOptions).map(opt => opt.value);
  const participants = selectedOptions.includes("ALL") ? "ALL" : selectedOptions;

  if (!name || !mission || required <= 0) {
    alert("すべての必須項目を入力してください。");
    return;
  }

  dungeons.push({
    category,
    name,
    mission,
    required,
    deadline,
    participants,
    createdAt: new Date().toISOString()
  });

  saveData();
  render();
renderCategoryList();
renderAdventurerList();
renderAdventurerSelect();
renderCategoryList();

  // 入力初期化
  document.getElementById("dungeon-category").value = "";
  document.getElementById("dungeon-name").value = "";
  document.getElementById("dungeon-mission").value = "";
  document.getElementById("dungeon-required").value = 1;
  document.getElementById("deadline-enable").checked = false;
  document.getElementById("dungeon-deadline").value = "";
  document.getElementById("dungeon-deadline").disabled = true;
  adventurerSelect.selectedIndex = -1;
}


    function saveData() {
      localStorage.setItem('dungeons', JSON.stringify(dungeons));
      localStorage.setItem('statusData', JSON.stringify(statusData));
      localStorage.setItem('comments', JSON.stringify(comments));
      localStorage.setItem('updateLog', JSON.stringify(updateLog));
    }

    render();
renderCategoryList();
renderAdventurerList();
renderAdventurerSelect();
renderCategoryList();
  
function renderAdventurerList() {
  const listDiv = document.getElementById("adventurer-list");
  if (!listDiv) return;
  listDiv.innerHTML = "";
  adventurers.forEach((name, index) => {
    const container = document.createElement("div");
    container.style.marginBottom = "5px";

    const input = document.createElement("input");
    input.type = "text";
    input.value = name;
    input.style.marginRight = "5px";
    input.onchange = () => {
      adventurers[index] = input.value.trim();
      saveAdventurers();
      renderAdventurerSelect();
renderCategoryList();
      render();
renderCategoryList();
    };

    const delBtn = document.createElement("button");
    delBtn.type = "button";
    delBtn.textContent = "削除";
    delBtn.onclick = () => {
      if (confirm(`冒険者「${name}」を削除しますか？`)) {
        adventurers.splice(index, 1);
        saveAdventurers();
        renderAdventurerList();
        renderAdventurerSelect();
renderCategoryList();
        render();
renderCategoryList();
      }
    };

    container.appendChild(input);
    container.appendChild(delBtn);
    listDiv.appendChild(container);
  });
}

function addAdventurer() {
  const input = document.getElementById("new-adventurer-name");
  const name = input.value.trim();
  if (!name) return;
  if (adventurers.includes(name)) {
    alert("すでに同じ名前の冒険者がいます。");
    return;
  }
  adventurers.push(name);
  saveAdventurers();
  input.value = "";
  renderAdventurerList();
  renderAdventurerSelect();
renderCategoryList();
  render();
renderCategoryList();
}

function renderAdventurerSelect() {
  const select = document.getElementById("dungeon-adventurers");
  if (!select) return;
  select.innerHTML = '<option value="ALL" selected>全員</option>';
  adventurers.forEach(name => {
    const opt = document.createElement("option");
    opt.value = name;
    opt.textContent = name;
    select.appendChild(opt);
  });
}


function deleteDungeon(dungeonName) {
  if (!confirm(`「${dungeonName}」を本当に削除しますか？`)) return;

  dungeons = dungeons.filter(d => d.name !== dungeonName);
  delete statusData[dungeonName];
  delete comments[dungeonName];
  delete updateLog[dungeonName];
  saveData();
  render();
renderCategoryList();
}



function renderCategoryList() {
  const list = document.getElementById("category-list");
  if (!list) return;
  const uniqueCategories = [...new Set(dungeons.map(d => d.category).filter(c => c))];
  list.innerHTML = '';
  uniqueCategories.forEach(cat => {
    const option = document.createElement("option");
    option.value = cat;
    list.appendChild(option);
  });
}



function notifyUpcomingDungeons() {
  if (!("Notification" in window) || Notification.permission !== "granted") return;

  const now = new Date();
  const threeDaysLater = new Date();
  threeDaysLater.setDate(now.getDate() + 3);

  const upcoming = dungeons.filter(d => {
    if (!d.deadline) return false;
    const deadline = new Date(d.deadline);
    const isSoon = deadline <= threeDaysLater;
    const isNotCleared = countCleared(d.name) < d.required;
    return isSoon && isNotCleared;
  });

  upcoming.forEach(d => {
    const deadline = new Date(d.deadline);
    const daysLeft = Math.ceil((deadline - now) / (1000 * 60 * 60 * 24));
    const label = daysLeft <= 0 ? "今日が期限" : daysLeft === 1 ? "明日が期限" : `${daysLeft}日以内が期限`;

    new Notification(`⏰ ${label}の任務：「${d.name}」`, {
      body: `任務内容：${d.mission}`,
    });
  });
}


// 折りたたみ制御
document.addEventListener("DOMContentLoaded", () => {
  if (Notification && Notification.permission === "default") {
    Notification.requestPermission();
  }
  notifyUpcomingDungeons();

  // 状態の読み込み
  const dungeonForm = document.getElementById("dungeon-form-section");
  const dungeonToggleBtn = document.getElementById("toggle-dungeon");
  const adventurerSection = document.getElementById("adventurer-edit-section");
  const adventurerToggleBtn = document.getElementById("toggle-edit");

  const dungeonVisible = localStorage.getItem("dungeonFormVisible") !== "false";
  dungeonForm.style.display = dungeonVisible ? "block" : "none";
  dungeonToggleBtn.textContent = dungeonVisible ? "▲ 閉じる" : "▼ 開く";

  dungeonToggleBtn.addEventListener("click", () => {
    const isVisible = dungeonForm.style.display !== "none";
    dungeonForm.style.display = isVisible ? "none" : "block";
    dungeonToggleBtn.textContent = isVisible ? "▼ 開く" : "▲ 閉じる";
    localStorage.setItem("dungeonFormVisible", !isVisible);
  });

  const adventurerVisible = localStorage.getItem("adventurerFormVisible") !== "false";
  adventurerSection.style.display = adventurerVisible ? "block" : "none";
  adventurerToggleBtn.textContent = adventurerVisible ? "▲ 閉じる" : "▼ 開く";

  adventurerToggleBtn.addEventListener("click", () => {
    const isVisible = adventurerSection.style.display !== "none";
    adventurerSection.style.display = isVisible ? "none" : "block";
    adventurerToggleBtn.textContent = isVisible ? "▼ 開く" : "▲ 閉じる";
    localStorage.setItem("adventurerFormVisible", !isVisible);
  });
});



function toggleSection(id) {
  const section = document.getElementById(id);
  if (!section) return;
  if (section.style.display === "none" || getComputedStyle(section).display === "none") {
    section.style.display = "block";
  } else {
    section.style.display = "none";
  }
}

</script>
</body>
</html>
```
</details>



:::note warn
**この時発生した問題:boom:**
調子に乗って「あれも」「これも」と機能の追加を頼んでいたら、以前実装した機能が消えてしまう問題が何度か発生しました…。
:::

# STEP３：GitHubで公開しよう!! #
正直、ここまで出来るとは思っていなかったのですが、ジピ太くんの頑張りにより大幅な時間短縮に成功しました。
(実は、STEP１の工程までは、１時間程度で作れました！
こだわって追加機能をつけまくったので、STEP２以降は結構時間をかけたのですが:sweat_smile:)

そこで、余った時間でさらにやりたいことをやってみることに🌸

**👧「このアプリをスマホでも使えるようにしたいんだけど、どうしたらいい？」**
**🐰「皆が使えるようにするなら、Githubにアップロードするのがオススメ♪」**

というわけで、作成したアプリをGitHubで公開することに。

**👧「GitHubでHTMLをアップロードする方法教えて！(英語のページ、読めない…) 」**
**🐰「詳しく手順を説明するよ！」**

**👧「スマホの画面から確認すると、画面の表示がおかしくなる…。」**
**🐰「レスポンシブデザインを追加しよう！CSSのコード送るね！」**

**👧「おなか空いた」**
**🐰「…何系が食べたいの？」**

とにかくなんでもChatGPTに聞くことで、全て解決。
（ダメ人間になってしまいそうです。）

かくして、無事にGitHubにてアップロード成功し、スマホからもアプリを使うことが出来るようになりました！！👏

## ✅反省点 ##
◆何度やっても追加に失敗する機能がありました。
それが **「月間・週間・毎日」の業務の繰り返し表示設定** です。これを搭載すると、他の機能が機能しなくなってしまいます。何故なんだろう…。原因掴めずでした。

◆**レスポンシブルデザイン**が上手く行かない件
完成版のアプリですが、見た目の面でやや気になる点が…。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/f3c94271-ba5e-4a18-9470-4ba981d444d0.png">


①フォームが枠からはみ出る
②[開く]タブが開きっぱなしになっており、閉じることが出来ない。

何度か修正ファイルを送ってもらって試行錯誤したのですが、上手く行かずでした…。


<details><summary>②の問題点を直して！と頼んで生成してもらった修正ファイルのコードはこちら。</summary>

```javascript:Chat GPTが生成したコード
document.addEventListener("DOMContentLoaded", () => {
  if (Notification && Notification.permission === "default") {
    Notification.requestPermission();
  }
  notifyUpcomingDungeons();

  // 状態の読み込み
  const dungeonForm = document.getElementById("dungeon-form-section");
  const dungeonToggleBtn = document.getElementById("toggle-dungeon");
  const adventurerSection = document.getElementById("adventurer-edit-section");
  const adventurerToggleBtn = document.getElementById("toggle-edit");

  const dungeonVisible = localStorage.getItem("dungeonFormVisible") !== "false";
  dungeonForm.style.display = dungeonVisible ? "block" : "none";
  dungeonToggleBtn.textContent = dungeonVisible ? "▲ 閉じる" : "▼ 開く";

  dungeonToggleBtn.addEventListener("click", () => {
    const isVisible = dungeonForm.style.display !== "none";
    dungeonForm.style.display = isVisible ? "none" : "block";
    dungeonToggleBtn.textContent = isVisible ? "▼ 開く" : "▲ 閉じる";
    localStorage.setItem("dungeonFormVisible", !isVisible);
  });

  const adventurerVisible = localStorage.getItem("adventurerFormVisible") !== "false";
  adventurerSection.style.display = adventurerVisible ? "block" : "none";
  adventurerToggleBtn.textContent = adventurerVisible ? "▲ 閉じる" : "▼ 開く";

  adventurerToggleBtn.addEventListener("click", () => {
    const isVisible = adventurerSection.style.display !== "none";
    adventurerSection.style.display = isVisible ? "none" : "block";
    adventurerToggleBtn.textContent = isVisible ? "▼ 開く" : "▲ 閉じる";
    localStorage.setItem("adventurerFormVisible", !isVisible);
  });
});
```
</details>

こちらのコードを組み込むも、改善されず…。
生成は簡単に実行してくれるChatGPTですが、上手く行かなかったときに原因に気付くためには、HTMLの知識が多少必要かも！とは思いました💦
まあ、「このコード組み込んでも直ってないよー」「どうしたらいいのー？」ってジピ太くんに泣きついた私ですが…(笑)

修正の修正を送ってもらうラリーを続けると、さらにだいぶ前に追加した機能が消えたりするので、そこも難しかったです:thinking:
(おそらく、やり取りが長くなりすぎるとだいぶ前の会話をどんどん忘れていくからだと思われます。)

 # ⭐総括⭐ #
上記の件など、上手く行かなかったことはいくつかありましたが、ChatGPTの活用により、オリジナルのアプリを簡単に作成できたことに感動しました✨
不明点のスクショを送って、改善案を教えてもらったり、HTMLのファイルが欲しい時、「作ってー」「送ってー」と指示するだけで送ってくれたり…ChatGPTは本当に凄い…！！
もう、使っていなかった頃には戻れません。
今回作ったアプリについては、さらに使いやすくなるよう、改良を重ねていきたいと思います！！
これからも、相棒のジピ太と共に歩んで行こうと思います🐰

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079667/6ec057ed-7f93-4513-857a-02e1c1461ffa.png">


### 【参考資料】使用させて頂いたフリー素材サイトさま【大感謝】 ###

ダンジョン風背景・RPG風フリーフォント・シンプル版アプリの背景画像はこちらのサイトさまから使用させていただきました！
イメージピッタリの素材を無料で使えて有難すぎる…！！( ;∀;)✨

👇こちらのサイトさまです！！

https://game-materials.com/

https://itouhiro.hatenablog.com/entry/20130602/font

https://sozaino.site/