在庫・出荷管理アプリを作ってます ⑭:日本時間の朝に「本日のデータ」が表示されない?タイムゾーンバグの原因と解決法

目次

はじめに

前回の記事にて、デモサイトを公開するに至ったのですが、
公開した直後に問題が見つかるのは良くある話で、「本日のデータ」が表示されないという奇妙なバグに遭遇しました。

この記事では、JavaScriptの日付処理におけるタイムゾーンの罠と、その解決方法を詳しく解説します。


バグの症状

発見の経緯

開発環境で操作していたときのこと。

朝7時36分に仮で出荷予定をいれていたとき

本日の出荷予定: 0件

「あれ?データを入れたはずなのに…」

念のためSupabaseのデータベースを確認すると、データは確かに存在していました。

バグの特徴

  • 時間帯: 朝7時台に発生
  • 症状: 「本日のデータ」として登録したはずのデータが表示されない
  • データベース: データは正常に保存されている
  • 前日の動作: 問題なく表示されていた

原因の調査

まずは現状を把握するため、ブラウザのコンソールで以下を実行しました。

console.log('UTC:', new Date().toISOString());
// 出力: UTC: 2025-10-19T22:36:03.472Z

console.log('JST:', new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
// 出力: JST: 2025/10/20 07:36:03

ここで気づきました。

  • 日本時間(JST): 2025年10月20日 7時36分
  • UTC時刻: 2025年10月19日 22時36分

日本時間とUTC時刻で日付が1日ズレているのです。

問題のコード

// 問題のあるコード
const today = new Date().toISOString().split("T")[0];
// → "2025-10-19" (UTC時刻基準)

// データベースの出荷日
shipment_date: "2025-10-20" (日本時間で登録)

// 比較結果
"2025-10-19" === "2025-10-20" // → false ❌

toISOString() は常にUTC時刻を返すため、日本時間の朝7時(UTC: 前日22時)では前日の日付になってしまいます。


解決策(第1回:失敗)

まずは、toLocaleString() で日本時間に変換する方法を試しました。

const getJSTDate = () => {
  const now = new Date();
  // 日本時間の文字列を取得
  const jstTime = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Tokyo' }));
  return jstTime.toISOString().split('T')[0];
};

修正をデプロイし、再度確認…

console.log('getJSTDate()の結果:', getJSTDate());
// → "2025-10-19" ❌

console.log('期待値:', '2025-10-20');

直っていない!

なぜ失敗したのか?

この方法が失敗した理由を詳しく見ていきます。

// ステップ1: toLocaleString が返す文字列
now.toLocaleString('en-US', { timeZone: 'Asia/Tokyo' })
// → "10/20/2025, 7:36:03 AM"

// ステップ2: 文字列をDateオブジェクトに変換
new Date("10/20/2025, 7:36:03 AM")
// → ブラウザがローカルタイムゾーン(JST)で解釈
// → 内部的に 2025-10-20 07:36:03 JST として保存

// ステップ3: toISOString()でUTC時刻に変換
jstTime.toISOString()
// → "2025-10-19T22:36:03.000Z" (9時間戻る!)

// ステップ4: 日付部分を抽出
.split('T')[0]
// → "2025-10-19" ❌

文字列パースの過程で再度タイムゾーン変換が入り、結局UTC基準に戻ってしまうのが原因でした。


✅ 解決策(第2回:成功)

文字列を介さず、タイムスタンプベースで直接計算する方式に変更しました。

const getJSTDate = () => {
  const now = new Date();
  const utcTime = now.getTime(); // UTCタイムスタンプ(ミリ秒)
  const jstOffset = 9 * 60 * 60 * 1000; // 9時間をミリ秒で(32,400,000ms)
  const jstTime = new Date(utcTime + jstOffset); // 日本時間のタイムスタンプ
  return jstTime.toISOString().split('T')[0]; // ISO形式から日付部分を抽出
};

この方法が成功する理由

// ステップ1: UTC時刻のタイムスタンプを取得
const utcTime = now.getTime();
// → 1729468563472 (ミリ秒)

// ステップ2: 9時間(32,400,000ms)を加算
const jstTime = new Date(utcTime + 32400000);
// → 内部的には UTC+9時間 のタイムスタンプ

// ステップ3: ISO文字列に変換
jstTime.toISOString()
// → "2025-10-20T07:36:03.472Z"
// (実際は日本時間だが、UTC形式で出力される)

// ステップ4: 日付部分を抽出
.split('T')[0]
// → "2025-10-20" ✅

動作確認:

console.log('修正後:', getJSTDate());
// → "2025-10-20" ✅

console.log('期待値:', '2025-10-20');
// → 一致!

完璧です!


修正箇所

複数のページファイルで日付処理を使っていたため、該当箇所を修正しました。

修正が必要だった場面

このアプリでは、以下のような場面で「今日の日付」を使っていました:

  1. 出荷予定一覧ページ
  • 今日の出荷予定を表示
  • 「本日」「過去」のバッジ表示
  • 1週間後、1ヶ月後の日付計算
  1. 新規登録ページ
  • デフォルト日付として今日の日付を設定

これらすべてで new Date().toISOString().split("T")[0] を使っていたため、朝7時台に問題が発生していました。

具体的な修正内容

パターン1: getJSTDate関数の追加

各ページファイルの冒頭に、日本時間の日付を取得する関数を追加しました。

// 各ページファイルに追加
const getJSTDate = () => {
  const now = new Date();
  const utcTime = now.getTime();
  const jstOffset = 9 * 60 * 60 * 1000;
  const jstTime = new Date(utcTime + jstOffset);
  return jstTime.toISOString().split('T')[0];
};

パターン2: 今日の日付取得を修正

// 修正前
const today = new Date().toISOString().split("T")[0];

// 修正後
const today = getJSTDate();

パターン3: 未来の日付計算を修正

1週間後、1ヶ月後などの日付計算も同様に修正:

// 修正前
const oneWeekLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  .toISOString()
  .split("T")[0];

// 修正後
const oneWeekLater = (() => {
  const date = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tokyo' }));
  date.setDate(date.getDate() + 7);
  return date.toISOString().split('T')[0];
})();

パターン4: 日付比較の条件式を修正

テーブル表示で「本日」「過去」を判定する部分:

// 修正前
const isToday = shipment.shipment_date === new Date().toISOString().split("T")[0];
const isPast = shipment.shipment_date < new Date().toISOString().split("T")[0];

// 修正後
const isToday = shipment.shipment_date === getJSTDate();
const isPast = shipment.shipment_date < getJSTDate();

これにより、出荷予定一覧で「本日」「過去」バッジが正しく表示されるようになりました。


検証とデプロイ

1. キャッシュクリアの重要性

修正後、以下を実行してください:

# Next.jsのビルドキャッシュを削除
rm -rf .next

# 開発サーバーを再起動
npm run dev

ブラウザ側でも:

  • スーパーリロード: Ctrl + Shift + R(Windows/Linux)、Cmd + Shift + R(Mac)
  • または開発者ツールを開いた状態でリロード

2. 動作確認のコツ

朝7時まで待たずにテストする方法:

// ブラウザコンソールで実行
const testDate = new Date('2025-10-20T07:00:00+09:00');
console.log('テスト日時:', testDate);
console.log('UTC表記:', testDate.toISOString());
// → "2025-10-19T22:00:00.000Z" (前日22時)

// getJSTDate()関数をテスト
console.log('getJSTDate()結果:', getJSTDate());
// → "2025-10-20" ✅

学んだこと

1. JavaScriptの日付処理における3つの罠

罠1: toISOString() は常にUTC時刻

// 日本時間 2025-10-20 07:00
new Date('2025-10-20T07:00:00+09:00').toISOString()
// → "2025-10-19T22:00:00.000Z"(前日!)

罠2: toLocaleString() の文字列パース

const str = new Date().toLocaleString('en-US', { timeZone: 'Asia/Tokyo' });
// → "10/20/2025, 7:00:00 AM"

new Date(str) // ← ブラウザがローカルTZで解釈
// → 再度タイムゾーン変換が発生

罠3: 日付比較の落とし穴

// 文字列比較は有効だが、取得方法を間違えると...
"2025-10-19" === "2025-10-20" // → false

2. 確実な日本時間取得の実装パターン

推奨方法: タイムスタンプ + オフセット計算

const getJSTDate = () => {
  const now = new Date();
  const utcTime = now.getTime();
  const jstOffset = 9 * 60 * 60 * 1000;
  const jstTime = new Date(utcTime + jstOffset);
  return jstTime.toISOString().split('T')[0];
};

メリット:

  • 文字列パースを経由しないため確実
  • シンプルで理解しやすい
  • パフォーマンスが良い
  • ISO 8601形式(YYYY-MM-DD)が直接得られる

3. デバッグの重要性

修正したつもりでも、必ず検証:

// 期待値と実際の値を比較
console.log('修正後:', getJSTDate());
console.log('期待値:', '2025-10-20');
console.log('一致?:', getJSTDate() === '2025-10-20');

4. タイムゾーンを意識した設計

  • データベースに保存する日付は常にISO 8601形式で統一
  • 表示時のみローカライズ
  • サーバーサイドとクライアントサイドで一貫性を保つ

再発防止策

共通関数化の検討

今回は各ページファイルに同じgetJSTDate()関数を定義しましたが、本来は以下のように共通ファイルに切り出すのがベストプラクティスです。

// lib/dateUtils.ts(今後の実装予定)
export const getJSTDate = (): string => {
  const now = new Date();
  const utcTime = now.getTime();
  const jstOffset = 9 * 60 * 60 * 1000;
  const jstTime = new Date(utcTime + jstOffset);
  return jstTime.toISOString().split('T')[0];
};

export const getJSTDateTime = (): Date => {
  const now = new Date();
  const utcTime = now.getTime();
  const jstOffset = 9 * 60 * 60 * 1000;
  return new Date(utcTime + jstOffset);
};

これにより:

  • コードの重複を削減
  • メンテナンス性向上
  • テストが容易に

次のリファクタリングで対応予定です。

他のページの確認状況

日付比較を行っている可能性のある箇所もチェックしました:

  • ✅ 出荷予定一覧ページ – 修正完了
  • ✅ 新規登録ページ – 修正完了
  • ⚠️ 出荷実績比較ページ – 要確認
  • ⚠️ その他のページ – 今後確認予定

まとめ

日本時間の朝7時 = UTC時刻の前日22時

この9時間のズレを意識せずに toISOString() を使うと、日付比較で予期せぬバグが発生します。

重要なポイント

  1. toISOString() は常にUTC時刻を返す
  2. 文字列パースは避けるtoLocaleString() の落とし穴)
  3. タイムスタンプ + オフセット計算が最も確実
  4. 修正後は必ずキャッシュクリア
  5. console.log で必ず検証

最終的な解決コード

const getJSTDate = () => {
  const now = new Date();
  const utcTime = now.getTime();
  const jstOffset = 9 * 60 * 60 * 1000;
  const jstTime = new Date(utcTime + jstOffset);
  return jstTime.toISOString().split('T')[0];
};

JavaScriptで日付処理を行う際は、タイムゾーンを意識しないといけないと思いました。


参考リンク


目次