家計簿アプリを作る #45:CSVインポート時のカテゴリ検証
家計簿アプリ作成シリーズの第45回です。前回作ったCSVインポート機能は、カテゴリ名が実際に登録済みのものと一致しているかをチェックしていませんでした。存在しないカテゴリ名でインポートすると、カテゴリ別予算の集計に反映されない取引ができてしまうため、インポート時にカテゴリの妥当性もチェックするようにします。
実装方針
登録・編集フォームで使っている categoryMap({ income: string[], expense: string[] })と同じ形をインポート処理内でも作り、各行のカテゴリ名がそのtypeに対応するカテゴリ一覧に含まれているかを includes でチェックします。
const allCategories = await db.select().from(categories).where(eq(categories.userId, locals.user!.id));
const categoryMap: Record<string, string[]> = {
income: allCategories.filter(c => c.type === 'income').map(c => c.name),
expense: allCategories.filter(c => c.type === 'expense').map(c => c.name)
};
// 各行のバリデーションに追加
if (!categoryMap[typeLabel === '収入' ? 'income' : 'expense'].includes(category)) {
// エラー行として記録
}
エラー行の持たせ方を見直す
これまで不正な行の情報は errorRows: number[] として行番号だけを持たせていましたが、カテゴリ検証を追加するにあたり「何が原因で不正なのか」も伝えたくなりました。エラー種別を持たせて後からメッセージを組み立てる方法も考えましたが、条件分岐のたびに category や amount の値がその場で全部揃っているので、検出した時点でメッセージ文字列を完成させてしまう方がシンプルでした。
const errorRows: { row: number; message: string }[] = [];
const newTransactions = rows.map((row, index) => {
const lineNumber = index + 2;
// ...
if (!categoryMap[typeLabel === '収入' ? 'income' : 'expense'].includes(category)) {
errorRows.push({ row: lineNumber, message: `${lineNumber}行目: カテゴリ「${category}」は存在しません` });
return null;
}
// ...
});
if (errorRows.length > 0) {
return fail(400, {
errors: { file: errorRows.map(r => r.message).join('\n') }
});
}
\n はHTML上でそのまま改行にならない
複数のエラーメッセージを .join('\n') で1つの文字列にまとめて fail() で返し、+page.svelte 側では <span>{form.errors.file}</span> でそのまま表示していたところ、複数行のはずのエラーが1行に詰まって表示されてしまいました。
HTMLはテキストノード内の改行文字(\n)をデフォルトでは無視して1つの空白として扱うため、CSSの white-space を明示的に指定しない限り、見た目には反映されません。
<!-- ✅ whitespace-pre-lineで\nを改行として表示する -->
<span class="font-medium whitespace-pre-line">{form.errors.file}</span>
whitespace-pre-line クラスを追加するだけで、\n が改行として画面に反映されるようになりました。
まとめ
| ポイント | 内容 |
|---|---|
| カテゴリ検証 | 登録済みカテゴリ一覧(categoryMap)と突き合わせてチェックする |
| エラー情報の持たせ方 | 種別を後から組み立てず、検出時点でメッセージを完成させる |
| 複数行メッセージの表示 | \n はデフォルトで無視されるので whitespace-pre-line が必要 |