Webアプリケーションで「Excelテンプレートをダウンロードしてもらい、記入後にアップロードしてもらう」というワークフローを実装する機会がありました。

要件は以下の通りです。

  • テンプレートの内容がプロジェクトごとに異なる(メンバーやファイルの一覧が動的に変わる)
  • Excelのドロップダウン(入力規則)で、別シートのデータを選択肢として参照させたい
  • サーバーサイド(Next.js APIルート)でExcelを生成し、ブラウザにダウンロードさせたい

これらを ExcelJS で実現できたので、方法を記録します。

ExcelJS

ExcelJSは、Node.jsおよびブラウザ上でExcelファイル(.xlsx)の読み書きを行えるライブラリです。GitHubスター14,000以上、npm週間ダウンロード約200万の定番パッケージです。

npm install exceljs

スタイル設定、数式、画像挿入など多彩な機能を持ちますが、本記事では**データバリデーション(入力規則)**に焦点を当てます。

テンプレートの構成

今回は4シート構成のExcelを生成します。

シート役割
Activity行為の記録(ユーザー入力用)
Entity情報資源の記録(ユーザー入力用)
Membersメンバー一覧(参照データ)
Filesファイル一覧(参照データ)

Activityシートの「担当者」列ではMembersシートの値をドロップダウンで選択、Entityシートの「生成元」列ではActivityシートのIDをドロップダウンで選択、という構成です。

APIルートの基本構造

Next.js App RouterのRoute Handlerとして実装します。

// src/app/api/projects/[id]/template/route.ts
import { NextRequest, NextResponse } from "next/server";
import ExcelJS from "exceljs";

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id: projectId } = await params;

  // 外部APIからデータを取得
  const members = await fetchMembers(projectId);

  const workbook = new ExcelJS.Workbook();
  // ... シート作成(後述)

  const buffer = await workbook.xlsx.writeBuffer();

  return new NextResponse(buffer, {
    status: 200,
    headers: {
      "Content-Type":
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "Content-Disposition": `attachment; filename="template.xlsx"`,
    },
  });
}

writeBuffer() でExcelをバイナリに変換し、適切なContent-Typeでレスポンスを返します。

参照データシートの作成

ドロップダウンの選択肢ソースとなるシートを先に作ります。

const membersSheet = workbook.addWorksheet("Members");
membersSheet.columns = [
  { header: "表示名", key: "displayName", width: 30 },
  { header: "ID", key: "userId", width: 15 },
  { header: "URI", key: "uri", width: 50 },
];

// ヘッダースタイル
membersSheet.getRow(1).eachCell((cell) => {
  cell.font = { bold: true, color: { argb: "FFFFFFFF" } };
  cell.fill = {
    type: "pattern",
    pattern: "solid",
    fgColor: { argb: "FF4472C4" },
  };
});

for (const m of members) {
  membersSheet.addRow({
    displayName: `${m.name} (${m.id})`,
    userId: m.id,
    uri: m.uri,
  });
}

ドロップダウンの設定

ここが本記事の核心です。ExcelJSの dataValidation プロパティでセルに入力規則を設定します。

固定リストのドロップダウン

選択肢が事前に決まっている場合、formulae に文字列リストを直接指定します。

const ACTIVITY_TYPES = ["問題設定", "資料収集", "解釈", "叙述"];

const actSheet = workbook.addWorksheet("Activity");
actSheet.columns = [
  { header: "ID", key: "id", width: 12 },
  { header: "種類", key: "type", width: 16 },
  { header: "担当者", key: "agent", width: 30 },
  { header: "開始日", key: "startDate", width: 14 },
];

const inputRows = 100;

for (let row = 2; row <= inputRows + 1; row++) {
  actSheet.getCell(`B${row}`).dataValidation = {
    type: "list",
    allowBlank: true,
    formulae: [`"${ACTIVITY_TYPES.join(",")}"`],
    showErrorMessage: true,
    errorTitle: "無効な値",
    error: "リストから種類を選択してください",
  };
}

formulae の値は ["\"項目1,項目2,項目3\""] のようにダブルクォートで囲む必要がある点に注意してください。

