🤫

# Claude Code で業務書類ジェネレーターを作った話

に公開

〜 React + FastAPI + Claude API の実装で詰まった5つのポイント 〜

はじめに

「見積書を作るたびにWordを開いて、毎回同じような文章を書き直す」

これを中小企業の営業担当者から何度も聞いてきました。書類1枚に30分〜1時間かかることも珍しくありません。

エンジニアとしてこの課題を解決できないかと考え、質問に答えるだけでAIが業務書類を自動生成するWebアプリを作りました。

🔗 デモ: https://columbus-doc-generator-p7w5etuj6-columbus0370s-projects.vercel.app/
📦 GitHub: https://github.com/columbus0370/columbus-doc-generator


作ったもの

Columbus AI 書類ジェネレーター

ウィザード形式の質問(6〜8問)に答えるだけで、以下の書類をAIが自動生成してPDF出力できるWebアプリです。

  • 見積書(6ステップ)
  • 提案書(5ステップ)
  • 業務レポート(5ステップ)

主な機能

  • ウィザード形式のUI(選択・テキスト・明細入力)
  • Claude APIによる日本語HTML書類の自動生成
  • ブラウザ上でのWYSIWYG編集(太字・斜体・取り消し線など)
  • 事業者ロゴのアップロードと書類への自動埋め込み
  • 印刷 / PDF保存対応
  • 事業者情報のブラウザローカル保存

技術スタック

項目 技術
フロントエンド React 18 + Vite + Tailwind CSS
バックエンド Python / FastAPI
AI Anthropic Claude API
PDF生成 WeasyPrint(Linux)/ fpdf2(Windows)
フロントデプロイ Vercel
バックデプロイ Render.com
レート制限 slowapi

アーキテクチャ設計

[ブラウザ]
  ↓ フォーム送信
[React + Vite / Vercel]
  ↓ POST /api/generate
[FastAPI / Render.com]
  ↓ Claude API 呼び出し
[Anthropic Claude API]
  ↓ HTML書類を返す
[FastAPI]
  ↓ JSONで返却
[React]
  ↓ iframe でプレビュー表示
[ブラウザ]
  ↓ 印刷ダイアログ
[PDF保存]

バックエンドはFastAPIを選んだ理由は3つです。

  1. Pythonで書ける:Claude APIのSDKがPython製で相性が良い
  2. 高速:uvicornによる非同期処理でレスポンスが速い
  3. Pydanticでバリデーション:リクエストのバリデーションをモデルで一元管理できる

詰まったポイント5つ

① iframeのサンドボックス設定

生成されたHTMLをブラウザ上でプレビュー表示するためにiframeを使いました。

最初はsandbox="allow-scripts allow-same-origin"で実装していましたが、allow-same-originを許可すると、生成HTML内のスクリプトが親ページのDOMにアクセスできてしまうリスクがあることに気づきました。

対応:

// NG:allow-same-originがあると親DOMへのアクセスが可能になる
<iframe srcDoc={htmlString} sandbox="allow-scripts allow-same-origin" />

// OK:allow-scriptsのみに絞る
<iframe srcDoc={htmlString} sandbox="allow-scripts" />

ただし、WYSIWYG編集モードではdesignMode = 'on'を使うため、編集用iframeはやむなく別途CSP metaタグを動的挿入して対処しました。

// 編集モードiframe内でCSPを動的挿入して外部通信をブロック
const meta = doc.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'";
doc.head.insertBefore(meta, doc.head.firstChild);

② Claude APIのprompt cachingで60%コスト削減

書類生成のたびに長いシステムプロンプトを送信するため、APIコストがかさむことに気づきました。

Claude APIにはprompt cachingという機能があり、同じシステムプロンプトへの繰り返しリクエストをキャッシュしてコストを削減できます。

実装:

response = anthropic_client.messages.create(
    model="claude-opus-4-7",
    max_tokens=4096,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"}  # ← これを追加するだけ
        }
    ],
    messages=[{"role": "user", "content": user_prompt}]
)

cache_control: ephemeralを指定するだけで、同じシステムプロンプトは5分間キャッシュされます。繰り返し生成する場合の入力トークンコストが約60%削減できました。


③ WeasyPrintのSSRF対策

HTMLをPDF変換するためにWeasyPrintを使いましたが、デフォルト設定では生成HTML内に埋め込まれた外部URLへリクエストが飛ぶ可能性があります。

悪意あるユーザーが<img src="http://internal-server/">などを含むHTMLを送り込むと、サーバーから内部ネットワークへのリクエストが発生するSSRF(Server-Side Request Forgery)のリスクがあります。

対応:

def _blocked_url_fetcher(url, timeout=10):
    """外部URLへのアクセスをブロックするurl_fetcher"""
    parsed = urlparse(url)
    
    # data:とfile:のみ許可(埋め込みロゴなど)
    if parsed.scheme in ('data', 'file'):
        return default_url_fetcher(url, timeout=timeout)
    
    # それ以外は全てブロック
    raise ValueError(f"External URL blocked for security: {url}")

