Employee Blog
社員ブログ

Cursorを使ったWEBアプリ開発(後編)

こんにちは、システム2部改め3部のRyanです。

前編の記事公開からかなりのお時間が経ってしまいましたが、Cursorを使ったWEBアプリ(割り勘アプリ)開発について、続きの過程を書いていきたいと思います。

前回はアプリのドラフトページを作成するところまでだったかと思いますので、ここからブラッシュアップをしていきたいと思います。

では、始めていきましょう。


この時点では、まだ登録されたデータから割り勘の金額を計算するためのロジックは実装されておらず、あくまで外枠のみ作成されている状況です。

#### 画面でできること(現時点)

1. プロジェクト情報(日付・名前)の入力
2. 参加者リストの作成(追加・削除、最大10人)
3. 支払い記録の入力(誰が・店名・アイテム名・金額、追加・削除)
4. 「計算開始」ボタン(※計算ロジックはこれから実装)

---

支出情報の登録はできる状態になっていますので、試しにテストデータを登録してみます。
(今回はとある遊園地に5人で出かけたシナリオでテストデータを作成してみます。)

ロジックは未実装のドラフトページにテストデータを登録

無事にテストデータの登録に成功しました。


次のステップとして、「計算開始」ボタンで割り勘計算のロジックを実装していきます。  
画面の見た目や操作感など、気になる点があれば教えてください。  
特に問題なければ、割り勘計算ロジックの実装に進みます。

上記のレスポンスの通り、Cursor側としてはこの後すぐに計算ロジックの実装に移りたいようですが、その前に画面デザインや文言を整えたいと思います。
次のプロンプトで、気になる箇所やブラッシュアップしたい点について、一気に注文を付けてみました。

デザインについて、黒ベースのもう少し近未来な感じにしてください。
1.について
・「プロジェクト情報」ではなく、「基本情報」としてください。また、「プロジェクト名」ではなく、「タイトル」としてください。
2.について
・「参加者リスト」ではなく、「参加者」としてください。
・「削除」ではなく、「×」ボタンとしてください。
・追加時は「追加」ボタンを押さなくてもEnterキーを押せば追加できるようにしてください。
・追加フォーム内のプレースホルダーは「名前」としてください。
3.について
・「支払い記録」のところは「支払情報」としてください。
・「誰が支払った?」のところは「支払者」としてください。
・フォームは縦1列に並べてください。
・Enterキーを押せば、次のフォームに移るようにしてください。
4.について
・「4.計算開始」ではなく「計算実行」としてください。
・このボタンはページの中揃えとし、目立つボタンにしてください。

上記のプロンプトにより、CSSや各種ソースファイルに修正が入り、下記のような画面が生成されました。

画面デザインや文言修正を依頼するプロンプト投入後に生成された画面

サイバー風のスタイリッシュなデザインに変わりましたね。
しかし、ちょっと目がチカチカしすぎる為、もう少しダークモード風のデザインに近くなるようにしたいと思います。

また、プルダウンメニューを操作した場合や支払情報を登録した際の挙動で想定外のものがあったり、画面の視認性や結果の明確性の向上が必要な個所があった為、それらも合わせて是正依頼を出しました。

・支払情報を1つ追加した後の挙動について、「店名」に戻りますが、「支払者」のフォームに戻るようにしてください。
・デザインについて、近未来的なのは良いのですが、もう少し色使いを抑えたものにしてください。
・画面のサイズや端末に応じて適切なサイズになるようにできますか。
・3. 支払情報の「支払者」のプレイスホルダーを「----支払者を選択----」としてください。
・1つの支払情報を入力し、追加を押下した後、「支払者」のプルダウンが開いてしまいますが、開かないようにしてほしいです。あくまで囲いの色付けをするだけにしてほしいです。
・計算結果が見づらいので、改善してほしいです。
・合計金額、一人あたりの金額、精算方法で行を分けてください。

上記のプロンプトを投入後に生成された画面は下記のとおりです。

更なる画面デザインの修正とプルダウン等の挙動修正を依頼するプロンプト投入後に生成された画面

一旦、画面デザインの修正はここまでとして、いよいよ割り勘金額を計算するロジックの実装に移りたいと思います。

ありがとうございます!  
デザインが確定しましたので、割り勘計算ロジックの実装に進みます。

---

## 割り勘計算ロジックの要件整理

- 各支払いごとに「支払者」「金額」が記録されている
- 参加者全員で均等に割り勘(支払者も含む)
- すべての支払いを合算し、各自の「立替額」と「本来支払うべき額」を計算
- 最終的に「誰が誰にいくら支払えば精算できるか」を算出

---

### 実装方針

1. 各参加者ごとの「立替合計額」を集計
2. 全体の合計金額を参加者数で割り、各自の「本来支払うべき額」を算出
3. 各自の「精算すべき差額(プラス=立替超過、マイナス=不足)」を計算
4. 差額をもとに「誰が誰にいくら払えばよいか」を最小限の送金回数で算出

---

これから`handleCalculate`関数を中心にロジックを実装します。  
まずは計算ロジックを追加し、結果を分かりやすく表示できるようにします。

