ChatGPT 4oと対話しながらwebサービスを個人開発する話 その③

これまでの記事でChatGPTと対話しながらWebサービスを開発する様子を掲載してきました。今回は、そこで開発したWebサービスに関する記事を紹介します。

開発したwebサービスは「みんなのナビ」というマニュアルを作成し、共有するサービスになります。

https://manualshare.net

↓以前の記事

https://qiita.com/travelclass0606/items/fceb85872620a418e8ab

https://qiita.com/travelclass0606/items/c38d042520e134ddcd57

# きっかけ
妻の実家に行った際、義母が大阪万博でブース鑑賞の申請をWebで行うのにとても苦労している様子を見て、誰かがわかりやすいマニュアルを作成し、共有できるサービスがあれば助かると感じた。

# ベース作り
まずは、ベースとなる部分をChatGPTに作成してもらいました。個人開発では、初期設定をChatGPTにさくっと作成してもらうことで、効率よく進められるのでおすすめです。以下のプロンプトをChatGPTに入力しました。

プロンプト①
---
やりたいこと：申請方法や入手方法のマニュアルを作成し、共有するwebサービスを作りたい。

きっかけ：母が大阪万博で鑑賞するブースの申請をwebで行う際に、とても苦労しており、誰かがわかりやすいマニュアルを作成し、共有できるサービスがあれば助かるなと感じた。

開発条件は以下の通り、環境構築の手順からコードも含め、詳細な方法を教えてください。
・環境：AWS(S3,Lambda,DynamoDB)
・フロントエンド：React
・バックエンド：Node
トップページ：
投稿されたものを閲覧ビューが多い順に10件を表示(それぞれ冒頭の数行を表示)
投稿の詳細閲覧ボタン、検索ボタン(ワード、カテゴリ別)、投稿ボタン
投稿ページ：
文字入力と途中に写真も掲載でき、入力しやすい仕様にしたい
投稿確認ページ：
投稿ページで入力したものの確認を行う。
問題なければ「確定」、修正する場合は「修正」ボタンを押す
検索結果一覧ページ：
トップページと同じような感じで閲覧ビューが多い順に10件ごとに表示
「次へ」ボタンで次の10件を表示させる、前の10件に戻る場合は「戻る」ボタンを押す
投稿詳細ページ
投稿内容を閲覧できる
閲覧ビュー数をカウントして表示させたい
コメント機能もいれたい(ニックネームとメッセージ)
補足：
ユーザー登録はなし
もし、これよりも適切な方法があれば適宜、提示してください

ChatGPTからの回答①
---

以下に、申請方法や入手方法のマニュアルを作成・共有するWebサービスの構築手順を示します。この設計は、AWSのサーバーレスサービス（S3, Lambda, DynamoDB）を利用して、フロントエンドをReact、バックエンドをNode.jsで構築する方法に基づいています。

### 1. AWS環境のセットアップ

1. **S3バケットの作成**:
   - S3で新規バケットを作成し、Reactのビルドファイル（HTML、CSS、JS）をホスティングする設定を行います。
   - 「静的ウェブサイトホスティング」を有効にし、デフォルトのドキュメントを`index.html`に設定します。

2. **DynamoDBテーブルの設定**:
   - `ManualsTable`と`CommentsTable`の2つのテーブルを作成します。
     - `ManualsTable`の主キー: `manualId` (String)
     - `CommentsTable`の主キー: `commentId` (String)、ソートキー: `manualId`
   - 必要に応じて`viewCount`や`category`属性を追加して、マニュアルの情報管理がしやすくします。

3. **Lambda関数の作成とAPI Gatewayの設定**:
   - **API Gateway**を使用してREST APIを構築し、Lambda関数で各エンドポイント（GET, POSTなど）を定義します。
   - 以下のようなエンドポイントを作成します：
     - `/manuals` (GET): 最新マニュアル、閲覧ビュー順に取得
     - `/manuals/{id}` (GET): マニュアルの詳細
     - `/manuals` (POST): マニュアルの作成
     - `/comments/{manualId}` (POST): コメントの追加

### 2. フロントエンド（React）の開発

#### プロジェクトのセットアップ
- Reactのプロジェクトを作成します。`npx create-react-app manual-share`コマンドでプロジェクトを作成します。
- 必要なライブラリを追加します（例: `axios` など）。

#### ページの構成

1. **トップページ** (`Home.js`):
   - DynamoDBから取得したマニュアルリストを表示するコンポーネント。
   - 各マニュアルの冒頭数行を表示し、「詳細を表示」ボタンで詳細ページへ移動。
   - `axios`を使用してAPI Gateway経由でマニュアルを取得。

