Skip to content

Comments

【永井】 frontend_challenge_2 電気料金シュミレーションフォーム実装#75

Open
tsbs0514 wants to merge 29 commits intoenechange:masterfrom
tsbs0514:master
Open

【永井】 frontend_challenge_2 電気料金シュミレーションフォーム実装#75
tsbs0514 wants to merge 29 commits intoenechange:masterfrom
tsbs0514:master

Conversation

@tsbs0514
Copy link

@tsbs0514 tsbs0514 commented Aug 7, 2025

電気料金シミュレーションアプリケーションの実装

概要

課題として開発した電気料金シミュレーションアプリケーションです。
ユーザーが郵便番号と電気使用状況を入力することで、電気料金のシミュレーション結果を提示します。

デモは GitHub Pages で確認できます。

主な機能

  • 郵便番号による電力エリア判定: 入力された郵便番号から東京電力または関西電力エリアを判定
  • エリアに応じた電力会社・プランの動的選択
  • リアルタイムバリデーション
  • レスポンシブデザイン

技術スタック

カテゴリ 使用技術
Framework Next.js 15.4.5 (App Router)
Language TypeScript
Styling Tailwind CSS
Form React Hook Form + Zod
Testing Jest / React Testing Library / Playwright
API Mock MSW
Deploy GitHub Pages

デザイン提案

  • selectのUIの矢印アイコンの位置を左から右に変更。見慣れた配置にすることでユーザーの戸惑いを防ぐ
  • フォームの項目を入力段階に応じて徐々に出す仕様を追加。まだ入力できない項目を選ぼうとするユーザーの誤操作や、フォームの項目が少なく見えるので心理的ハードルも下げる
  • コントラスト比の関係からエラーメッセージの文字をboldに

使用ライブラリと選定理由

コアライブラリ

Next.js 15.4.5

  • 選定理由:
    • このプロジェクトでの活用: App Router による静的生成で高速な初期表示を実現。
    • 将来性: 静的生成によるSEO最適化とパフォーマンスの維持
    • 開発効率: Turbopackによる高速な開発サーバーで開発を効率化

React 19.1.0

  • 選定理由:
    • このプロジェクトでの活用: コンポーネント指向により、将来的に発生しうる複雑なUIを効率的に構築・管理
    • 将来性: パフォーマンスを維持しつつ、UIの拡張性を確保
    • 開発効率: 豊富なエコシステムと開発者コミュニティにより、問題解決や機能追加を迅速に実施

TypeScript 5.x

  • 選定理由:
    • このプロジェクトでの活用: 電力エリア判定ロジックの型安全性確保。
    • 将来性: 新しい電力会社やプランの追加時に、型チェックによる安全性を確保
    • 開発効率: フォーム状態管理の型推論により、開発速度を向上

フォーム管理

React Hook Form 7.62.0

  • 選定理由:
    • このプロジェクトでの活用: 郵便番号入力時のリアルタイムバリデーションと、電力会社選択による動的フィールド表示を高パフォーマンスで実現
    • 将来性: 新しい入力項目追加時に、再レンダリングを最小化
    • ユーザー体験: フォーム送信時の即座なフィードバックと、エラー状態の適切な管理

Zod 4.0.14

  • 選定理由:
    • このプロジェクトでの活用: 郵便番号の正規表現チェック、電気代の最小値(1000 円)バリデーション、プラン別の契約容量必須チェックを型安全に実装
    • 将来性: 新しい電力会社やプランの追加時に、スキーマ定義による一貫したバリデーション
    • 保守性: 複雑な条件付きバリデーション(関西電力従量電灯 A は契約容量不要など)を明確に定義

@hookform/resolvers 5.2.1

  • 選定理由:
    • このプロジェクトでの活用: React Hook Form と Zod の統合により、型安全なフォームバリデーションを実現
    • 将来性: 新しいバリデーションルール追加時の統合の簡素化
    • 開発効率: 複雑な条件付きバリデーションの実装を効率化

スタイリング

Tailwind CSS 4.x

  • 選定理由:
    • このプロジェクトでの活用: 課題期間内に UI を素早く形にできる。
    • 将来性: 新しいデザインパターンや入力項目を追加しても、JSX の差分が見た目の差分となるのでわかりやすい。またグローバルCSSを触らないため、既存ページへのリスクが低い。
    • 開発効率: 事前にデザインシステムを整備しなくても、ユーティリティクラスだけ一気に形にできる

clsx 2.1.1

  • 選定理由:
    • このプロジェクトでの活用: エラー状態に応じたフォームフィールドのスタイル変更を動的に管理
    • 将来性: 複雑な条件付きスタイリングの管理を効率化

tailwind-merge 3.3.1

  • 選定理由:
    • このプロジェクトでの活用: Tailwind CSS のクラス名が重複した場合に、競合を解決し、意図したスタイルを適用。これにより、コンポーネントの再利用性を高めつつ、柔軟なスタイリングを実現
    • 将来性: 新しい UI コンポーネントやデザイン変更時にも、スタイル競合を気にせず効率的に開発
    • 保守性: クラス名の管理を簡素化し、予期せぬスタイル崩れを防止

