Picasso Docs
Developers

バックエンド接続手順書


概要

このドキュメントでは、フロントエンドをバックエンドAPIに接続するための作業手順を記載します。
現在、フロントエンドはMSW(Mock Service Worker)を使用したモック環境で動作しています。
段階的にバックエンドAPIに切り替えていくことで、安全かつ確実に統合を進めます。


フェーズ1: 環境準備

1.1. バックエンドの起動確認

  • バックエンドサーバーが起動していることを確認
  • 起動ポートの確認(例: http://localhost:3000
  • APIエンドポイントが正しく実装されているか確認

1.2. 環境変数の設定

フロントエンドのルートディレクトリに .env.development ファイルを作成します。

# .env.development
VITE_API_BASE_URL=http://localhost:3000
VITE_USE_MOCK=true  # MSWを使用する場合はtrue、バックエンド接続する場合はfalse

本番環境用の .env.production も作成します。

# .env.production
VITE_API_BASE_URL=
VITE_USE_MOCK=false

[!NOTE] 本番環境では、フロントエンドとバックエンドが同じオリジンで配信されるため、VITE_API_BASE_URLは空文字列にします。

1.3. CORS設定の確認

バックエンド側でCORSが適切に設定されているか確認します。

  • 開発環境: http://localhost:5173 からのリクエストを許可
  • 本番環境: 同一オリジンのため不要

Express.jsでのCORS設定例:

import cors from 'cors';

const app = express();

if (process.env.NODE_ENV === 'development') {
  app.use(cors({
    origin: 'http://localhost:5173',
    credentials: true
  }));
}

フェーズ2: APIクライアントの修正

2.1. BASE_URLの環境変数化

src/api/client.tsBASE_URL を環境変数ベースに変更します。

// src/api/client.ts

// 開発時はバックエンドURL、本番時は相対パス
const BASE_URL = import.meta.env.VITE_API_BASE_URL || "";

2.2. エラーハンドリングの強化

ネットワークエラーやタイムアウトに対応します。

async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
  try {
    const res = await fetch(url, {
      ...options,
      headers: {
        "Content-Type": "application/json",
        ...options?.headers,
      },
    });

    if (!res.ok) {
      const errorData = await res.json().catch(() => ({}));
      throw new Error(
        `API Error: ${res.status} ${res.statusText} - ${JSON.stringify(errorData)}`
      );
    }

    if (res.status === 204) {
      return {} as T;
    }

    return res.json();
  } catch (error) {
    if (error instanceof TypeError) {
      throw new Error('ネットワークエラー: バックエンドに接続できません');
    }
    throw error;
  }
}

フェーズ3: 段階的な切り替え

3.1. MSWの条件付き無効化

src/main.tsx を修正して、環境変数でMSWのON/OFFを切り替え可能にします。

// src/main.tsx

async function enableMocking() {
  // 環境変数でMSWを制御
  if (import.meta.env.VITE_USE_MOCK === 'true') {
    const { worker } = await import('./mocks/browser');
    return worker.start({
      onUnhandledRequest: 'bypass',
    });
  }
}

3.2. API単位でのテスト

以下の順序で1つずつバックエンドに接続してテストします。

テスト順序

  1. GET /games/index.json - 過去ゲーム一覧取得
  2. POST /games - 新規ゲーム作成
  3. GET /games/:gameId/state.json - ゲーム状態取得
  4. POST /games/:gameId/turn/advance - ターン進行
  5. POST /games/:gameId/capture - カメラ撮影
  6. POST /games/:gameId/ai/analyze - ユーザ画像解析
  7. POST /games/:gameId/ai/next - AI単語生成
  8. POST /games/:gameId/ai/plot - プロッタ描画開始
  9. GET /games/:gameId/ai/plot/:jobId - 描画ステータス取得
  10. POST /games/:gameId/ai/hint - ヒント生成
  11. POST /games/:gameId/end - ゲーム強制終了

テスト方法