2. **投稿ページ** (`CreateManual.js`):
   - マニュアルのタイトル、内容、カテゴリ、写真を入力するフォーム。
   - フォームの送信ボタンで内容をAPI Gatewayの`POST /manuals`エンドポイントへ送信し、データを保存。

3. **投稿確認ページ** (`ConfirmManual.js`):
   - `CreateManual.js`から受け取ったデータを確認表示し、`確定`ボタンでデータを送信、`修正`ボタンで投稿ページに戻ります。

4. **検索結果一覧ページ** (`SearchResults.js`):
   - カテゴリ・キーワード検索で取得した結果を閲覧ビューの多い順に表示。
   - 「次へ」「戻る」ボタンでページネーション処理を行います。

5. **投稿詳細ページ** (`ManualDetail.js`):
   - 詳細なマニュアル情報を表示し、閲覧ビュー数をカウント。
   - コメント入力欄を追加し、ニックネームとコメントを投稿可能にします。

#### AWS S3にデプロイ
- `npm run build`でビルド後、S3にビルドファイルをアップロードします。

### 3. バックエンド（Node.js with Lambda）

#### Lambda関数の実装

1. **GET `/manuals`**: DynamoDBの`ManualsTable`から最新10件を閲覧ビュー順で取得する関数を作成。
2. **GET `/manuals/{id}`**: マニュアルの詳細を取得し、閲覧ビュー数を更新します。
3. **POST `/manuals`**: 新規マニュアルを作成し、DynamoDBに保存します。
4. **POST `/comments/{manualId}`**: `CommentsTable`に新規コメントを追加します。

#### 例：Lambda関数のコード（GET `/manuals`）

```javascript
const AWS = require("aws-sdk");
const dynamoDb = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event) => {
  try {
    const params = {
      TableName: "ManualsTable",
      Limit: 10,
      // スキャンインデックス順序を指定する例
      ScanIndexForward: false
    };
    const data = await dynamoDb.scan(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(data.Items),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Failed to retrieve manuals" }),
    };
  }
};
```

### 4. API Gatewayの設定とフロントエンドとの統合

- 各エンドポイントごとにCORSを有効にし、`axios`を使ってフロントエンドからAPI Gatewayにリクエストを送信できるようにします。

### 5. コメント機能の追加

- `ManualDetail.js`内にコメントを入力するフォームを追加し、`POST /comments/{manualId}`エンドポイントを使用してコメントを保存。
- `CommentsTable`から該当の`manualId`に紐づくコメント一覧を取得し、投稿詳細ページに表示します。

### 6. 最終確認

- 各ページや機能が正しく動作するか確認します。
- エラー処理やセキュリティ（AWS IAMのアクセス制御やAPI Gatewayのリクエスト制限）も検討します。
---


プロンプト②
---
フロントエンドのコードを教えてください

ChatGPTからの回答①
---
以下に、フロントエンドで必要なReactコードの例を示します。このコードは各ページ（トップページ、投稿ページ、投稿確認ページ、検索結果一覧ページ、投稿詳細ページ）の実装を含んでおり、`axios`を使ってAPI Gatewayのエンドポイントにリクエストを送信します。

### 1. プロジェクトセットアップ

1. プロジェクトの初期化:
   ```bash
   npx create-react-app manual-share
   cd manual-share
   npm install axios
   ```

2. ディレクトリ構成:
   ```
   src/
   ├── components/
   │   ├── Home.js
   │   ├── CreateManual.js
   │   ├── ConfirmManual.js
   │   ├── SearchResults.js
   │   └── ManualDetail.js
   └── App.js
   ```

### 2. トップページ (`Home.js`)

`Home.js`では、DynamoDBからマニュアルのリストを取得し、閲覧ビューが多い順に表示します。

```javascript
import React, { useEffect, useState } from "react";
import axios from "axios";

const Home = () => {
  const [manuals, setManuals] = useState([]);

  useEffect(() => {
    axios.get("https://your-api-endpoint/manuals")
      .then(response => setManuals(response.data))
      .catch(error => console.error("Error fetching manuals:", error));
  }, []);

  return (
    <div>
      <h1>トップページ</h1>
      {manuals.map(manual => (
        <div key={manual.manualId}>
          <h2>{manual.title}</h2>
          <p>{manual.content.substring(0, 100)}...</p>
          <button onClick={() => window.location.href = `/manual/${manual.manualId}`}>
            詳細を見る
          </button>
        </div>
      ))}
      <button onClick={() => window.location.href = "/create"}>マニュアルを投稿する</button>
    </div>
  );
};

export default Home;
```

### 3. 投稿ページ (`CreateManual.js`)