// ... existing code ...
  // 割り勘計算ロジック
  const handleCalculate = () => {
    if (members.length === 0 || payments.length === 0) {
      setResult("");
      return;
    }
    // 1. 各参加者の立替合計額を集計
    const paidMap: { [name: string]: number } = {};
    members.forEach(name => { paidMap[name] = 0; });
    payments.forEach(p => {
      paidMap[p.payer] += p.amount;
    });
    // 2. 全体合計・一人あたりの負担額
    const total = payments.reduce((sum, p) => sum + p.amount, 0);
    const perPerson = total / members.length;
    // 3. 各自の差額(プラス=立替超過、マイナス=不足)
    const diffMap: { [name: string]: number } = {};
    members.forEach(name => {
      diffMap[name] = Math.round((paidMap[name] - perPerson) * 100) / 100;
    });
    // 4. 精算リスト作成
    // プラス(受け取る側)とマイナス(支払う側)で分ける
    const plus = members.filter(n => diffMap[n] > 0).map(n => ({ name: n, amount: diffMap[n] }));
    const minus = members.filter(n => diffMap[n] < 0).map(n => ({ name: n, amount: -diffMap[n] }));
    // 送金リスト
    const transactions: { from: string; to: string; amount: number }[] = [];
    let i = 0, j = 0;
    while (i < minus.length && j < plus.length) {
      const pay = Math.min(minus[i].amount, plus[j].amount);
      transactions.push({ from: minus[i].name, to: plus[j].name, amount: Math.round(pay * 100) / 100 });
      minus[i].amount -= pay;
      plus[j].amount -= pay;
      if (Math.abs(minus[i].amount) < 0.01) i++;
      if (Math.abs(plus[j].amount) < 0.01) j++;
    }
    // 結果表示
    if (transactions.length === 0) {
      setResult("すでに精算されています!");
      return;
    }
    const resultText = [
      `合計金額:${total.toLocaleString()}円` ,
      `一人あたり:${perPerson.toLocaleString(undefined, { maximumFractionDigits: 2 })}円` ,
      "\n【精算方法】",
      ...transactions.map(t => `${t.from} → ${t.to}:${t.amount.toLocaleString()}円`)
    ].join("\n");
    setResult(resultText);
  };
// ... existing code ...

割り勘計算ロジックを実装しました!

- すべての支払いを集計し、各自の立替額・本来の負担額・差額を計算
- 誰が誰にいくら支払えばよいかを最小限の送金回数で算出
- 合計金額・一人あたり金額・精算方法を分かりやすく表示

ぜひ実際に「計算実行」ボタンでご確認ください。

ご要望や表示形式の調整などがあればお知らせください。  
このままテストや微調整も可能です。

Cursorが自律的に考えて、割り勘金額を算出する為のプログラムを生成してくれました。
改めてテストデータを登録して計算ロジックの検証をしてみましょう。
あらかじめ、参加者リスト(山田、島本、本庄、中島、田中)を作成しておき、誰が何にいくら支払ったのかを登録します。

割勘計算ロジックの実装が完了したアプリにテストデータを登録

計算実行ボタンを押下し、下記のような計算結果が表示されました。
(結果の表示のされ方がよろしくないですが、この修正は後ほど対応しましょう。)

登録したデータをもとに表示された割り勘の計算結果

次に計算結果に問題がないかどうか、自分の力で計算をしてみましょう。
最初に、各々が立て替えた金額の合計が正しいかどうかを確かめます。

全員の立替額合計の算出結果

上記は問題なさそうですね。
そこから、一人当たりがいくら支払った状態にすればよいかを計算します。

■79,500÷5=15,900

こちらの計算結果も問題ありません。
ここから、算出された結果をもとに全員が¥15,900円支払った状態にあるかを確かめます。

精算後の各々の支払額

上記についても問題ないことが確認できました。
本来であれば、例として下記のようなテストケースも試すべきですが、今回は割愛とさせていただきます。

  • 何も支払わなかったメンバーがいる場合
  • 精算前の一人当たりの金額の多さに偏りが少ない場合
  • 誰かが2回以上の場面で立て替えをした場合
    など

最後のステップとしてデザインの最終調整とロゴを作成します。
割り勘金額の計算結果の出力方法が見づらかった為、下記のようなプロンプトを実行し、是正しました。

計算結果が見づらいので、改善してほしいです。
合計金額、一人あたりの金額、精算方法で行を分けてください。

これにより、下記のような出力に改善され、計算結果が格段に見やすくなりました。

是正された計算結果の出力画面

次にロゴの作成です。
現状は「割り勘アプリ」の文字列を表示しただけとなっています。

ロゴ作成前の文字列のみのタイトル

今回はアプリに製品名を命名した上でロゴを作成し、より本格感を出したいと思います。
アプリの名前は「“割り勘”が”感動”するほど簡単にできる”場所”」という思いを込めて、「割勘堂」にしたいと思います。

ここからは、Google Geminiに依頼をしてロゴマークを作ってもらいます。
色々とリクエストを付けながら、最終的にブルーを基調としたモダンなデザインで、お堂とお札をスプリットしている画像をそれぞれ生成してもらい、それらをこちらで合成しました。

最終的な「割勘堂」のロゴデザイン

さらに透明化や文字の追加も行い、最終的に上記のようなデザインとなりました。 
できあがったロゴをアプリに実装した結果は下記の通りです。

ロゴ実装後のアプリの画面デザイン

以上でCursorを用いたアプリ開発は終了とします。

私自身、Webアプリ開発の経験がないところからスタートしましたが、AIを活用することで「ある程度のレベル」までは驚くほどスムーズに実現できました。

もちろん、AIが生成するコードはまだ完璧ではなく、内容の不安定さや誤りも散見されます。だからこそ、最終的な生成結果を検証し、正しく判断するという人間の役割の重要性は、むしろ増していると言えるでしょう。

AIをパートナーとして活用し、その能力を最大限に引き出すためには、我々エンジニア自身が、より高度なスキルと知識を身につけ、常に学び続ける姿勢が不可欠であることが大切であると改めて考えさせられました。