テクノロジー / 生成AI

【Gemini Code Assist】VS Code + React でフロントエンドアプリケーション作成(2)| デザイン修正とREST APIからのデータ取得

2025.06.06

前回の記事(前回記事のURL)では、Vite + React で作成したアプリケーションに、カート情報を表示する簡単な画面の作成を行いました。

当記事では、前回作成したアプリケーションを用いて、表示する項目やデザインの修正し、実際に REST API を通じて取得したデータを React アプリケーション上に表示するまで、全て Gemini Code Assist でコードを生成してみます。

デザイン修正

JSON データの指定

データを外部から取得する前に、画面の項目とデザインを、実際のデータに合わせて修正します。 REST API から取得する想定の JSON データの型をプロンプトで送信し、実際に表示する項目を指定します。

今回は、以下の内容でプロンプトを送信しました。

カートのデータのJSONは、以下で示す形式としてください。

{
"userId": "USER789",
"senderName": "山田 太郎",
"recipientName": "佐藤 花子",
"postalCode": "100-0005",
"recipientAddressPrefecture": "東京都",
"recipientAddressCity": "千代田区",
"recipientAddressLine1": "丸の内1-2-3",
"recipientAddressLine2": "パシフィックセンチュリープレイス丸の内 20F",
"items": [
{
"productId": "ITEM001",
"productName": "美味しいリンゴ",
"priceWithTax": 108,
"quantity": 3,
"totalAmount": 324,
"productPageUrl": "https://example.com/products/ITEM001"
},
{
"productId": "ITEM002",
"productName": "高級バナナセット",
"priceWithTax": 540,
"quantity": 1,
"totalAmount": 540,
"productPageUrl": "https://example.com/products/ITEM002"
},
{
"productId": "ITEM003",
"productName": "旬のいちごパック",
"priceWithTax": 756,
"quantity": 2,
"totalAmount": 1512,
"productPageUrl": "https://example.com/products/ITEM003"
}
]
}

userId、senderName は発送者(ユーザー)情報、items はカート内の商品情報、その他の情報は配送先情報です。
項目名については、recipientAddressLine1 は「番地等」、recipientAddressLine2 は「建物名」としてください。

カートのデータはREST APIを利用し、ユーザーIDをキーとして送信して1つだけ返される想定です。まずデザインを確認したいので、まだ通信は行わずダミーデータとして設定してください。

提案された内容を反映して、デザインを確認してみます。


デザイン面では気になる点がありますが、表示する情報は想定どおりの構成となりました。また、住所関連の項目が、個別ではなく都道府県~番地等まで連結して表示されていましたが、これはこのほうが自然でしたので、このまま採用しました。

デザイン修正

表示する項目が揃ったら、細かいデザイン修正を行います。今回は以下のような点を修正するように指示しました。

・「カート情報アプリ」のタイトル部分を削除
・この画面の表示時のtitle を「カート情報」に変更
・カート情報の表示欄全体を中央寄せに変更
・発送者情報、配送先情報の中の項目は左寄せに変更
・商品一覧の数値の項目(単価(税込)、数量、小計(税込))の値を右寄せに変更


デザインの細かい箇所は、なかなか一回でイメージ通りとはならないケースがありますので、詳細を追加で送信するなどして、繰り返し微調整すると良いと思います。

データの取得先アプリケーションの準備

今回利用するアプリケーション

React アプリケーションからデータ取得のために接続するアプリケーション(REST API)は、以前の記事で作成した Spring Boot アプリケーションを利用します。

【Gemini Code Assist】REST APIを作成する | VS Code + Gemini Code Assist で Spring Boot アプリケーション(1)

外部のアプリケーション(React アプリケーション)から REST API を利用する場合、クロスオリジンへの対応を追加する必要があります。

クロスオリジンとは

REST APIを利用してReactアプリケーション内にデータを取り込む際、取得先が React アプリケーションとは別のサーバーとなる場合があります。

例えば今回のケースでは、データを取得するため Spring Boot アプリケーションは「http://localhost:8080」、React アプリケーションは「http://localhost:5173」のサーバーで動作しています。

このため、Webブラウザ上では「http://localhost:5173」のコンテンツの中に、「http://localhost:8080」という別のオリジン(出自)から取得した情報を埋め込もうとすることになります。