`CreateManual.js`では、タイトル、内容、カテゴリ、画像などの入力を行い、確認ページにデータを送信します。

```javascript
import React, { useState } from "react";
import { useHistory } from "react-router-dom";

const CreateManual = () => {
  const history = useHistory();
  const [manualData, setManualData] = useState({
    title: "",
    content: "",
    category: "",
    image: null
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setManualData(prevData => ({ ...prevData, [name]: value }));
  };

  const handleFileChange = (e) => {
    setManualData(prevData => ({ ...prevData, image: e.target.files[0] }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    history.push("/confirm", { manualData });
  };

  return (
    <div>
      <h1>マニュアルの投稿</h1>
      <form onSubmit={handleSubmit}>
        <input type="text" name="title" placeholder="タイトル" onChange={handleChange} />
        <textarea name="content" placeholder="内容" onChange={handleChange}></textarea>
        <input type="text" name="category" placeholder="カテゴリ" onChange={handleChange} />
        <input type="file" name="image" onChange={handleFileChange} />
        <button type="submit">確認</button>
      </form>
    </div>
  );
};

export default CreateManual;
```

### 4. 投稿確認ページ (`ConfirmManual.js`)

`ConfirmManual.js`では、入力した内容を表示し、確認して送信します。

```javascript
import React from "react";
import axios from "axios";
import { useHistory, useLocation } from "react-router-dom";

const ConfirmManual = () => {
  const history = useHistory();
  const { state } = useLocation();
  const { manualData } = state;

  const handleConfirm = () => {
    const formData = new FormData();
    Object.keys(manualData).forEach(key => {
      formData.append(key, manualData[key]);
    });

    axios.post("https://your-api-endpoint/manuals", formData)
      .then(() => history.push("/"))
      .catch(error => console.error("Error posting manual:", error));
  };

  return (
    <div>
      <h1>確認ページ</h1>
      <h2>{manualData.title}</h2>
      <p>{manualData.content}</p>
      <p>カテゴリ: {manualData.category}</p>
      <button onClick={() => history.goBack()}>修正</button>
      <button onClick={handleConfirm}>確定</button>
    </div>
  );
};

export default ConfirmManual;
```

### 5. 検索結果一覧ページ (`SearchResults.js`)

`SearchResults.js`では、検索キーワードやカテゴリに基づき結果を表示します。

```javascript
import React, { useEffect, useState } from "react";
import axios from "axios";

const SearchResults = ({ searchQuery }) => {
  const [results, setResults] = useState([]);

  useEffect(() => {
    axios.get(`https://your-api-endpoint/manuals?query=${searchQuery}`)
      .then(response => setResults(response.data))
      .catch(error => console.error("Error fetching search results:", error));
  }, [searchQuery]);

  return (
    <div>
      <h1>検索結果</h1>
      {results.map(result => (
        <div key={result.manualId}>
          <h2>{result.title}</h2>
          <p>{result.content.substring(0, 100)}...</p>
          <button onClick={() => window.location.href = `/manual/${result.manualId}`}>
            詳細を見る
          </button>
        </div>
      ))}
    </div>
  );
};

export default SearchResults;
```

### 6. 投稿詳細ページ (`ManualDetail.js`)

`ManualDetail.js`では、マニュアルの詳細を表示し、コメントを追加します。

```javascript
import React, { useEffect, useState } from "react";
import axios from "axios";

const ManualDetail = ({ match }) => {
  const [manual, setManual] = useState(null);
  const [comment, setComment] = useState("");
  const manualId = match.params.id;

  useEffect(() => {
    axios.get(`https://your-api-endpoint/manuals/${manualId}`)
      .then(response => setManual(response.data))
      .catch(error => console.error("Error fetching manual:", error));
  }, [manualId]);

  const handleCommentSubmit = () => {
    axios.post(`https://your-api-endpoint/comments/${manualId}`, { comment })
      .then(() => setComment(""))
      .catch(error => console.error("Error posting comment:", error));
  };

  return (
    <div>
      <h1>マニュアルの詳細</h1>
      {manual && (
        <>
          <h2>{manual.title}</h2>
          <p>{manual.content}</p>
          <p>閲覧数: {manual.viewCount}</p>
        </>
      )}
      <div>
        <h3>コメント</h3>
        <input type="text" value={comment} onChange={(e) => setComment(e.target.value)} />
        <button onClick={handleCommentSubmit}>コメントを投稿</button>
      </div>
    </div>
  );
};

export default ManualDetail;
```

### 7. ルーティング設定 (`App.js`)

```javascript
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import CreateManual from "./components/CreateManual";
import ConfirmManual from "./components/ConfirmManual";
import SearchResults from "./components/SearchResults";
import ManualDetail from "./components/ManualDetail";