各APIをテストする際は、以下の手順を実施します:

  1. ブラウザのDevToolsを開く(Network タブ)
  2. 該当する操作を実行
  3. リクエスト/レスポンスの内容を確認
  4. エラーがないか確認
  5. UIが正しく更新されるか確認

3.3. 画像パスの調整

バックエンドが静的ファイルを配信する場合、画像パスを確認します。

現在の実装:

// history entry の image フィールドは相対パス(例: "turn-1.png")

画像表示時のURL構築:

// GameHistoryEntry.tsx などで使用
const imageUrl = entry.image 
  ? `${import.meta.env.VITE_API_BASE_URL || ''}/games/${gameId}/${entry.image}`
  : null;

[!IMPORTANT] バックエンドで games/ ディレクトリを静的配信する設定が必要です。詳細は API仕様書 を参照してください。

Express.jsでの静的配信設定例

import express from 'express';
import path from 'path';

const app = express();

// games/ ディレクトリを静的配信
// /games/20251007-192416/state.json のようなパスでアクセス可能になる
app.use('/games', express.static(path.join(__dirname, '../games')));

// オプション: キャッシュ制御を追加
app.use('/games', express.static(path.join(__dirname, '../games'), {
  maxAge: '1d', // 画像ファイルは1日キャッシュ
  etag: true,
  lastModified: true
}));

Fastifyでの静的配信設定例

import Fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import path from 'path';

const fastify = Fastify();

// @fastify/static プラグインを登録
await fastify.register(fastifyStatic, {
  root: path.join(__dirname, '../games'),
  prefix: '/games/', // URLプレフィックス
  constraints: {} // オプション
});

await fastify.listen({ port: 3000 });

Python Flask での静的配信設定例

from flask import Flask, send_from_directory
import os

app = Flask(__name__)

GAMES_DIR = os.path.join(os.path.dirname(__file__), '../games')

@app.route('/games/<path:filepath>')
def serve_games(filepath):
    """games/ ディレクトリの静的ファイルを配信"""
    return send_from_directory(GAMES_DIR, filepath)

# または、静的フォルダとして登録
# app = Flask(__name__, static_folder='../games', static_url_path='/games')

Python FastAPI での静的配信設定例

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import os

app = FastAPI()

GAMES_DIR = os.path.join(os.path.dirname(__file__), '../games')

# games/ ディレクトリを静的ファイルとしてマウント
app.mount("/games", StaticFiles(directory=GAMES_DIR), name="games")

[!NOTE] 静的配信を設定することで、フロントエンドは以下のようなパスで直接ファイルにアクセスできます:

  • http://localhost:3000/games/index.json - ゲーム一覧
  • http://localhost:3000/games/20251007-192416/state.json - ゲーム状態
  • http://localhost:3000/games/20251007-192416/turn-1.png - 画像ファイル
  • http://localhost:3000/games/20251007-192416/svg/turn-2.svg - SVGファイル

フェーズ4: エラーハンドリングとUX改善

4.1. ローディング状態の管理

各API呼び出しでローディング状態を適切に管理します。

  • ユーザが操作できない間は、ボタンを無効化
  • ローディングインジケーターを表示
  • タイムアウト時のエラーメッセージ

4.2. リトライロジック

プロッタ描画のポーリング処理でリトライロジックを実装します。