このように、リソースにアクセスしようとしているウェブサイトと、リソースを提供しているサーバーのオリジンが異なることをクロスオリジンと呼びます。

クロスオリジンの許可設定

一般的にWebブラウザでは、セキュリティ機能で異なるオリジン間のリソースアクセスをデフォルトで制限しています。これは「同一オリジンポリシー」と呼ばれます。

制限された状態では、JavaScript で外部から取得したデータを操作しようとした際にエラーとなります。


同一オリジンポリシーの制約を安全に緩和し、異なるオリジン間でのリソース共有を可能にするための仕組みは CORS(Cross-Origin Resource Sharing)と呼ばれ、API を提供するサーバー側での設定が必要になります。

Spring Boot アプリケーションからデータを提供する際に、CORS の許可設定を行う方法はいくつかありますが、最も簡単なのは CrossOrigin アノテーションを利用して、レスポンスに「Access-Control-Allow-Origin」ヘッダーを付加することです。

 - 参考:Enabling Cross Origin Requests for a RESTful Web Service(Spring公式サイト・英語)

手動で設定することできますが、今回はこれも Gemini Code Assist を利用して設定してしまいましょう。React アプリケーションとは別のウインドウ(VS Code)で、Spring Boot アプリケーションのフォルダを開き、Gemini Code Assist のチャットスペースで指示します。

@DemoController.java
getUserCart メソッドに、外部のフロントエンドアプリケーションからのアクセスを許可するようにしてください。
アクセス元のURLは「http://localhost:5173」です。

getUserCart メソッドに CrossOrigin アノテーションが追加されていました。これを保存して、データ取得先である REST API の準備は完了です。

React アプリケーションからのデータ取得

それでは Gemini Code Assist を利用して、実際に外部データを取得する処理を実装してみましょう。

REST API からのデータ取得

React アプリケーションで外部データを取得して利用するには、どのような対応が必要でしょうか。一般的な方法を簡単に説明すると、以下のような内容になります。

ブラウザに標準で組み込まれている JavaScript の fetch 関数を用いて、外部データを取得してアプリケーションで利用する場合、React アプリケーションでは通常 useEffect API(Hook)を使用します。

useEffect の中で fetch 関数を呼び出し、取得したデータをuseState API を利用してステート関数に格納することで、データが更新され、自動的に画面上の描画に反映させることができます。

では、具体的にはどのようなコードになるのでしょうか。本来であれば、ステート関数を準備し、useEffect を用いて処理を記述して、といったコーディングが必要となりますが、この処理も丸ごと Gemini Code Assist で生成してみます。

今回は、Gemini Code Assist のチャットスペースで、以下のように入力しました。

@CartInfoScreen.jsx
現在のダミーJSONデータから表示している、「表示」ボタン押下時に表示するカート情報を、「http://localhost:8080/cart/{userId} 」に通信して取得するように変更してください。
このとき、URLの {userId} には、画面で入力したユーザーIDの値を設定するようにしてください。

データの取得処理のコードとともに、データ取得中(ロード中)表示のためのスタイルなどがあわせて提案されました。変更内容をファイルに反映して保存します。

(jsx ファイルの変更内容)


(css ファイルの変更内容)

データの取得先を設定ファイルに保存

生成されたコードを確認すると、データの取得先のURLがハードコーディング(プログラム上に直接値を記述)されています。React アプリケーションのようなフロントエンドアプリケーションでは、テスト時や本番環境(実際の運用環境)によって、データ取得のための接続先が変わるため、これは望ましくありません。