テスト

Jest 30.0.5

  • 選定理由:
    • このプロジェクトでの活用: フォームロジック(エリア判定、電力会社選択、プラン選択)の単体テストを高速実行
    • 将来性: 新しい電力会社やプランの追加時のテスト自動化
    • 品質保証: 76 個のテストケースで主要ロジックの動作を保証

React Testing Library 16.3.0

  • 選定理由:
    • このプロジェクトでの活用: ユーザーの実際の操作(郵便番号入力 → 電力会社選択 → プラン選択)をテスト
    • 将来性: 新しい UI 機能追加時のユーザー中心テストも容易
    • 品質保証: アクセシビリティを考慮したテストで、幅広いユーザーに対応

Playwright 1.54.2

  • 選定理由:
    • このプロジェクトでの活用: 郵便番号入力からシミュレーション完了までの E2E フローを複数ブラウザでテスト
    • 将来性: 新しい電力会社や地域追加時の E2E テスト自動化
    • 品質保証: 実際のユーザー体験を保証

API モック

MSW 2.10.4

  • 選定理由:
    • このプロジェクトでの活用: 郵便番号によるエリア判定 API をモックし、開発・テスト環境での一貫性を確保
    • 将来性: 実際の API 実装時の段階的移行を可能
    • 開発効率: バックエンド開発に依存しないフロントエンド開発を実現

開発ツール

ESLint 9.x

  • 選定理由:
    • このプロジェクトでの活用: 複雑なフォームロジックのコード品質を維持
    • 将来性: チーム開発時の一貫したコーディングスタイル
    • 品質保証: 早期のバグ発見とコードの可読性向上

ts-node 10.9.2

  • 選定理由:
    • このプロジェクトでの活用: Jest 設定での TypeScript 対応により、型安全なテスト環境を構築
    • 将来性: 新しいテストケース追加時の TypeScript 活用
    • 開発効率: テスト環境での型チェックによる開発速度向上

その他

react-icons 5.5.0

  • 選定理由:
    • このプロジェクトでの活用: フォーム送信ボタンのアイコン表示で UX を向上
    • 将来性: 新しい UI 要素追加時の豊富なアイコン選択肢
    • 一貫性: 統一されたアイコンデザインシステム

設計方針

  • フォームバリデーション: RHF + Zod でリアルタイムかつ型安全
  • ロジック分離: useElectricForm フックで状態管理を集中
  • テスト戦略: 単体+E2E で主要フローを網羅
  • API モック: MSW で一貫したレスポンス・エラーハンドリング

セットアップ

# 依存関係
npm install

# 開発サーバー
npm run dev
# → http://localhost:3000

).toBeVisible();

await expect(page.getByLabel("電力会社")).toBeVisible();
await page.selectOption('select[name="powerCompany"]', "tokyo-electric");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

想定される選択肢として '東京電力' と 'その他' のみになっていることを確認するテストもあると、品質が高まり安心かと思います。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!
こちらで東京、関西それぞれoptionをチャックするテストを追加いたしました!
35597b0

area = "out-of-service";
isValid = false;
message = "サービスエリア対象外です。";
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エリアが今後増えることを考えると、else if を連ねるよりも、以下のように Record でマッピングしておく方が、可読性・保守性が高くなりそうです。

const areaMap: Record<string, PowerArea> = {
  "1": "tokyo",
  "5": "kansai",
};

const firstDigit = postalCode.charAt(0);
const area = areaMap[firstDigit] ?? "out-of-service";
const isValid = area !== "out-of-service";
const message = isValid ? "" : "サービスエリア対象外です。";

let を使うと変数の最終的な値を追うのが難しくなるため、できる限り const を使って、意図が明確な実装にすると良いと思います。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

いただいたコード参考にリファクタいたしました!
7ee5f16

