<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Excel on デジタルアーカイブシステムの技術ブログ</title><link>https://tech.ldas.jp/ja/tags/excel/</link><description>Recent content in Excel on デジタルアーカイブシステムの技術ブログ</description><generator>Hugo</generator><language>ja</language><lastBuildDate>Thu, 02 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://tech.ldas.jp/ja/tags/excel/index.xml" rel="self" type="application/rss+xml"/><item><title>Next.js APIルートでExcelJSを使い、別シート参照ドロップダウン付きテンプレートを動的生成する</title><link>https://tech.ldas.jp/ja/posts/nextjs-exceljs-dynamic-template/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0900</pubDate><guid>https://tech.ldas.jp/ja/posts/nextjs-exceljs-dynamic-template/</guid><description>&lt;p>Webアプリケーションで「Excelテンプレートをダウンロードしてもらい、記入後にアップロードしてもらう」というワークフローを実装する機会がありました。&lt;/p>
&lt;p>要件は以下の通りです。&lt;/p>
&lt;ul>
&lt;li>テンプレートの内容がプロジェクトごとに異なる（メンバーやファイルの一覧が動的に変わる）&lt;/li>
&lt;li>Excelのドロップダウン（入力規則）で、別シートのデータを選択肢として参照させたい&lt;/li>
&lt;li>サーバーサイド（Next.js APIルート）でExcelを生成し、ブラウザにダウンロードさせたい&lt;/li>
&lt;/ul>
&lt;p>これらを &lt;a href="https://github.com/exceljs/exceljs">ExcelJS&lt;/a> で実現できたので、方法を記録します。&lt;/p>
&lt;h2 id="exceljs">ExcelJS&lt;/h2>
&lt;p>ExcelJSは、Node.jsおよびブラウザ上でExcelファイル（.xlsx）の読み書きを行えるライブラリです。GitHubスター14,000以上、npm週間ダウンロード約200万の定番パッケージです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npm install exceljs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>スタイル設定、数式、画像挿入など多彩な機能を持ちますが、本記事では**データバリデーション（入力規則）**に焦点を当てます。&lt;/p>
&lt;h2 id="テンプレートの構成">テンプレートの構成&lt;/h2>
&lt;p>今回は4シート構成のExcelを生成します。&lt;/p>
&lt;div class="table-wrapper">
 &lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>シート&lt;/th>
 &lt;th>役割&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Activity&lt;/td>
 &lt;td>行為の記録（ユーザー入力用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Entity&lt;/td>
 &lt;td>情報資源の記録（ユーザー入力用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Members&lt;/td>
 &lt;td>メンバー一覧（参照データ）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Files&lt;/td>
 &lt;td>ファイル一覧（参照データ）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
 &lt;/table>
&lt;/div>
&lt;p>Activityシートの「担当者」列ではMembersシートの値をドロップダウンで選択、Entityシートの「生成元」列ではActivityシートのIDをドロップダウンで選択、という構成です。&lt;/p>
&lt;h2 id="apiルートの基本構造">APIルートの基本構造&lt;/h2>
&lt;p>Next.js App RouterのRoute Handlerとして実装します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/app/api/projects/[id]/template/route.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">NextRequest&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">NextResponse&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s2">&amp;#34;next/server&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nx">ExcelJS&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s2">&amp;#34;exceljs&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">GET&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">request&lt;/span>: &lt;span class="kt">NextRequest&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">params&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">params&lt;/span>: &lt;span class="kt">Promise&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="p">{&lt;/span> &lt;span class="nx">id&lt;/span>: &lt;span class="kt">string&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">id&lt;/span>: &lt;span class="kt">projectId&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">params&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 外部APIからデータを取得
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">members&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">fetchMembers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projectId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">workbook&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ExcelJS&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Workbook&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... シート作成（後述）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">buffer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">workbook&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">xlsx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">writeBuffer&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">NextResponse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">buffer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">status&lt;/span>: &lt;span class="kt">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Content-Disposition&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`attachment; filename=&amp;#34;template.xlsx&amp;#34;`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>writeBuffer()&lt;/code> でExcelをバイナリに変換し、適切なContent-Typeでレスポンスを返します。&lt;/p>
&lt;h2 id="参照データシートの作成">参照データシートの作成&lt;/h2>
&lt;p>ドロップダウンの選択肢ソースとなるシートを先に作ります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">membersSheet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">workbook&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addWorksheet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Members&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">membersSheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">columns&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">header&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;表示名&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;displayName&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">width&lt;/span>: &lt;span class="kt">30&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">header&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;ID&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">width&lt;/span>: &lt;span class="kt">15&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">header&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;URI&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;uri&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">width&lt;/span>: &lt;span class="kt">50&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ヘッダースタイル
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">membersSheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getRow&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">eachCell&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">cell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">font&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">bold&lt;/span>: &lt;span class="kt">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">argb&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;FFFFFFFF&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;pattern&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">pattern&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;solid&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fgColor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">argb&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;FF4472C4&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">m&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="nx">members&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">membersSheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addRow&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">displayName&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> (&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">)`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">userId&lt;/span>: &lt;span class="kt">m.id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">uri&lt;/span>: &lt;span class="kt">m.uri&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ドロップダウンの設定">ドロップダウンの設定&lt;/h2>
&lt;p>ここが本記事の核心です。ExcelJSの &lt;code>dataValidation&lt;/code> プロパティでセルに入力規則を設定します。&lt;/p></description></item></channel></rss>