接続先が変更になった際にプログラムコードを修正しなくても良いように、接続先の情報を設定ファイル上に保存して読み込むように変更します。今回はホスト(http://localhost:8080)の部分を設定ファイルに保持するようプロンプトで指示しました。

@CartInfoScreen.jsx
カート情報の取得先のURL「http://localhost:8080/cart/${userIdToFetch}」のうち、ホストの部分(http://localhost:8080)を、コード上ではなく設定ファイルに保持して読み込むようにしてください。


環境変数を管理する .env ファイルおよび設定内容と、データ取得処理のコードの修正内容が提案されました。これらの内容をソースに反映します。
(Git でソース管理する際の除外設定も提案されましたが、今回は適用しなくても構いません)

(.envファイルの作成内容)


(jsx ファイルの変更内容)

外部からのデータ取得の実行

これで外部の REST API からデータを取得する準備ができました。実際に動かして確認してみましょう。

まず、Spring Boot アプリケーション(REST API)を起動します。


次に、ターミナルで「npm run dev」を実行し、React アプリケーションを起動します。


Webブラウザから http://localhost:5173 にアクセスし、ユーザーIDを入力して表示ボタンをクリックします。


一瞬でしたが、データ取得中の表示も確認できました。


Spring Boot アプリケーションから渡されたデータが、React アプリケーション上に表示されました。Spring Boot アプリケーションで引き渡すデータを編集して再度表示すれば、React アプリケーションに内容が反映されるようになります。


今回作成した React アプリケーションでは、全てのコードを Gemini Code Assist で生成して、外部データの取得、表示まで実現することができました。

以前の記事(【Gemini Code Assist】REST APIを作成する)では「フロントエンド向けにテスト用APIを簡単に準備する、などといったこともできそうです」と述べましたが、その逆もまた然りで、 REST API のようなバックエンド(サーバーサイド)アプリケーションの開発時に、このように簡単なフロントエンドアプリケーションを作成してテストする、といった使い道もありそうです。

(参考)Gemini Code Assist によるコード

一連の修正によって、最終的に Gemini Code Assist によって生成されたコードは以下の通りです。(Gemini Code Assist によって作成または修正されたファイルのみ)

src/App.jsx

import React from 'react';
import CartInfoScreen from './components/CartInfoScreen';
import './App.css';

function App() {
  return (
    <div className="App">
      <main>
        <CartInfoScreen />
      </main>
    </div>
  );
}

export default App


src/App.css

body {
  margin: 0;
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; /* index.cssから移動または統一 */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #e0f0ff; /* アプリケーション全体の背景を薄い青系に */
  color: #333; /* アプリケーション全体のデフォルト文字色 */
}

.App {
  width: 100%; /* 必要に応じて幅を指定 */
}


src/index.css

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  /* color-scheme: light dark; */ /* App.cssで制御するためコメントアウトまたは削除 */
  /* color: rgba(255, 255, 255, 0.87); */ /* App.cssで制御するためコメントアウトまたは削除 */
  /* background-color: #242424; */ /* App.cssで制御するためコメントアウトまたは削除 */

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
body {
  margin: 0;
  /* display: flex; */ /* div#root を中央寄せする原因となるため削除 */
  /* place-items: center; */ /* div#root を中央寄せする原因となるため削除 */
  min-width: 320px;
  min-height: 100vh;
  /* bodyの背景色と文字色はApp.cssで指定したものを優先 */
}

#root {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center; /* .App を左右中央に配置 */
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}

button:hover {
  border-color: #646cff;
}

button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}


src/components/CartInfoScreen.jsx

import React, { useState, useEffect } from 'react';
import './CartInfoScreen.css';