別シートのセル範囲を参照するドロップダウン

動的なデータを選択肢にする場合、別シートのセル範囲をExcelの数式形式で参照します。

const memberCount = Math.max(members.length, 1);

for (let row = 2; row <= inputRows + 1; row++) {
  actSheet.getCell(`C${row}`).dataValidation = {
    type: "list",
    allowBlank: true,
    formulae: [`Members!$A$2:$A$${memberCount + 1}`],
  };
}

Members!$A$2:$A$5 はExcelの数式と同じ形式です。$ は絶対参照で、コピーや移動で参照先がずれないようにしています。

シート間の相互参照

EntityシートからActivityシートのIDを参照させることもできます。

const entSheet = workbook.addWorksheet("Entity");
entSheet.columns = [
  { header: "ID", key: "id", width: 12 },
  { header: "種類", key: "type", width: 18 },
  { header: "説明", key: "desc", width: 60 },
  { header: "生成元Activity", key: "generatedBy", width: 16 },
];

for (let row = 2; row <= inputRows + 1; row++) {
  entSheet.getCell(`D${row}`).dataValidation = {
    type: "list",
    allowBlank: true,
    formulae: [`Activity!$A$2:$A$${inputRows + 1}`],
  };
}

Activityシートに Act_0001, Act_0002 … と入力していくと、Entityシートの「生成元Activity」列のドロップダウンにそれらが自動的に現れます。入力シート同士を相互参照できるのが便利なところです。

日付フォーマットの設定

日付列には numFmt で表示形式を指定します。

for (let row = 2; row <= inputRows + 1; row++) {
  actSheet.getCell(`D${row}`).numFmt = "yyyy-mm-dd";
}

フロントエンドからのダウンロード

クライアント側では fetchBlob → ダウンロードリンクの生成、という流れです。

const handleDownload = async () => {
  const response = await fetch(`/api/projects/${projectId}/template`);
  const blob = await response.blob();
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `template_${new Date().toISOString().slice(0, 19)}.xlsx`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  window.URL.revokeObjectURL(url);
};

アップロードされたExcelの読み込み

ユーザーが記入したExcelをアップロードして処理する側です。

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get("file") as File;

  const arrayBuffer = await file.arrayBuffer();
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.load(arrayBuffer);

  const actSheet = workbook.getWorksheet("Activity");
  if (actSheet) {
    actSheet.eachRow((row, rowNumber) => {
      if (rowNumber === 1) return; // ヘッダースキップ

      const id = row.getCell(1).text?.trim();
      const type = row.getCell(2).text?.trim();
      const agent = row.getCell(3).text?.trim();

      if (!id) return; // 空行スキップ

      // 変換処理(例: RDF生成、DB登録など)
    });
  }

  return NextResponse.json({ success: true });
}

row.getCell(n).text でセルの値を文字列として取得できます。日付セルも .text で文字列化されるため、型変換を気にせず扱えます。

dataValidationのプロパティ

参考として、主要なプロパティをまとめます。

プロパティ説明
type"list" / "whole" / "decimal" / "date" / "custom"バリデーション種別
formulaestring[]数式または値リスト
allowBlankboolean空白セルの許可
showErrorMessageboolean無効値入力時のエラー表示
errorTitlestringエラーダイアログのタイトル
errorstringエラーメッセージ本文
showInputMessagebooleanセル選択時のヒント表示
promptTitlestringヒントのタイトル
promptstringヒントメッセージ

まとめ

ExcelJSのデータバリデーション機能を使うことで、以下のパターンが実現できます。

  • 固定リストのドロップダウン: formulae: ['"項目1,項目2"']
  • 別シート参照のドロップダウン: formulae: ['SheetName!$A$2:$A$10']
  • 入力シート間の相互参照: 同じ formulae の仕組みで対応

Next.js APIルートとの組み合わせにより、外部APIから取得した動的データを選択肢に含むExcelテンプレートをオンデマンドで生成し、「ダウンロード → 記入 → アップロード → 処理」というワークフローを構築できます。