{/* プラン */}
{watchedPowerCompany && !companyError && (
<FormField label="プラン" labelHtmlFor="plan" required>
<Select
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

指定されたデザインのように、選択肢の説明文を実装する場合は、どのように実装する想定でしょうか?
もし設計方針などがあれば共有いただけると助かります。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

すいません、こちら完全に漏れておりました。。。
下記で機能追加いたしました!
170782a

<div className={cn("bg-gray-200 p-1 rounded-sm", className)}>
<input
type={type}
className={cn(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input と Select の padding に差異があるようなので、デザイン上の一貫性を保つためにも揃えておくと良さそうです!

image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

デザイン上でSelectとInputのUIの高さに差異があったので、あえて差をつけていましたが、
おっしゃる通り一貫性保つ視点で考えると揃えた方が良いですね!
こちら対応いたしました!
26a7b4a

const getAvailableCapacities = useCallback(() => {
if (!watchedPlan) return [];

switch (watchedPlan) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらの switch 文についても、下記のように 選択肢の配列を定数として事前に定義し、
Record を使ってマッピングする形にリファクタしておくと、可読性・保守性が高まると思いました。

type CapacityOption = { value: ContractCapacity; label: string };

const tokyoJuryouBOptions: CapacityOption[] = [
  "10A", "15A", "20A", "30A", "40A", "50A", "60A",
].map((amp) => ({ value: amp as const, label: amp }));

const juryouCKansaiOptions: CapacityOption[] = Array.from({ length: 44 }, (_, i) => {
  const kva = i + 6;
  const label = `${kva}kVA`;
  return { value: label as ContractCapacity, label };
});

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

おっしゃる通りですね。。。!アドバイス基にリファクタしたらかなりスッキリしました!
勉強になります🙇
4d6c6e4

@kenji7157
Copy link
Member

@tsbs0514
指定されたデザインに近い形で丁寧にチャレンジに取り組んでいただき、ありがとうございます!

いくつかコメントをさせていただきましたので、無理のない範囲でご対応いただければと思います、よろしくお願いいたします!

Comment on lines 75 to 76
`電力会社: ${data.powerCompany}\n` +
`プラン: ${data.plan}\n` +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

結果画面の実装までしていただきありがとうございます!

一点だけコメントさせてください 🙇
電力会社やプランの表示について、現在は tokyo-electric のようなコードがそのまま表示されているようですので、東京電力や関西電力など、正式名称に変換して表示するとより分かりやすくなりそうです!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

おっしゃる通りですね、、、確かにわかりづらいです💦
下記で定数モジュールを追加した際に修正いたしました!
f0ae55b

Comment on lines 169 to 173
// プラン変更時に契約容量をリセット
useEffect(() => {
resetField("contractCapacity");
clearErrors("contractCapacity");
}, [watchedPlan, resetField, clearErrors]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

プラン変更時に契約容量がリセットされますが、仕様通りとはいえ画面下部の結果確認ボタンが非活性のままになるため、ユーザーがエラーや未入力に気付きにくい可能性があります。

プラン変更後は、エラー表示などで再入力が必要であることを明示すると良さそうです!

2025-08-08.12.28.24.mov

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

おっしゃる通りですね!
下記で対応してみました!
47c1a1c#diff-23c2d9f0d06c5634220a6ad1b77122bc33e918102e28819a85752c5ee1caf2ddR44-R73

Comment on lines 35 to 36
const watchedPowerCompany = watch("powerCompany");
const watchedPlan = watch("plan");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ask

電力会社とプランに関しては、ElectricSimulationForm.tsxでも監視されていますが、ビジネスロジックとUI表示ロジックで分離させているなど、意図的なものでしょうか?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

おっしゃる通り、ビジネスロジックとUI表示ロジックで分ける意図で監視のwatchをそれぞれ書いておりましたが、冗長だと思ったので、機能をまるっとhooksの方に下記でまとめました!
d296142

ただその結果、一つのhooksが機能を持ちすぎて保守性が悪くなると思ったので下記で機能を細分化してみました!
47c1a1c

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほどですね、意図的であること承知しました。
また、細分化して整理までしていただきありがとうございます!


return currentArea === "tokyo"
? [
{ value: "tokyo-electric" as const, label: "東京電力" },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo

型安全性は担保されているものの、選択肢の value やラベルを定数として切り出して一元管理すると、追加・変更に強く、再利用もしやすくなりそうですね!

プランや契約容量についても同様の対応ができそうです。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

おっしゃる通り、ここの詰めが甘かったです。。。
下記で定数モジュールを追加し、全体的にリファクタいたしました!
f0ae55b

description:
"現在の電気料金からどのくらいお得になるかチェック!郵便番号を入力するだけで簡単にシミュレーションできます。",
icons: {
icon: "https://enechange.jp/favicon.ico",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ファビコンが enechange のものに指定されていていいですね!細かいところまで実装いただきありがとうございます!

Comment on lines +4 to +8
/**
* 郵便番号からエリア判定
* 静的エクスポート環境ではクライアントサイドで判定
*/
export async function checkArea(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

開発環境と本番環境で挙動を分ける実装にしていただきありがとうございます!
実際の運用環境を想定されている点、素晴らしいなと思いました 👍

@ToruShimizu
Copy link
Collaborator

ToruShimizu commented Aug 8, 2025

@tsbs0514

お忙しい中、早急にご対応いただきありがとうございました!

テストも充実しており、安心してレビューや動作確認を行うことができました。(実際に不具合等も見つかりませんでした 👍)

また、ユーザーの操作性や心理面まで意識し、細部にまで配慮されたデザインのご提案もありがとうございます。
私自身も勉強になりました!

コメントへのご対応やご返信は、無理のない範囲で大丈夫です 🙇
(世間的にはお盆休みの時期ですので、本当にご無理のないようお願いします。)

引き続き、どうぞよろしくお願いいたします。

@tsbs0514
Copy link
Author

tsbs0514 commented Aug 8, 2025

@ToruShimizu @kenji7157
お忙しいところ、早速のレビューをありがとうございます。大変勉強になるものばかりです!
いただいたコメントについては、順次対応してまいります!
引き続き、どうぞよろしくお願いいたします。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants