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";
}
フロントエンドからのダウンロード
クライアント側では fetch → Blob → ダウンロードリンクの生成、という流れです。
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" | バリデーション種別 |
formulae | string[] | 数式または値リスト |
allowBlank | boolean | 空白セルの許可 |
showErrorMessage | boolean | 無効値入力時のエラー表示 |
errorTitle | string | エラーダイアログのタイトル |
error | string | エラーメッセージ本文 |
showInputMessage | boolean | セル選択時のヒント表示 |
promptTitle | string | ヒントのタイトル |
prompt | string | ヒントメッセージ |
まとめ
ExcelJSのデータバリデーション機能を使うことで、以下のパターンが実現できます。
- 固定リストのドロップダウン:
formulae: ['"項目1,項目2"'] - 別シート参照のドロップダウン:
formulae: ['SheetName!$A$2:$A$10'] - 入力シート間の相互参照: 同じ
formulaeの仕組みで対応
Next.js APIルートとの組み合わせにより、外部APIから取得した動的データを選択肢に含むExcelテンプレートをオンデマンドで生成し、「ダウンロード → 記入 → アップロード → 処理」というワークフローを構築できます。