1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsでCSVを保存しない明細チェックツールを作ったときの実装メモ

1
Posted at

はじめに

個人開発で、明細CSVをアップロード前にチェックできるツールを作りました。

対象は、freee会計にアップロードする前のクレジットカード明細CSV・銀行口座明細CSVです。

ただし、この記事はツールの宣伝というより、実装時に考えたことのメモです。

特に意識したのは以下です。

  • CSVを保存しない
  • CSV本文をログに出さない
  • 明細データをAnalyticsに送らない
  • アップロード前に日付・金額・列ずれを検出する
  • 自動補正できる範囲だけ修正済みCSVを出力する
  • Next.jsで小さく作る

作ったものはこちらです。

作った背景

CSVアップロード系のエラーは、原因がすぐに分かりにくいことがあります。

たとえば、以下のようなケースです。

  • 日付形式が違う
  • 金額にカンマや全角数字が混ざっている
  • 必須項目が空欄
  • CSVの列数が合っていない
  • 銀行口座明細で入金額・出金額を逆に指定している
  • 文字コードが合っていない

会計ソフトにアップロードしてからエラーに気づくよりも、アップロード前に軽く確認できるツールがあると便利だと思い、MVPとして作ってみました。

構成

構成はシンプルです。

  • Next.js App Router
  • TypeScript
  • API Routes
  • Vitest
  • Vercel

大きく分けると、以下のような構成です。

app/
  api/
    csv/
      inspect/
      validate/
      fix/
components/
  csv-checker.tsx
lib/
  csv/
    core.ts
    types.ts
    access-control.ts
    core.test.ts
public/
  samples/

CSVの処理はできるだけ lib/csv 側に寄せて、UIやAPI Routeから分離するようにしました。

APIを3つに分けた

CSV処理のAPIは、以下の3つに分けました。

POST /api/csv/inspect
POST /api/csv/validate
POST /api/csv/fix

役割は以下です。

API 役割
inspect CSVのヘッダーやプレビュー行を読み取る
validate 日付・金額・必須項目などをチェックする
fix 自動補正できる内容だけ修正済みCSVとして返す

最初は1つのAPIにまとめてもよいと思いましたが、UI側の流れを考えると分けた方が扱いやすかったです。

ユーザー操作としては、

  1. CSVを選択
  2. CSV種別を選択
  3. ヘッダーを確認
  4. バリデーション結果を見る
  5. 必要なら修正済みCSVをダウンロード

という流れにしています。

CSVは保存しない

今回かなり意識したのが、CSVの扱いです。

明細CSVには、取引日、金額、取引内容などが含まれます。
そのため、以下の方針にしました。

  • CSVファイルを保存しない
  • DBに保存しない
  • S3などのストレージにも保存しない
  • 処理完了後に破棄する
  • CSV本文をログ出力しない
  • CSV本文をAnalyticsに送らない

APIレスポンスにもキャッシュさせないように、以下のようなヘッダーを付けています。

const noStoreHeaders = {
  "Cache-Control": "no-store",
  Pragma: "no-cache",
  Expires: "0",
};

CSVアップロード系のサービスでは、機能以上に「どこまでデータを扱うのか」を明確にすることが大事だと感じました。

CSV本文をログに出さない

開発中はデバッグのために console.log を入れたくなります。

ただ、今回のような明細CSVでは、CSV本文や取引内容をログに出すのは避けたいです。

そのため、ログに出すとしても以下のようなメタ情報に限定する方針にしました。

OK:
- CSV種別
- 処理結果
- エラー件数
- ファイルサイズの範囲
- 実行ステップ

NG:
- CSV本文
- 取引内容
- 金額
- 残高
- ファイル名
- 個人情報

個人開発でも、このあたりは最初からルール化しておいた方が後で楽だと思います。

ファイルサイズ・行数を制限する

MVPなので、大きなCSVを何でも処理できるようにはしていません。

まずは明細CSVとして現実的な範囲に絞り、以下のような制限を入れました。

  • ファイルサイズ上限
  • 行数上限
  • 列数上限
  • 1セルあたりの文字数上限

これを入れておくと、想定外に大きいファイルや、壊れたCSVが来たときに処理が重くなりすぎるのを避けられます。

対応したCSV種別

現在は以下に対応しています。

credit-card
bank-account

UI上では、

  • クレジットカード明細CSV
  • 銀行口座明細CSV β版

として表示しています。

銀行口座明細CSVは、銀行や出力形式によって差が大きいので、まだβ版扱いです。

クレジットカード明細CSV

クレジットカード明細では、主に以下を見ています。

  • 取引日
  • 取引金額
  • 取引内容
  • カードラベル

チェック内容は以下のようなものです。

  • 取引日が空欄でないか
  • 日付として解釈できるか
  • 金額が数値として解釈できるか
  • 取引内容が空欄でないか
  • 自動補正できる表記ゆれがあるか

銀行口座明細CSV

銀行口座明細CSVでは、標準出力を以下の形式にしています。

取引日,出金額,入金額,残高,取引内容

対応している主な形式は以下です。

  • 取引日が1列にまとまっている形式
  • 取引年・取引月・取引日(日)が分かれている形式
  • 出金額・入金額が別々の列になっている形式
  • 金額1列でプラス/マイナスを判定する形式
  • 残高が任意で入っている形式

金額1列の場合は、以下のように扱っています。

プラス金額 → 入金額
マイナス金額 → 出金額