// useAiSequence.ts などで実装
const pollPlotJob = async (jobId: string, maxRetries = 60) => {
  for (let i = 0; i < maxRetries; i++) {
    const jobStatus = await api.getPlotJob(gameId, jobId);
    
    if (jobStatus.status === 'done') {
      return;
    }
    
    if (jobStatus.status === 'error') {
      throw new Error('プロッタ描画に失敗しました');
    }
    
    // 1秒待機
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  
  throw new Error('プロッタ描画がタイムアウトしました');
};

4.3. エラーメッセージの改善

src/constants.ts にエラーメッセージを追加します。

export const ERROR_MESSAGES = {
  NETWORK_ERROR: 'ネットワークエラーが発生しました',
  BACKEND_CONNECTION_FAILED: 'バックエンドに接続できません',
  TIMEOUT: 'タイムアウトしました',
  // ... 既存のメッセージ
};

フェーズ5: 検証とデバッグ

5.1. ブラウザDevToolsでのチェック

Networkタブ

  • リクエストURLが正しいか
  • リクエストヘッダーが正しいか
  • レスポンスステータスコードが期待通りか
  • レスポンスボディが正しいか

Consoleタブ

  • エラーが出ていないか
  • 警告が出ていないか

5.2. エンドツーエンドテスト

以下のシナリオでフルテストを実施します:

シナリオ1: ユーザが先手

  1. ホーム画面で「ゲームスタート」をクリック
  2. 「せんて」を選択
  3. 絵を描く動作をシミュレート
  4. 「かけた」ボタンをクリック
  5. 「OK」ボタンをクリック
  6. AI が単語を生成し、プロッタで描画
  7. カメラで撮影
  8. 次のターンへ
  9. ゲーム終了まで繰り返し

シナリオ2: AIが先手

  1. ホーム画面で「ゲームスタート」をクリック
  2. 「こうて」を選択
  3. AI が単語を生成し、プロッタで描画
  4. カメラで撮影
  5. ユーザのターン
  6. ゲーム終了まで繰り返し

シナリオ3: ヒント機能

  1. ユーザのターンで「ヒント」ボタンをクリック
  2. ヒントが表示されることを確認

シナリオ4: ゲーム途中終了

  1. ゲーム中に「おわる」ボタンをクリック
  2. 確認ダイアログで「OK」をクリック
  3. ホーム画面に戻ることを確認
  4. 履歴に引き分けとして記録されることを確認

5.3. エッジケースの確認

  • 「ん」で終わる単語での終了
  • ネットワークエラー時の挙動
  • プロッタ描画中の中断
  • 画像が取得できない場合の挙動
  • 同時リクエストの処理

フェーズ6: 本番環境への展開

6.1. ビルド確認

npm run build
  • ビルドエラーがないか
  • dist/ ディレクトリが正しく生成されているか

6.2. プレビュー確認

npm run preview
  • プレビューサーバーが起動するか
  • 本番ビルドで動作するか

6.3. Raspberry Piでの動作確認

  1. Raspberry Piにコードをデプロイ
  2. バックエンドとフロントエンドを起動
  3. 実際のハードウェア(カメラ、プロッタ)を接続
  4. エンドツーエンドテストを実施

トラブルシューティング

CORSエラーが発生する

症状:

Access to fetch at 'http://localhost:3000/games' from origin 'http://localhost:5173' 
has been blocked by CORS policy

対処法:

  • バックエンドでCORS設定を確認
  • 開発環境で http://localhost:5173 を許可

画像が表示されない

症状:

  • 画像のURLが404エラーになる

対処法:

  • バックエンドで静的ファイル配信が設定されているか確認
  • 画像パスが正しいか確認(相対パス vs 絶対パス)

API レスポンスが遅い

症状:

  • ローディングが長時間続く

対処法:

  • バックエンドのログを確認
  • ネットワークタブでレスポンス時間を確認
  • タイムアウト設定を調整

プロッタ描画が完了しない

症状:

  • ポーリングがずっと続く

対処法:

  • ジョブステータスのログを確認
  • ハードウェア接続を確認
  • タイムアウト設定を確認

チェックリスト

準備

  • バックエンドサーバーの起動確認
  • .env.development ファイル作成
  • .env.production ファイル作成
  • CORSの設定確認

コード修正

  • BASE_URL を環境変数ベースに変更
  • MSWの条件付き無効化実装
  • エラーハンドリングの強化
  • 画像パスの調整

テスト

  • 開発環境でMSW動作確認
  • バックエンド接続テスト(全APIエンドポイント)
  • 画像の表示確認
  • エンドツーエンドテスト(シナリオ1〜4)
  • エッジケースの確認

本番準備

  • 本番用ビルド確認
  • プレビュー確認
  • 静的ファイル配信の確認
  • Raspberry Pi での動作確認
  • ハードウェア連携確認(カメラ、プロッタ)

参考資料