function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/create" component={CreateManual} />
        <Route path="/confirm" component={ConfirmManual} />
        <Route path="/search" component={SearchResults} />
        <Route path="/manual/:id" component={ManualDetail} />
      </Switch>
    </Router>
  );
}

export default App;
```

このコードを基に、各エンドポイントやUIの調整を行ってください。

---
わずか2ターンで実装の手順と基本コードを教えてもらいました。
また、実装中に詰まった箇所やエラーメッセージを送信すると、解決策を提案してくれるほか、CSSなどのデザインも作成してくれます。
例えば、

プロンプト例
---

以下のページをAirbnbみたいなデザインにして欲しい
```
import React, { useEffect, useState } from "react";
import axios from "axios";

const Home = () => {
  const [manuals, setManuals] = useState([]);

  useEffect(() => {
    axios.get("https://your-api-endpoint/manuals")
      .then(response => setManuals(response.data))
      .catch(error => console.error("Error fetching manuals:", error));
  }, []);

  return (
    <div>
      <h1>トップページ</h1>
      {manuals.map(manual => (
        <div key={manual.manualId}>
          <h2>{manual.title}</h2>
          <p>{manual.content.substring(0, 100)}...</p>
          <button onClick={() => window.location.href = `/manual/${manual.manualId}`}>
            詳細を見る
          </button>
        </div>
      ))}
      <button onClick={() => window.location.href = "/create"}>マニュアルを投稿する</button>
    </div>
  );
};

export default Home;
```

ChatGPTからの回答
---
Airbnbのようなスタイリッシュで視覚的に魅力的なデザインにするためには、シンプルで柔らかい色使いと余白を活かしたレイアウトを取り入れることが重要です。以下は、そのようなデザインに沿った修正コード例です。

```javascript
import React, { useEffect, useState } from "react";
import axios from "axios";
import "./Home.css"; // CSSファイルを別途作成してスタイルを管理

const Home = () => {
  const [manuals, setManuals] = useState([]);

  useEffect(() => {
    axios.get("https://your-api-endpoint/manuals")
      .then(response => setManuals(response.data))
      .catch(error => console.error("Error fetching manuals:", error));
  }, []);

  return (
    <div className="home-container">
      <header className="home-header">
        <h1 className="home-title">マニュアル一覧</h1>
        <button className="post-button" onClick={() => window.location.href = "/create"}>
          マニュアルを投稿する
        </button>
      </header>

      <div className="manuals-list">
        {manuals.map(manual => (
          <div key={manual.manualId} className="manual-card">
            <h2 className="manual-title">{manual.title}</h2>
            <p className="manual-content">{manual.content.substring(0, 100)}...</p>
            <button
              className="view-button"
              onClick={() => window.location.href = `/manual/${manual.manualId}`}
            >
              詳細を見る
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default Home;
```

次に、CSSを追加してAirbnb風のデザインに仕上げます。

```css
/* Home.css */

body {
  font-family: Arial, sans-serif;
  background-color: #f7f7f7;
  margin: 0;
  padding: 0;
}

.home-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.home-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.home-title {
  font-size: 2em;
  color: #333;
  margin: 0;
}

.post-button {
  background-color: #ff5a5f;
  color: white;
  border: none;
  padding: 10px 20px;
  font-size: 1em;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.post-button:hover {
  background-color: #e94b50;
}

.manuals-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}

.manual-card {
  background-color: white;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  transition: transform 0.2s;
}

.manual-card:hover {
  transform: translateY(-5px);
}

.manual-title {
  font-size: 1.5em;
  color: #333;
  margin: 0;
}

.manual-content {
  font-size: 0.9em;
  color: #666;
  margin: 10px 0;
}

.view-button {
  background-color: #007a87;
  color: white;
  border: none;
  padding: 10px 15px;
  font-size: 0.9em;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.view-button:hover {
  background-color: #006169;
}
```

### ポイント解説
- **全体的なレイアウト**: Airbnbのようにシンプルで洗練された印象にするため、カードスタイルを採用し、リストをグリッド表示。
- **ボタンスタイル**: 投稿ボタンと詳細ボタンに異なる背景色を設定し、ホバーエフェクトで視覚的な反応を追加。
- **カードの影とアニメーション**: 軽いボックスシャドウとホバー時のアニメーションでインタラクティブな要素を追加。

---

このようにイメージを伝えると、CSSも作成してくれます。
そんなこんなで、育休中の子どもが寝ている間の3日間ほどで形になるものを作成できました。ぜひアクセスして、さまざまなフィードバックをいただけると嬉しいです。

https://manualshare.net