function CartInfoScreen() {
  const [userId, setUserId] = useState('');
  const [cartInfo, setCartInfo] = useState(null);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const originalTitle = document.title;
    document.title = 'カート情報';
    return () => {
      document.title = originalTitle; // もしくは固定のデフォルトタイトル 'Vite + React' など
    };
  }, []);

  const fetchCartData = async (userIdToFetch) => {
    const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; // デフォルト値を設定
    const apiUrl = `${apiBaseUrl}/cart/${userIdToFetch}`;
    try {
      const response = await fetch(apiUrl);
      if (!response.ok) {
        if (response.status === 404) {
          return null; // ユーザーが見つからない場合はnullを返す
        }
        const errorData = await response.json().catch(() => ({ message: `サーバーエラーが発生しました (ステータス: ${response.status})` }));
        throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      return data;
    } catch (e) {
      console.error("カート情報の取得に失敗しました:", e);
      throw e;
    }
  };

  const handleDisplayCart = async () => {
    setError('');
    setCartInfo(null);
    setLoading(true);

    if (!userId.trim()) {
      setError('ユーザーIDを入力してください。');
      setLoading(false);
      return;
    }

    try {
      const data = await fetchCartData(userId.toUpperCase());
      if (data) {
        const cartTotal = data.items.reduce((sum, item) => sum + item.totalAmount, 0);
        setCartInfo({ ...data, cartTotalAmount: cartTotal });
      } else {
        setError('該当するユーザーのカート情報は見つかりませんでした。');
      }
    } catch (e) {
      setError(e.message || 'カート情報の取得中にエラーが発生しました。');
    } finally {
      setLoading(false);
    }
  };

  const handleClear = () => {
    setUserId('');
    setCartInfo(null);
    setError('');
    setLoading(false);
  };

  return (
    <div className="cart-info-screen">
      <h2>カート情報</h2>
      <div className="input-area">
        <label htmlFor="userIdInput">ユーザーID:</label>
        <input
          type="text"
          id="userIdInput"
          value={userId}
          onChange={(e) => setUserId(e.target.value)}
          placeholder="ユーザーIDを入力"
        />
        <button onClick={handleDisplayCart} className="action-button">
          表示
        </button>
        <button onClick={handleClear} className="clear-button">
          クリア
        </button>
      </div>

      {error && <p className="error-message">{error}</p>}

      {loading && <p className="loading-message">カート情報を取得中...</p>}
      {cartInfo && (
        <div className="cart-display-area">
          <h3>カート内容 (ユーザーID: {cartInfo.userId})</h3>

          <div className="info-section sender-info">
            <h4>発送者情報</h4>
            <p><strong>氏名:</strong> {cartInfo.senderName}</p>
          </div>

          <div className="info-section recipient-info">
            <h4>配送先情報</h4>
            <p><strong>氏名:</strong> {cartInfo.recipientName}</p>
            <p><strong>郵便番号:</strong> {cartInfo.postalCode}</p>
            <p><strong>住所:</strong> {cartInfo.recipientAddressPrefecture}{cartInfo.recipientAddressCity}{cartInfo.recipientAddressLine1}</p>
            {cartInfo.recipientAddressLine2 && <p><strong>建物名:</strong> {cartInfo.recipientAddressLine2}</p>}
          </div>

          {cartInfo.items.length > 0 ? (
            <>
              <h4>商品情報</h4>
              <table>
                <thead>
                  <tr>
                    <th>商品名</th>
                    <th>単価(税込)</th>
                    <th>数量</th>
                    <th>小計(税込)</th>
                    <th>商品ページ</th>
                  </tr>
                </thead>
                <tbody>
                  {cartInfo.items.map((item) => (
                    <tr key={item.productId}>
                      <td>{item.productName}</td>
                      <td>¥{item.priceWithTax.toLocaleString()}</td>
                      <td>{item.quantity}</td>
                      <td>¥{item.totalAmount.toLocaleString()}</td>
                      <td><a href={item.productPageUrl} target="_blank" rel="noopener noreferrer">表示</a></td>
                    </tr>
                  ))}
                </tbody>
                <tfoot>
                  <tr>
                    <td colSpan="3" style={{ textAlign: 'right' }}>
                      <strong>合計金額 (税込):</strong>
                    </td>
                    <td colSpan="2">
                      <strong>¥{cartInfo.cartTotalAmount.toLocaleString()}</strong>
                    </td>
                  </tr>
                </tfoot>
              </table>
            </>
          ) : (
            <p>カートに商品がありません。</p>
          )}
        </div>
      )}
    </div>
  );
}

export default CartInfoScreen;


src/components/CartInfoScreen.css

.cart-info-screen {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  padding: 25px;
  background-color: #f0f8ff; /* AliceBlue - 薄い青系 */
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  color: #333;
  max-width: 800px;
  margin: 20px auto;
}

.cart-info-screen h2 {
  color: #2c3e50; /* Dark Slate Gray - 落ち着いた色 */
  text-align: center;
  margin-bottom: 25px;
  font-size: 1.8em;
}

.input-area {
  display: flex;
  align-items: center;
  gap: 15px;
  margin-bottom: 25px;
  padding: 15px;
  background-color: #e6f3ff; /* さらに薄い青 */
  border-radius: 8px;
}

.input-area label {
  font-weight: 600;
  color: #34495e; /* Wet Asphalt - 落ち着いた色 */
}

.input-area input[type="text"] {
  padding: 10px 12px;
  border: 1px solid #b0c4de; /* LightSteelBlue */
  border-radius: 6px;
  font-size: 1em;
  flex-grow: 1;
}

.input-area input[type="text"]:focus {
  outline: none;
  border-color: #6fa8dc; /* CornflowerBlue - フォーカス時の色 */
  box-shadow: 0 0 0 2px rgba(111, 168, 220, 0.2);
}

