Backlog REST API v2 から課題を取得し、完了タスクを Excel 形式(.xlsx
)でダウンロードできる機能を追加しました。
UI(/backlog) ──▶ GET /api/export-backlog ──▶ Backlog API (/issues)
▲ │
└────────────── Excel ファイル (.xlsx) ─────────┘
| 追加/変更ファイル | 役割 |
| ------------------------------------------------------- | --------------------------------- |
| `app/_features/backlog/funcs.ts` | Backlog API 呼び出し & データ整形 |
| `app/_features/backlog/type.ts` | Backlog Issue 型定義 |
| `app/_features/backlog/components/task-table/` | 課題一覧テーブル UI |
| `app/_features/backlog/components/export-excel-button/` | Excel ダウンロードボタン UI |
| `app/api/export-backlog/route.ts` | Excel 生成 & ダウンロード API |
| `app/backlog/page.tsx` | 機能を束ねるページ |
| `package.json` | 依存に `exceljs` を追加 |
const BASE_URL = `https://${BACKLOG_SPACE_ID}.backlog.com/api/v2`
async function backlogFetch<T>(
endpoint: string,
params?: Record<string, unknown>
): Promise<T> {
const url = new URL(`${BASE_URL}${endpoint}`)
const searchParams = new URLSearchParams()
// API keyはパラメーターで渡す
searchParams.append('apiKey', BACKLOG_API_KEY as string)
url.search = searchParams.toString()
const res = await fetch(url.toString())
if (!res.ok) {
throw new Error(`Backlog API error: ${res.status} ${res.statusText}`)
}
return (await res.json()) as T
}
async function fetchBacklogTasks(): Promise<BacklogTask[]> {
// 課題取得一覧
// @see: https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/
// マイルストーン
// @see: https://developer.nulab.com/ja/docs/backlog/api/2/get-version-milestone-list/
const issues = await backlogFetch<BIssues[]>('/issues')
const tasks: BacklogTask[] = []
for (const issue of issues) {
tasks.push({
id: issue.id,
summary: issue.summary,
assigneeName: issue.assignee?.name,
statusName: issue.status?.name,
created: issue.created,
parentIssueId: issue.parentIssueId,
})
}
return tasks
}
// page.tsx
import { BacklogExportButton } from '@/_features/backlog/components/export-excel-button'
import { TaskTable } from '@/_features/backlog/components/task-table'
type BacklogTask = {
id: number
summary: string
assigneeName: string
statusName: string
created: string
parentIssueId: number | null
}
export default async function BacklogPage() {
let tasks: BacklogTask[] = []
let error: string | null = null
try {
tasks = await fetchBacklogTasks()
} catch (e) {
error = (e as Error).message
}
return (
<main className="p-4">
<h1 className="mb-4 text-2xl font-bold">Backlog タスク一覧</h1>
<div className="mb-4">
<BacklogExportButton />
</div>
{error ? (
<p className="text-red-500">{error}</p>
) : (
<table className="min-w-full text-left">
<thead className="border-b">
<tr>
<th className="px-2 py-1">担当者名</th>
<th className="px-2 py-1">件名(Issue)</th>
<th className="px-2 py-1">Created</th>
<th className="px-2 py-1">ステータス</th>
<th className="px-2 py-1">子課題</th>
</tr>
</thead>
<tbody>
{tasks.map((t) => (
<tr key={t.id} className="border-b last:border-b-0">
<td className="px-2 py-1">{t.assigneeName}</td>
<td className="px-2 py-1">{t.summary}</td>
<td className="px-2 py-1">{t.created}</td>
<td className="px-2 py-1">{t.statusName}</td>
<td className="px-2 py-1">{t.parentIssueId}</td>
</tr>
))}
</tbody>
</table>
)}
</main>
)
}
issuesAPIを叩いたレスポンスの型
export interface BIssues {
id: number
projectId: number
issueKey: string
keyId: number
issueType: {
id: number
projectId: number
name: string
color: string
displayOrder: number
}
/** 件名 */
summary: string
description: string
resolution: string | null
priority: {
id: number
name: string
}
/** ステータス */
status: {
// ステータスのID(完了の場合は4のような数値)
id: number
projectId: number
// ステータスの名前(完了など)
name: string
color: string
displayOrder: number
}
/* 担当者*/
assignee: {
id: number
userId: string
name: string
roleType: number
lang: string | null
nulabAccount: {
nulabId: string
name: string
uniqueId: string
}
mailAddress: string
lastLoginTime: string
}
category: string[]
versions: string[]
milestone: string[]
/** 開始日 */
startDate: string | null
/** 完了日 */
completed: string | null
/** 期限日 */
dueDate: string | null
/** 見積もり時間 */
estimatedHours: number | null
/** 実績時間 */
actualHours: number | null
/** 親課題ID */
parentIssueId: number | null
createdUser: {
id: number
userId: string
name: string
roleType: number
lang: string | null
}
created: string
updatedUser: {
id: number
userId: string
name: string
roleType: number
lang: string | null
}
updated: string
customFields: any[]
attachments: any[]
sharedFiles: any[]
stars: any[]
}
// 16:44:app/_features/backlog/funcs.ts
export async function fetchBacklogTasks(): Promise<BacklogTask[]> {
const issues = await backlogFetch<BIssues[]>('/issues')
return issues.map((issue) => ({
id: issue.id,
summary: issue.summary,
assigneeName: issue.assignee?.name,
statusName: issue.status?.name,
created: issue.created,
parentIssueId: issue.parentIssueId,
}))
}
backlogFetch
が環境変数 BACKLOG_SPACE_ID
と BACKLOG_API_KEY
を用いて REST API を実行。BacklogTask
モデルへ変換しています。必要な環境変数
BACKLOG_SPACE_ID=example # サブドメイン部
BACKLOG_API_KEY=xxxxxxxxxxx # 発行した個人 API キー
// 9:38:app/api/export-backlog/route.ts
const workbook = new ExcelJS.Workbook()
const sheet = workbook.addWorksheet("Backlog Report")
sheet.columns = [
{ header: "担当者名", key: "assigneeName", width: 20 },
{ header: "件名(Issue)", key: "summary", width: 40 },
{ header: "Created", key: "created", width: 25 },
{ header: "ステータス", key: "statusName", width: 25 },
{ header: "ParentIssueId", key: "parentIssueId", width: 15 },
]
// tasks.forEach(...) // ← 行追加はここ(今はコメントアウト)
const buffer = await workbook.xlsx.writeBuffer()
return new Response(buffer, {
headers: {
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": "attachment; filename=backlog_report.xlsx",
},
})
fetchBacklogTasks()
を呼んで即座にファイルダウンロード。// 3:8:app/_features/backlog/components/export-excel-button/hooks.ts
export function useBacklogExport() {
return useCallback(() => {
const link = document.createElement('a')
link.href = '/api/export-backlog'
link.click()
}, [])
}
/api/export-backlog
にリダイレクト → ブラウザが自動ダウンロード。このフックを使った UI コンポーネント
// 10:16:app/_features/backlog/components/export-excel-button/index.tsx
<button onClick={handleExport}>📥 Excel出力(完了課題)</button>
// 14:31:app/_features/backlog/components/task-table/index.tsx
<tbody>
{rows.map((t) => (
<tr key={t.id}>
<td>{t.assigneeName}</td>
<td>{t.summary}</td>
<td>{t.created}</td>
<td>{t.statusName}</td>
<td>{t.parentIssueId}</td>
</tr>
))}
</tbody>
useTaskRows
で tasks
をメモ化し、シンプルに描画。sheet.addRow(...)
)のコメントアウトを解除して実データを書き込むBACKLOG_PROJECT_ID
や statusId=4
(完了)の絞り込みパラメータ追加以上で Backlog ツール機能の概要と実装ポイントの説明を終わります。