一方で、出金額・入金額が別々の列になっている場合は、マイナス値をエラーとして扱うようにしました。

理由は、2列形式でマイナス値が入っていると、出金・入金の意味が分かりにくくなるためです。

日付の正規化

日付はCSVによってかなり表記が違います。

対応した例は以下です。

2026/06/01
2026-06-01
2026年6月1日
令和8年6月1日

修正済みCSVでは、基本的に以下の形式へ寄せています。

YYYY/MM/DD

また、銀行口座明細CSVでは以下のような分割日付にも対応しました。

取引年,取引月,取引日(日)
2026,6,1

出力時は以下のように変換します。

2026/06/01

存在しない日付、たとえば 2026/02/30 のような値はエラーにしています。

金額の正規化

金額も表記ゆれが多いです。

対応した例は以下です。

1200
1,200
1200
-1200

修正済みCSVでは、カンマや全角数字を補正して出力します。

1,200 → 1200
1200 → 1200

ただし、以下のような値はエラー扱いにしています。

abc
不明
1,200円

残高の扱い

銀行口座明細CSVでは、残高を任意項目として扱っています。

ルールは以下です。

空欄 → OK
数値 → OK
カンマ付き数値 → 補正
全角数字 → 補正
数値として扱えない値 → エラー

残高は任意ですが、入力されている場合は数値として扱えるか確認した方が安全だと考えました。

自動補正できない場合はfixを止める

修正済みCSVの生成では、自動補正できないエラーが残っている場合は出力しないようにしています。

たとえば、

  • 必須項目が空欄
  • 日付として認識できない
  • 金額が数値ではない
  • 入金額と出金額が両方入力されている

このような場合に無理やりCSVを出力してしまうと、ユーザーが「修正済みなら大丈夫」と誤解する可能性があります。

そのため、fix APIでは非補正可能なエラーがある場合、400で返すようにしています。

{
  "ok": false,
  "message": "自動補正できないエラーが残っているため、修正済みCSVを生成できません。"
}

最後の行が消えるバグ

実装中に、修正済みCSVの最後の行が出力されない問題がありました。

原因は、出力処理でプレビュー行だけを使ってしまっていたことです。

inspectvalidate では画面表示用にプレビュー件数を絞っていましたが、fix では全行を出力する必要があります。

このあたりはテストを追加して防ぐようにしました。

- corrected CSVに全行が含まれていること
- 最後の行が消えないこと

テスト

テストは Vitest を使っています。

現在は、CSV処理のコアロジックに対してテストを追加しています。

credit-card CSV validation
bank-account CSV validation

テストしている主なケースは以下です。

- 正常なクレジットカード明細CSV
- 正常な銀行口座明細CSV
- 修正済みCSVに全行が含まれること
- split date columns
- invalid date
- required field
- deposit/withdrawal both empty
- deposit/withdrawal both entered
- zero amount warning
- single amount column
- comma amount
- full-width digit
- Japanese era date
- balance validation

実行は以下です。

npm test --if-present

ビルドも確認しています。

npm run build

AnalyticsにはCSV内容を送らない

利用状況は見たいですが、明細データは扱いたくありません。

そのため、Analyticsに送るイベントも以下のようなものに限定しています。

OK:
- CSV種別
- 成功/失敗
- エラー件数
- サンプルCSV利用
- フィードバック導線クリック

NG:
- CSV本文
- 取引内容
- 金額
- 残高
- カード番号
- 氏名
- ファイル名

個人開発でも、こういうデータの境界は明確にしておいた方が安心です。

UIで意識したこと

最初から多機能にしすぎると分かりにくいので、UIでは以下を意識しました。

  • 実データを使う前にサンプルCSVで試せる
  • CSV種別を選べる
  • 銀行口座明細CSVはβ版と明記する
  • 条件付き必須の項目を分かりやすくする
  • 修正済みCSVの内容は保証しないと明記する
  • freee公式サービスではないと明記する

特に、銀行口座明細CSVは形式が多いので、β版であることをはっきり出すようにしています。

実装してみて感じたこと

CSVチェックツールは一見シンプルですが、実際に作ってみると考えることが多かったです。

特に難しかったのは以下です。

  • CSV形式のばらつき
  • 日付表記のばらつき
  • 金額表記のばらつき
  • 入金額・出金額の扱い
  • どこまで自動補正するか
  • どこからエラーにするか
  • 明細データをどう安全に扱うか

便利にしようとしすぎると、逆に危険な補正をしてしまう可能性があります。

そのため、MVPでは「補正できるものだけ補正し、判断が必要なものはエラーや警告にする」という方針にしました。

まとめ

Next.jsでCSVチェックツールを作る中で、以下を意識しました。

  • CSVを保存しない
  • CSV本文をログに出さない
  • Analyticsに明細データを送らない
  • APIを inspect / validate / fix に分ける
  • 自動補正できない場合はfixを止める
  • CSV種別ごとにルールを分ける
  • テストで最後の行欠落などを防ぐ

CSVアップロード系のツールは、機能だけでなく、データの扱い方を最初に決めることが大事だと感じました。

作ったツールはこちらです。

明細CSVチェックナビ
https://meisai-csv-check.jp/

※本ツールはfreee公式サービスではありません。
※チェック結果や修正済みCSVの内容を保証するものではありません。
※実際にアップロードする前に、CSVの内容を必ずご自身で確認してください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?