# WeasyPrintにカスタムfetcherを渡す
pdf_bytes = HTML(
    string=html_content,
    url_fetcher=_blocked_url_fetcher
).write_pdf()

data:スキームのみ許可することで、ロゴ画像(Base64)は埋め込みながら外部URL通信を完全にブロックできました。


④ ロゴ画像のBase64埋め込みとMagic bytes検証

事業者ロゴをアップロードして書類に自動埋め込みする機能を実装しました。

ファイル拡張子だけのチェックでは、.jpgに偽装したPHPスクリプトなどを受け付けてしまうリスクがあります。

対応:Magic bytes検証

// ファイルの先頭バイトで実際のフォーマットを判定
async function validateImageMagicBytes(file) {
  const buffer = await file.arrayBuffer();
  const bytes = new Uint8Array(buffer);
  
  const signatures = {
    jpeg: [0xFF, 0xD8, 0xFF],
    png:  [0x89, 0x50, 0x4E, 0x47],
    gif:  [0x47, 0x49, 0x46, 0x38],
  };
  
  for (const [type, sig] of Object.entries(signatures)) {
    if (sig.every((byte, i) => bytes[i] === byte)) {
      return type; // 正規のフォーマット
    }
  }
  
  throw new Error('JPEG/PNG/GIF以外のファイルは使用できません');
}

SVGはXSSリスクがあるため明示的にブロックし、JPEG/PNG/GIFのみ受け付けるようにしました。500KB以上のファイルも事前に弾いています。


⑤ Render.comのコールドスタート対策

Render.comの無料プランでは、15分間リクエストがないとインスタンスがスリープします。次のリクエスト時に30〜60秒かかるコールドスタートが発生します。

対応:ヘルスチェックエンドポイントの活用

@app.get("/health")
async def health_check():
    return {"status": "ok"}

フロントエンド側でアプリ起動時に/healthを叩いてバックエンドをウォームアップする実装を入れました。

// アプリ起動時にバックエンドをウォームアップ
useEffect(() => {
  fetch(`${import.meta.env.VITE_API_URL}/health`)
    .catch(() => {}); // エラーは無視(ウォームアップ目的のみ)
}, []);

完全な解決にはなりませんが、フォームを入力している間にバックエンドが起動するため、実用上の体感が改善しました。


セキュリティ対策まとめ

今回は営業資料として実際に使える品質を目指したため、セキュリティ対策を設計段階から組み込みました。

対策 実装内容
SSRF防止 WeasyPrintのurl_fetcherをカスタム実装
XSS防止 iframeサンドボックス + CSPヘッダー
レート制限 slowapi(生成10回/分、PDF変換20回/分)
入力バリデーション Pydantic Fieldで全フィールドに文字数上限
ファイル検証 Magic bytes検証でSVGブロック
エラー隠蔽 例外メッセージをレスポンスに含めず内部ログのみ
Swagger無効化 docs_url=None, redoc_url=None
プロンプトインジェクション対策 全プロンプト末尾にセキュリティ制約を付加
CORSの厳格化 ENV=productionかつCORS_ORIGINS未設定で起動拒否

Claude Codeを使った開発体験

今回はClaude Codeを使って開発しました。

特に効果的だったのは以下の3点です。

1. コンポーネント設計の壁打ち

WizardContainerの状態管理設計で迷ったとき、「こういう要件があるんだけど」と話しかけるだけで最適な設計案を複数提示してくれました。

2. セキュリティの網羅的なチェック

「このコード、セキュリティ上の問題点を列挙して」と聞くと、SSRF・XSS・ファイルアップロードの脆弱性など、見落としていた点を次々と指摘してくれました。

3. WeasyPrintのLinux/Windows差異の対応

WeasyPrintはLinux(Render.com)では動くがWindowsローカルでは動きにくい問題がありました。Claude Codeにfpdf2をWindowsフォールバックとして実装させることで解決しました。


今後の拡張予定

  • 追加テンプレート(発注書・納品書・請求書・契約書)
  • Redisによるレート制限の分散対応
  • DOMPurifyによる生成HTML出力のサニタイズ
  • APIキー認証による不正利用防止
  • ロゴ画像のサーバーサイドストレージ(S3等)

おわりに

「書類作成に毎回1時間かかる」という課題は、中小企業の多くが抱えているにもかかわらず、後回しにされがちです。

このツールを作ったことで、AI × 実装力で中小企業の業務課題を解決できるという手応えを得られました。

同様の課題をお持ちの方、または自社向けにカスタマイズして導入したい方は、お気軽にご連絡ください。

📧 columbus0370@gmail.com
🐦 @columbus0370
🌐 https://columbus520.xsrv.jp/


参考リンク

Discussion