ClaudeとJavaScriptで作るポモドーロタイマー【Webアプリ】

に公開

はじめに

科学的な時間管理術として広く知られるポモドーロテクニックを実践するためのWebタイマーアプリを開発しました。

ブラウザ最小化時も正確に動作する高精度タイマーと、作業フローを中断しない自動切り替え機能が特徴です。

本記事について

今回のポモドーロタイマー開発および記事執筆は、Claude Code(Anthropic社のAI)との協業で進めました。
AI時代の新しい開発スタイルの実践例として参考になれば幸いです。

自己紹介

ホネグミ代表、応用情報技術者の資格を持つエンジニア×マーケターです。
これまでIT系の会社役員を4年、独立して4年目になります。
クライアントワークでは「こうしたい」を技術で形にすることを専門としていますが、最近は思想駆動型サービス開発の第一人者として、AIを活用した様々なサービス開発を続けています。

https://zenn.dev/5naokichi/articles/8f9446a136a874

サービス概要

https://hone-gumi.com/pomodoro-timer/

主な機能:

  • 25分作業・5分休憩・長休憩(15/20/25/30分)タイマー
  • ブラウザ最小化対応の高精度計測
  • タイマー完了時の自動切り替え機能
  • 5種類のアラーム音と音量調整
  • 本日の完了セット数管理

技術スタック

フロントエンド(HTML/CSS/JavaScript)
├── 高精度タイマー(Page Visibility API対応)
├── 音響システム(HTML5 Audio API)
├── 状態管理(Local Storage)
└── レスポンシブUI(CSS Grid/Flexbox)

使用技術

  • HTML5: セマンティックマークアップ、Audio要素
  • CSS3: Grid/Flexbox、レスポンシブデザイン、CSS変数
  • JavaScript ES6+: モジュール設計、非同期処理
  • Page Visibility API: バックグラウンド動作対応
  • Local Storage: クライアントサイドデータ管理

実装手順

1. 高精度タイマーの実装

従来のsetIntervalベースのタイマーは、ブラウザがバックグラウンドに移行すると動作が不安定になります。この問題を解決するため、実際の経過時間を基準とした高精度タイマーを実装しました。

// 高精度タイマー用変数
let startTimestamp = null;
let totalDuration = null;
let pausedTime = 0;

function startTimer() {
  if (isRunning) return;
  
  isRunning = true;
  // 実際の開始時刻を記録(一時停止分を考慮)
  startTimestamp = Date.now() - pausedTime * 1000;
  totalDuration = currentTime + pausedTime;
  
  timerInterval = setInterval(() => {
    // 実際の経過時間を基に残り時間を算出
    const now = Date.now();
    const actualElapsed = Math.floor((now - startTimestamp) / 1000);
    const expectedRemaining = totalDuration - actualElapsed;
    
    if (expectedRemaining <= 0) {
      currentTime = 0;
      completeTimer();
    } else {
      currentTime = expectedRemaining;
      updateDisplay();
      updateTabTitle();
    }
  }, 100); // 高頻度更新で精度向上
}

2. Page Visibility API対応

ブラウザのタブ切り替えやアプリ最小化を検出し、フォアグラウンド復帰時に正確な時間を再計算します。

function setupVisibilityAPI() {
  document.addEventListener('visibilitychange', function() {
    if (isRunning) {
      if (document.hidden) {
        // バックグラウンドに移行
        console.log('タイマーがバックグラウンドに移行');
      } else {
        // フォアグラウンドに復帰
        console.log('タイマーがフォアグラウンドに復帰');
        if (startTimestamp) {
          // 実際の経過時間を再計算
          const now = Date.now();
          const actualElapsed = Math.floor((now - startTimestamp) / 1000);
          const expectedRemaining = totalDuration - actualElapsed;
          
          if (expectedRemaining <= 0) {
            currentTime = 0;
            completeTimer();
          } else {
            currentTime = expectedRemaining;
            updateDisplay();
            updateTabTitle();
          }
        }
      }
    }
  });
}

3. 自動切り替え機能の実装

ポモドーロテクニックの作業フローを中断しないため、タイマー完了時に自動的に次のフェーズに移行する機能を実装しました。