.action-button,
.clear-button {
  padding: 10px 18px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 1em;
  transition: background-color 0.2s ease;
}

.action-button {
  background-color: #6fa8dc; /* CornflowerBlue */
  color: white;
}

.action-button:hover {
  background-color: #548bba; /* 少し濃い青 */
}

.clear-button {
  background-color: #d0e0f0; /* LightSteelBlue より少し濃い */
  color: #333;
}

.clear-button:hover {
  background-color: #b0c4de; /* LightSteelBlue */
}

.error-message {
  color: #c0392b; /* Pomegranate - エラー色 */
  background-color: #fdecea;
  padding: 10px;
  border-radius: 6px;
  margin-bottom: 20px;
  border: 1px solid #e74c3c;
}

.loading-message {
  color: #2980b9; /* Belize Hole - 青系 */
  font-style: italic;
  text-align: center;
  padding: 10px;
  margin-bottom: 20px;
}

.sender-info,
.recipient-info {
  background-color: #e6f3ff; /* 薄い青 */
  padding: 15px;
  margin-bottom: 20px;
  border-radius: 8px;
  border: 1px solid #d1e0ec;
  text-align: left;
}

.sender-info h4,
.recipient-info h4 {
  margin-top: 0;
  color: #34495e; /* Wet Asphalt */
  border-bottom: 1px solid #b0c4de;
  padding-bottom: 8px;
  margin-bottom: 10px;
}

.sender-info p,
.recipient-info p {
  margin: 5px 0;
  line-height: 1.6;
}

.cart-display-area {
  margin-top: 25px;
  padding: 20px;
  background-color: #ffffff;
  border: 1px solid #d1e0ec; /* 薄い青系の境界線 */
  border-radius: 8px;
}

.cart-display-area h3 {
  color: #2980b9; /* Belize Hole - 青系 */
  margin-top: 0;
  margin-bottom: 15px;
  border-bottom: 2px solid #e0f0ff;
  padding-bottom: 10px;
}

.cart-display-area h4 { /* 商品情報用のh4 */
  color: #2980b9; /* Belize Hole - 青系 */
  margin-top: 20px;
  margin-bottom: 10px;
  border-bottom: 2px solid #e0f0ff;
  padding-bottom: 8px;
  text-align: left;
}

.cart-display-area table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 15px;
}

.cart-display-area th,
.cart-display-area td {
  padding: 12px 15px;
  text-align: left;
  border-bottom: 1px solid #e0f0ff; /* 薄い青系の境界線 */
}

.cart-display-area th {
  background-color: #e6f3ff; /* さらに薄い青 */
  color: #34495e; /* Wet Asphalt */
  font-weight: 600;
}

.cart-display-area tbody td:nth-child(2), /* 単価(税込) */
.cart-display-area tbody td:nth-child(3), /* 数量 */
.cart-display-area tbody td:nth-child(4) { /* 小計(税込) */
  text-align: right;
}

.cart-display-area tr:last-child td {
  border-bottom: none;
}

.cart-display-area tr:hover {
  background-color: #f8fcff;
}

.cart-display-area tfoot td {
  font-weight: bold;
  color: #2c3e50;
  background-color: #e6f3ff;
}

.cart-display-area tfoot td:last-child { /* 合計金額の数値セル */
  text-align: right;
}


.env

VITE_API_BASE_URL=http://localhost:8080

まとめ

いかがでしたでしょうか?今回例示したプログラムコードは、全て Gemini Code Assist のチャットスペースで指示した結果で、人の手でのコーディングは一切していません。

また、チャットスペースではコードの生成だけでなく、生成したプログラムのポイントや概要についての説明も表示されるため、デザイン修正やデータの取得などの処理を段階的に作成し、その都度差分や説明を確認することで、プログラムの学習にもつながります。

もちろん、商用のアプリケーションの作成には、複雑な画面構成やセキュリティ対策などが必要になりますが、新しい言語やフレームワーク、プログラムの構成などを学ぶ際には非常に優れたツールであると感じます。

皆さんも、是非 Gemini Code Assist を利用したプログラミングに挑戦してみてください。

株式会社GSI 採用サイト

新しいこと、始めよう

あなたとともに歩を進めるWEBメディア

広告

広告