0
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?

Nurse Calc | Next.js App Router × Zod × RHFで作る、型安全な医療計算ツール設計

0
Last updated at Posted at 2026-04-09

📘 はじめに

臨床看護師歴10年以上、現在はWebフロントエンドエンジニアも兼業しています。

看護業務で日常的に行う

  • 投薬計算
  • 点滴速度
  • 電解質補正

といった処理を、型安全かつ保守しやすい形でまとめたWebアプリを開発しました。

デモ:こちらからお試しできます

コード:Github🐈

本記事では、実装の中でも特に重要だった

  • アーキテクチャ設計
  • 型安全なバリデーション
  • 再利用設計

について技術的にお話します。

利用上の注意

本アプリは個人開発による参考ツールです。
実際の医療現場で使用する際は、必ず公式な資料や医師の指示をご確認ください。

安全性を担保するためにも、
本アプリの結果のみで判断しないようお願いいたします。

⚙️ 技術スタック

分類 採用技術
フレームワーク Next.js(App Router)
言語 TypeScript(strict)
フォーム管理 React Hook Form v7
バリデーション Zod v3
スタイリング Tailwind CSS
DnD @dnd-kit
永続化 localStorage
デプロイ Vercel
PWA Service Worker

⚙️ アーキテクチャ概要

9種類の計算機はすべて同一パターンで構築しています。

src/
├── app/
│   ├── _components/
│   ├── [calculator]/
│   │   ├── page.tsx
│   │   ├── Calculator.tsx
│   │   └── schema.ts
├── hooks/
│   └── useCalculator.ts
├── lib/
│   ├── history.ts
│   └── reuse/
└── types/

🧩 1. useCalculator:計算処理の共通化

解決した課題

各計算機で共通して発生する処理:

  • useForm初期化
  • バリデーション設定
  • 履歴復元
  • result管理
  • スクロール制御

→ カスタムフックに集約

export function useCalculator<TInputs, TResult = string>(
  schema: ZodTypeAny,
  typeId: string,
) {
  const [result, setResult] = useState<TResult | null>(null);

  const form = useForm<TInputs>({
    resolver: zodResolver(schema),
    shouldUnregister: false,
  });

  useEffect(() => {
    const data = getTypedReusePayloadOnce(typeId, schema);
    if (data) form.reset(data);
  }, []);

  return { result, setResult, ...form };
}

メリット

  • ボイラープレート削減
  • 型安全なresult管理
  • 全計算機で統一された挙動

🧠 2. Zodによる型安全バリデーション

空文字問題の対策

export const prep = (val: unknown) =>
  val === "" ? undefined : val;
z.preprocess(
  prep,
  z.coerce.number().min(1)
);

→ "" → undefined に変換し、エラーを正しく発火

クロスフィールド検証

.refine((data) => data.targetHb > data.currentHb, {
  message: "目標Hbは現在Hbより高く設定してください",
  path: ["targetHb"],
});

動的enum生成

const deviceIds = oxygenDevices.map(d => d.id);
z.enum(deviceIds as [string, ...string[]]);

→ 定数とスキーマの二重管理を防止

🔁 3. 履歴→再利用フロー(型保証あり)

設計

履歴保存
→ localStorage
→ 別ページで読み込み
→ Zodで検証
→ form.reset

ポイント

  • 外枠スキーマ(JSON構造)
  • 内枠スキーマ(入力データ)
  • 不正データは破棄
const outer = _validateOuter(raw);
if (!outer) return null;

const result = schema.safeParse(outer.inputs);
if (!result.success) return null;

🧱 4. SSR × localStorage 対応

Next.jsではSSR中に localStorage が使えない

対策

if (typeof window === "undefined") return;
useEffect(() => {
  if (typeof window === "undefined") return;
}, []);

🎯 5. DnD × モバイル対応

センサー設定

useSensor(PointerSensor, {
  activationConstraint: { delay: 180 },
});

touch-action問題

  • touch-action: none → スクロール不可
  • DnDとスクロールが競合

解決

  • DnDラッパーに touch-none
  • ボタンは touch-manipulation

📱 6. モバイルUI設計

レスポンシブ

{/* モバイル */}
<div className="overflow-x-auto flex" />

{/* PC */}
<div className="grid grid-cols-4" />

ナイトモード

useLayoutEffect(() => {
  if (typeof window === "undefined") return;
}, []);

🚀 7. PWA設計

採用理由

  • インストール不要
  • 即利用可能
  • 更新が容易

トレードオフ

  • 初回通信は必要

🧠 設計まとめ

項目 選択
バリデーション Zod
フォーム React Hook Form
永続化 localStorage
UI Tailwind
DnD dnd-kit

🧠 まとめ

  • useCalculatorで共通化
  • Zodで型安全なバリデーション
  • SSR対応で安全なデータアクセス
  • DnDとモバイル操作の両立

医療領域のようにミスが許されないドメインでは、
型安全性とバリデーション設計が非常に重要だと実感しました。

一方で、実際の医療現場での活用を考えると、

  • 電子カルテ連携
  • 個人情報・セキュリティ
  • 院内システムとの整合性

といった課題もあり、個人開発だけでは難しい領域も多く存在します。

そのため今後は、

医療DXに関わる方々と対話しながら、現場で使える形へ発展させていきたい

と考えています!


📘 詳細なコード解説はZennで公開中👇
👉 【看護師エンジニアが、現場の「あれがほしい」をNext.jsで形にした話】


0
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
0
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?