function completeTimer() {
  isRunning = false;
  clearInterval(timerInterval);
  
  // 高精度タイマー変数をリセット
  startTimestamp = null;
  totalDuration = null;
  pausedTime = 0;
  
  // セット数のカウント(作業時間完了時のみ)
  if (currentMode === 'work') {
    sessionCount++;
    updateSessionDisplay();
  }
  
  // アラーム再生
  playAlarm();
  
  // 完了状態表示
  timerStatus.textContent = '完了!';
  document.title = '完了! - ポモドーロタイマー';
  
  // 次のタイマーに自動切り替え
  setTimeout(() => {
    if (currentMode === 'work') {
      // 作業完了後は5分休憩に切り替え
      selectTime(5, 'break');
    } else if (currentMode === 'break' || currentMode === 'longbreak') {
      // 休憩完了後は25分作業に切り替え
      selectTime(25, 'work');
    }
  }, 1000); // 1秒後に切り替え
  
  saveData();
}

4. ローカルストレージによるデータ管理

セット数カウントと設定情報をブラウザローカルに保存し、日付変更時に自動リセットする機能を実装しました。

function saveData() {
  const data = {
    sessionCount: sessionCount,
    volume: volumeSlider.value,
    sound: soundSelect.value,
    lastDate: new Date().toDateString()
  };
  localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}

function loadData() {
  const data = localStorage.getItem(STORAGE_KEY);
  if (data) {
    try {
      const parsed = JSON.parse(data);
      const today = new Date().toDateString();
      
      // 日付が変わっていたらセッション数をリセット
      if (parsed.lastDate === today) {
        sessionCount = parsed.sessionCount || 0;
      } else {
        sessionCount = 0;
      }
      
      // 設定の復元
      if (parsed.volume !== undefined) {
        volumeSlider.value = parsed.volume;
        updateSliderProgress();
      }
      if (parsed.sound) {
        soundSelect.value = parsed.sound;
      }
    } catch (e) {
      console.log('データ読み込みエラー:', e);
    }
  }
}

工夫したポイント・苦労した点

タイマー精度の課題と解決

最も苦労したのは、ブラウザのバックグラウンド動作での精度維持でした。通常のsetIntervalは、ブラウザがバックグラウンドに移行すると動作頻度が大幅に低下し、正確な時間計測ができません。

解決策として、Date.now()を基準とした実時間計算を採用しました。

詳細な実装理由

タイマー開始時刻を記録し、毎回の更新で実際の経過時間を計算することで、ブラウザの状態に関係なく正確な残り時間を表示できるようになりました。

従来の方法:

  • setIntervalで1秒ごとに残り時間を-1
  • ブラウザがバックグラウンドになると更新頻度が低下
  • 実際の時間とズレが発生

新しい方法:

  • 開始時刻をDate.now()で記録
  • 毎回の更新で実際の経過時間を計算
  • ブラウザの状態に関係なく正確

UXを重視した自動切り替え設計

従来のタイマーアプリでは、作業完了後にユーザーが手動で休憩タイマーを開始する必要があり、この操作で集中が途切れる問題がありました。

自動切り替え機能により、ユーザーは開始操作のみで継続的なポモドーロセッションを実行できるようになりました。

音響機能の最適化

作業環境に配慮した音響設計として、5種類のアラーム音と細かな音量調整機能を実装しました。

function playAlarm() {
  const selectedSound = soundSelect.value;
  const audio = document.getElementById('alarm-' + selectedSound);
  
  if (audio) {
    audio.volume = volumeSlider.value / 100;
    audio.currentTime = 0;
    audio.play().catch(e => console.log('音声再生に失敗:', e));
    
    // 現在再生中の音声を記録
    currentlyPlayingAudio = audio;
    
    // 停止ボタンを表示
    stopAlarmBtn.style.display = 'inline-flex';
  }
}

まとめ

Page Visibility APIと実時間計算を組み合わせることで、ブラウザの動作状況に左右されない高精度なタイマーアプリを実現できました。

特に重要だったのは、単なる時間計測ツールではなく、ユーザーの作業体験全体を考慮した設計にすることでした。

技術的な学び

  • Page Visibility APIの実用的な活用方法
  • ブラウザ環境での高精度タイマー実装のベストプラクティス
  • UXを重視したAPI設計の重要性
  • Claude Codeとの協業による効率的な開発手法

自動切り替え機能により、ポモドーロテクニックの理想的な作業フローを技術的にサポートすることで、真に実用的なツールとなっています。

サービスはこちら

https://hone-gumi.com/pomodoro-timer/

免責事項

本サービスは娯楽目的で提供されており、時間管理の効果には個人差があります。重要な業務や試験等での使用は、事前に十分なテストを行った上で自己責任でお願いいたします。

Discussion