100本ノック / システム開発 / システム開発100本ノック
製造業向けReact入門|一覧・フォーム・API連携を実務データで学ぶ10本ノック
製造現場の判断を止めない業務画面設計 — Reactフロントエンド実践10本ノック
製造現場向けの作業指示・品質確認アプリを題材に、Reactの基本構成からコンポーネント設計、一覧・詳細・入力フォーム、API連携、検索、ローディング・エラー表示までを一続きで整理します。単に画面を作るのではなく、作業者・班長・品質保証担当が「次に何をすべきか」を迷わず判断できることを目標にします。
本記事では外部データを利用せず、Pythonで生成した架空の製造データを用いて、画面設計に関係する件数、入力エラー率、API応答時間、業務完了率などを可視化します。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
はじめに:この記事で扱う製造業の実務課題
製造現場の業務画面は、一般的な情報閲覧サイトとは異なります。利用者は手袋を着けたままタブレットを操作することがあり、設備停止や品質異常への対応中には数秒の迷いも損失につながります。さらに、画面に表示される情報は作業指示、設備、品番、品質判定、担当者など複数の業務データにまたがります。
今回の実務課題は、次の3点です。
- 優先度の高い作業指示を短時間で見つけられること
- 実績入力の漏れや不整合を登録前に防げること
- APIの遅延や障害があっても、利用者が状況と次の行動を理解できること
Reactは画面を部品に分け、状態変化に応じて表示を更新するためのライブラリです。ただし、Reactを採用しただけで使いやすい業務システムになるわけではありません。コンポーネント境界・状態・API・例外時の体験を、業務ルールと対応づけることが重要です。
現場でよくある状況
- 一覧に列を追加し続けた結果、重要な異常や納期遅延が埋もれる
- 一覧と詳細でステータスの表示規則が異なり、利用者が判断に迷う
- 入力値を送信した後にサーバーからエラーが返り、再入力が発生する
- API通信中に画面が無反応に見え、利用者がボタンを連打する
- 「エラーが発生しました」だけが表示され、再試行すべきか担当部署へ連絡すべきか分からない
これらは見た目だけの問題ではありません。対応遅れ、誤投入、二重登録、問い合わせ増加を通じて、納期・品質・稼働率へ影響します。
なぜこの問題は判断が難しいのか
画面の良し悪しは、表示速度だけでは評価できません。例えば列を減らすと一覧性は上がりますが、判断材料が不足する可能性があります。厳しい入力チェックは品質を高めますが、現場の例外を許容できないと業務が止まります。
そこで、技術指標と業務指標を結びつけます。
| 設計対象 | 技術指標 | 業務指標 |
|---|---|---|
| 一覧・検索 | 表示件数、絞り込み件数 | 対象発見時間、見落とし率 |
| 入力フォーム | エラー項目数、再入力回数 | 登録完了率、入力時間 |
| API連携 | 応答時間、失敗率 | 待ち時間、二重操作率 |
| 状態表示 | ローディング・エラーの識別 | 離脱率、問い合わせ率 |
今回扱うノックの全体像
| No. | テーマ | 工場作業指示アプリでの確認点 |
|---|---|---|
| 041 | Reactの基本構成 | 画面・状態・処理の役割分担 |
| 042 | 画面コンポーネント | 再利用単位と変更影響範囲 |
| 043 | 一覧画面 | 優先作業の俯瞰 |
| 044 | 詳細画面 | 1件の判断根拠と履歴 |
| 045 | 入力フォーム | 入力項目と初期値 |
| 046 | バリデーション | 不整合の登録前検知 |
| 047 | API呼び出し | 応答時間と失敗率 |
| 048 | レスポンス表示 | APIデータから表示項目への変換 |
| 049 | 検索・絞り込み | 異常・遅延対象の発見 |
| 050 | ローディング・エラー | 待機・再試行・連絡の案内 |
10本を通して、App → Page → 業務コンポーネント → APIという責務の流れを作り、最後に運用KPIまで確認します。
Python環境の準備
pandasで表形式データを扱い、numpyで乱数を生成し、matplotlibで可視化します。日本語ラベル表示にはjapanize_matplotlibを利用します。乱数シードは固定し、再実行しても同じ結果になるようにします。
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import display
SEED = 20260711
rng = np.random.default_rng(SEED)
print(f"Python : {sys.version.split()[0]}")
print(f"numpy : {np.__version__}")
print(f"pandas : {pd.__version__}")
print(f"matplotlib : {matplotlib.__version__}")
print(f"random seed: {SEED}")
Python : 3.13.1
numpy : 2.5.1
pandas : 3.0.3
matplotlib : 3.11.0
random seed: 20260711
架空データの作成
3つの製造ラインで発行された360件の作業指示を作成します。各レコードは予定日時、品番、数量、進捗、優先度、不良率、設備状態を持ちます。加えて、APIアクセス800件と実績入力320件を生成します。
データ生成時点では、異常停止中の設備、納期超過、入力値の不整合などを意図的に含めます。これは画面が正常系だけでなく、現場で判断が必要になる状態を扱えるか確認するためです。
n_orders = 360
base_time = pd.Timestamp("2026-06-01 08:00")
orders = pd.DataFrame({
"order_id": [f"WO-{i:04d}" for i in range(1, n_orders + 1)],
"line": rng.choice(["第1ライン", "第2ライン", "第3ライン"], n_orders, p=[0.38, 0.34, 0.28]),
"product": rng.choice(["AX-100", "AX-200", "BZ-110", "CZ-300"], n_orders),
"planned_at": base_time + pd.to_timedelta(rng.integers(0, 30 * 24, n_orders), unit="h"),
"planned_qty": rng.integers(80, 501, n_orders),
"progress_pct": np.clip(rng.normal(72, 28, n_orders), 0, 100).round(0),
"priority": rng.choice(["高", "中", "低"], n_orders, p=[0.18, 0.52, 0.30]),
"defect_rate_pct": np.clip(rng.gamma(1.6, 0.55, n_orders), 0, 5).round(2),
"machine_status": rng.choice(["稼働", "段取", "点検", "異常停止"], n_orders, p=[0.70, 0.13, 0.11, 0.06]),
})
orders["status"] = np.select(
[orders["progress_pct"].eq(100), orders["progress_pct"].gt(0)],
["完了", "進行中"],
default="未着手",
)
snapshot_at = pd.Timestamp("2026-06-22 08:00")
orders["overdue"] = (orders["planned_at"] < snapshot_at) & (orders["status"] != "完了")
orders["needs_attention"] = (
orders["overdue"]
| (orders["defect_rate_pct"] >= 1.5)
| orders["machine_status"].eq("異常停止")
)
n_api = 800
api_logs = pd.DataFrame({
"endpoint": rng.choice(["GET /orders", "GET /orders/:id", "POST /results"], n_api, p=[0.48, 0.32, 0.20]),
"latency_ms": np.clip(rng.lognormal(np.log(420), 0.62, n_api), 80, 5000).round(0),
})
api_logs["failed"] = rng.random(n_api) < np.where(api_logs["latency_ms"] > 1500, 0.16, 0.025)
n_forms = 320
forms = pd.DataFrame({
"good_qty": rng.integers(60, 481, n_forms),
"defect_qty": rng.integers(0, 18, n_forms),
"planned_qty": rng.integers(80, 501, n_forms),
"temperature_c": rng.normal(182, 9, n_forms).round(1),
"operator_entered": rng.random(n_forms) > 0.035,
"reason_entered": rng.random(n_forms) > 0.42,
})
forms.loc[rng.choice(n_forms, 18, replace=False), "good_qty"] *= -1
forms["qty_invalid"] = (forms["good_qty"] < 0) | ((forms["good_qty"] + forms["defect_qty"]) > forms["planned_qty"])
forms["temperature_invalid"] = ~forms["temperature_c"].between(165, 200)
forms["reason_invalid"] = (forms["defect_qty"] >= 10) & ~forms["reason_entered"]
forms["operator_invalid"] = ~forms["operator_entered"]
forms["is_valid"] = ~forms[["qty_invalid", "temperature_invalid", "reason_invalid", "operator_invalid"]].any(axis=1)
print(f"作業指示: {len(orders):,}件 / 要注意: {orders['needs_attention'].sum():,}件")
print(f"APIログ: {len(api_logs):,}件 / 失敗: {api_logs['failed'].sum():,}件")
print(f"実績入力: {len(forms):,}件 / 入力不整合あり: {(~forms['is_valid']).sum():,}件")
display(orders.head(5))
作業指示: 360件 / 要注意: 226件
APIログ: 800件 / 失敗: 23件
実績入力: 320件 / 入力不整合あり: 209件
| order_id | line | product | planned_at | planned_qty | progress_pct | priority | defect_rate_pct | machine_status | status | overdue | needs_attention | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WO-0001 | 第1ライン | CZ-300 | 2026-06-13 04:00:00 | 239 | 73.0 | 中 | 0.23 | 稼働 | 進行中 | True | True |
| 1 | WO-0002 | 第3ライン | BZ-110 | 2026-06-10 13:00:00 | 155 | 56.0 | 低 | 0.33 | 稼働 | 進行中 | True | True |
| 2 | WO-0003 | 第2ライン | CZ-300 | 2026-06-12 20:00:00 | 470 | 60.0 | 中 | 0.55 | 稼働 | 進行中 | True | True |
| 3 | WO-0004 | 第2ライン | BZ-110 | 2026-06-08 07:00:00 | 415 | 19.0 | 中 | 1.94 | 稼働 | 進行中 | True | True |
| 4 | WO-0005 | 第3ライン | CZ-300 | 2026-06-09 11:00:00 | 137 | 78.0 | 中 | 0.57 | 稼働 | 進行中 | True | True |
No.041:Reactの基本構成を理解する
実務での意味
Reactでは、画面をコンポーネントという単位に分け、データや利用者操作による状態変化を表示へ反映します。製造業務では、検索条件、選択中の作業指示、入力途中の実績、API通信状態など、変化の頻度と影響範囲が異なる状態を扱います。
最上位のAppへすべての状態を集めると、小さな入力変更でも広い範囲が再評価され、改修時の影響も追いにくくなります。複数画面で共有する状態だけを上位へ置き、入力途中の値など局所的な状態は利用する画面の近くへ置くのが基本です。
分析・モデル化の考え方
状態 の変更頻度を 、影響を受けるコンポーネント数を とすると、設計比較用の単純な負荷指標を
と置けます。これは実測性能ではなく、更新波及と保守影響を比較するための指標です。状態を適切な階層へ配置した場合と、すべてAppへ配置した場合を比較します。
Reactでは、画面固有の状態を利用箇所の近くに置きます。
function OrderListPage() {
const [filters, setFilters] = useState({ line: "all", attentionOnly: false });
const [selectedOrderId, setSelectedOrderId] = useState(null);
return <OrderTable filters={filters} onSelect={setSelectedOrderId} />;
}
Pythonで確認する
state_design = pd.DataFrame({
"状態": ["ログイン利用者", "検索条件", "選択中の指示", "実績入力値", "API通信状態"],
"変更回数/時": [1, 24, 18, 120, 54],
"適切配置の影響数": [8, 3, 3, 2, 2],
"全てApp配置の影響数": [8, 8, 8, 8, 8],
})
state_design["適切配置の負荷"] = state_design["変更回数/時"] * state_design["適切配置の影響数"]
state_design["App集中の負荷"] = state_design["変更回数/時"] * state_design["全てApp配置の影響数"]
display(state_design)
load = state_design[["適切配置の負荷", "App集中の負荷"]].sum()
ax = load.plot(kind="bar", color=["#2E86AB", "#D1495B"], figsize=(7, 4))
ax.set_title("状態配置による更新波及負荷の比較")
ax.set_xlabel("状態管理の設計")
ax.set_ylabel("比較用負荷スコア")
ax.grid(axis="y", alpha=0.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
print(f"適切な配置による負荷削減率: {1 - load.iloc[0] / load.iloc[1]:.1%}")
| 状態 | 変更回数/時 | 適切配置の影響数 | 全てApp配置の影響数 | 適切配置の負荷 | App集中の負荷 | |
|---|---|---|---|---|---|---|
| 0 | ログイン利用者 | 1 | 8 | 8 | 8 | 8 |
| 1 | 検索条件 | 24 | 3 | 8 | 72 | 192 |
| 2 | 選択中の指示 | 18 | 3 | 8 | 54 | 144 |
| 3 | 実績入力値 | 120 | 2 | 8 | 240 | 960 |
| 4 | API通信状態 | 54 | 2 | 8 | 108 | 432 |
適切な配置による負荷削減率: 72.2%
結果の読み取り
入力値や通信状態のように頻繁に変わる状態ほど、必要な範囲へ閉じ込める効果が大きくなります。実務では、負荷スコアだけで設計を決めず、複数画面で値を共有する必要性、ブラウザ更新後も保持する必要性、監査ログへ残す必要性も合わせて判断します。
No.042:画面コンポーネントを作成する
実務での意味
業務画面をStatusBadge、OrderTable、OrderDetail、ResultFormなどへ分割すると、ステータス色や表示書式を統一できます。一方、細かく分けすぎるとデータ受け渡しが増え、変更箇所を追いにくくなります。
分析・モデル化の考え方
コンポーネントの候補は、業務上の責務が一つか、複数画面で再利用されるか、単独でテストできるかで評価します。ここでは責務数、利用画面数、受け取る値(props)の数を棚卸しし、責務数またはprops数が大きい部品を分割候補とします。
function StatusBadge({ status }) {
const label = { running: "稼働", stopped: "異常停止" }[status] ?? "未確認";
return <span className={`status status--${status}`}>{label}</span>;
}
Pythonで確認する
components = pd.DataFrame({
"component": ["App", "OrderListPage", "FilterPanel", "OrderTable", "StatusBadge", "OrderDetail", "ResultForm", "ErrorPanel"],
"責務数": [2, 3, 2, 2, 1, 3, 4, 1],
"利用画面数": [1, 1, 1, 2, 4, 1, 1, 3],
"props数": [2, 5, 6, 7, 2, 8, 11, 3],
})
components["分割レビュー"] = (components["責務数"] >= 4) | (components["props数"] >= 10)
display(components)
fig, ax = plt.subplots(figsize=(8, 5))
colors = np.where(components["分割レビュー"], "#D1495B", "#2E86AB")
ax.scatter(components["props数"], components["責務数"], s=components["利用画面数"] * 140, c=colors, alpha=0.8)
for _, row in components.iterrows():
ax.annotate(row["component"], (row["props数"], row["責務数"]), xytext=(4, 4), textcoords="offset points", fontsize=8)
ax.set_title("コンポーネント責務とデータ受け渡しの棚卸し")
ax.set_xlabel("props数")
ax.set_ylabel("責務数")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
| component | 責務数 | 利用画面数 | props数 | 分割レビュー | |
|---|---|---|---|---|---|
| 0 | App | 2 | 1 | 2 | False |
| 1 | OrderListPage | 3 | 1 | 5 | False |
| 2 | FilterPanel | 2 | 1 | 6 | False |
| 3 | OrderTable | 2 | 2 | 7 | False |
| 4 | StatusBadge | 1 | 4 | 2 | False |
| 5 | OrderDetail | 3 | 1 | 8 | False |
| 6 | ResultForm | 4 | 1 | 11 | True |
| 7 | ErrorPanel | 1 | 3 | 3 | False |
結果の読み取り
ResultFormは責務数とprops数がともに大きく、入力欄、検証メッセージ、送信操作へ分けるレビュー候補です。一方、複数画面で利用するStatusBadgeは小さく保つ価値があります。この表は機械的な合否判定ではなく、設計レビューで「なぜこの境界なのか」を説明する材料として使います。
No.043:一覧画面を作成する
実務での意味
一覧画面の目的は、データをすべて見せることではなく、担当者が対応順を決められることです。作業指示一覧では、ライン、予定日時、進捗、優先度、設備状態、要注意判定を第一画面で確認できるようにします。
分析・モデル化の考え方
一覧の集約単位をラインとし、指示件数、要注意件数、異常停止件数、平均進捗をKPIとして計算します。要注意率は
です。件数だけでなく分母を併記することで、規模の異なるラインを比較できます。
function OrderTable({ orders, onSelect }) {
return <table><tbody>{orders.map(order => (
<tr key={order.orderId} onClick={() => onSelect(order.orderId)}>
<td>{order.orderId}</td><td>{order.line}</td><td>{order.progressPct}%</td>
</tr>
))}</tbody></table>;
}
Pythonで確認する
line_summary = (
orders.groupby("line")
.agg(
指示件数=("order_id", "size"),
要注意件数=("needs_attention", "sum"),
異常停止件数=("machine_status", lambda s: s.eq("異常停止").sum()),
平均進捗率=("progress_pct", "mean"),
)
)
line_summary["要注意率"] = line_summary["要注意件数"] / line_summary["指示件数"]
line_summary["平均進捗率"] = line_summary["平均進捗率"].round(1)
display(line_summary.style.format({"要注意率": "{:.1%}", "平均進捗率": "{:.1f}%"}))
ax = line_summary["要注意率"].mul(100).plot(kind="bar", color="#E07A5F", figsize=(7, 4))
ax.set_title("ライン別の要注意作業指示率")
ax.set_xlabel("製造ライン")
ax.set_ylabel("要注意率(%)")
ax.grid(axis="y", alpha=0.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| 指示件数 | 要注意件数 | 異常停止件数 | 平均進捗率 | 要注意率 | |
|---|---|---|---|---|---|
| line | |||||
| 第1ライン | 130 | 89 | 4 | 67.1% | 68.5% |
| 第2ライン | 127 | 72 | 7 | 69.5% | 56.7% |
| 第3ライン | 103 | 65 | 9 | 71.8% | 63.1% |
結果の読み取り
ライン別に要注意率を表示すると、単純な件数順とは異なる優先順位が見える場合があります。Reactの一覧では集約カードと明細表を分け、カードから該当明細へ絞り込める設計が有効です。色だけに依存せず、「要注意」ラベルと件数を併記し、現場端末でも判別できるようにします。
No.044:詳細画面を作成する
実務での意味
詳細画面は、一覧で気づいた異常について、誰が・何を・いつまでに対応するか決める場所です。基本属性だけでなく、異常理由、実績、履歴、関連設備への導線が必要です。
分析・モデル化の考え方
要注意指示を、優先度、納期超過、不良率、設備停止からスコアリングします。
このスコアは画面にそのまま表示する絶対的な危険度ではなく、詳細確認対象を選ぶ例です。重みは安全・品質・納期の業務ルールに基づき合意する必要があります。
function OrderDetail({ order }) {
return <section>
<h2>{order.orderId}</h2>
<StatusBadge status={order.machineStatus} />
<dl><dt>不良率</dt><dd>{order.defectRatePct}%</dd></dl>
</section>;
}
Pythonで確認する
risk = (
3 * orders["priority"].eq("高").astype(int)
+ 2 * orders["overdue"].astype(int)
+ 2 * orders["defect_rate_pct"].ge(1.5).astype(int)
+ 3 * orders["machine_status"].eq("異常停止").astype(int)
)
detail_candidates = orders.assign(risk_score=risk).sort_values(
["risk_score", "planned_at"], ascending=[False, True]
)
detail_cols = ["order_id", "line", "product", "planned_at", "priority", "progress_pct", "defect_rate_pct", "machine_status", "risk_score"]
display(detail_candidates[detail_cols].head(8))
selected = detail_candidates.iloc[0]
timeline = pd.DataFrame({
"時刻": [selected["planned_at"] - pd.Timedelta(hours=4), selected["planned_at"] - pd.Timedelta(hours=1), selected["planned_at"]],
"イベント": ["作業指示を発行", "設備状態を更新", "予定開始時刻"],
})
print(f"詳細表示対象: {selected['order_id']} / リスクスコア: {selected['risk_score']}")
display(timeline)
| order_id | line | product | planned_at | priority | progress_pct | defect_rate_pct | machine_status | risk_score | |
|---|---|---|---|---|---|---|---|---|---|
| 55 | WO-0056 | 第1ライン | CZ-300 | 2026-06-07 01:00:00 | 高 | 64.0 | 0.23 | 異常停止 | 8 |
| 10 | WO-0011 | 第3ライン | BZ-110 | 2026-06-02 09:00:00 | 中 | 72.0 | 1.85 | 異常停止 | 7 |
| 259 | WO-0260 | 第2ライン | AX-200 | 2026-06-03 17:00:00 | 高 | 44.0 | 2.14 | 稼働 | 7 |
| 353 | WO-0354 | 第1ライン | AX-100 | 2026-06-04 17:00:00 | 高 | 77.0 | 3.44 | 点検 | 7 |
| 18 | WO-0019 | 第3ライン | AX-100 | 2026-06-04 20:00:00 | 高 | 66.0 | 2.46 | 稼働 | 7 |
| 39 | WO-0040 | 第3ライン | CZ-300 | 2026-06-05 23:00:00 | 中 | 50.0 | 1.58 | 異常停止 | 7 |
| 347 | WO-0348 | 第3ライン | AX-200 | 2026-06-16 03:00:00 | 高 | 33.0 | 2.08 | 段取 | 7 |
| 335 | WO-0336 | 第1ライン | CZ-300 | 2026-06-18 08:00:00 | 高 | 61.0 | 2.37 | 稼働 | 7 |
詳細表示対象: WO-0056 / リスクスコア: 8
| 時刻 | イベント | |
|---|---|---|
| 0 | 2026-06-06 21:00:00 | 作業指示を発行 |
| 1 | 2026-06-07 00:00:00 | 設備状態を更新 |
| 2 | 2026-06-07 01:00:00 | 予定開始時刻 |
結果の読み取り
詳細画面では、スコアだけでなく、その根拠となった「高優先度」「納期超過」「不良率」「異常停止」を個別に表示します。担当者は根拠を確認して対応でき、スコア計算ルールを変更した際も説明可能性を保てます。履歴は現在値と分離し、時刻順で追えるようにします。
No.045:入力フォームを作成する
実務での意味
実績入力フォームはデータ品質の入口です。項目名をDB列名のまま表示せず、作業者が理解できる単位と説明を付けます。作業指示から確定できる値は初期表示し、再入力を減らします。
分析・モデル化の考え方
入力時間を「手入力項目数 × 1項目当たり時間 + 画面切替時間」で近似し、初期値や選択肢による削減効果を見ます。ここでは現場観察を模した固定シナリオで比較します。
function ResultForm({ order }) {
const [values, setValues] = useState({ goodQty: "", defectQty: "" });
const update = event => setValues(v => ({ ...v, [event.target.name]: event.target.value }));
return <form><input name="goodQty" value={values.goodQty} onChange={update} /></form>;
}
Pythonで確認する
form_fields = pd.DataFrame({
"項目": ["作業指示ID", "品番", "良品数", "不良数", "加工温度", "作業者", "不良理由"],
"入力方式": ["指示から自動", "指示から自動", "数値入力", "数値入力", "数値入力", "ログイン情報", "条件付き選択"],
"必須条件": ["常に", "常に", "常に", "常に", "常に", "常に", "不良10個以上"],
"手入力秒": [0, 0, 8, 7, 7, 0, 10],
})
display(form_fields)
baseline_seconds = 7 * 8 + 12
designed_seconds = form_fields["手入力秒"].sum() + 4
comparison = pd.Series({"全項目を手入力": baseline_seconds, "初期値・条件分岐あり": designed_seconds})
ax = comparison.plot(kind="bar", color=["#D1495B", "#2E86AB"], figsize=(7, 4))
ax.set_title("1件当たり入力時間の設計比較")
ax.set_xlabel("フォーム設計")
ax.set_ylabel("想定入力時間(秒)")
ax.grid(axis="y", alpha=0.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
print(f"想定入力時間の削減: {baseline_seconds - designed_seconds}秒/件({1-designed_seconds/baseline_seconds:.1%})")
| 項目 | 入力方式 | 必須条件 | 手入力秒 | |
|---|---|---|---|---|
| 0 | 作業指示ID | 指示から自動 | 常に | 0 |
| 1 | 品番 | 指示から自動 | 常に | 0 |
| 2 | 良品数 | 数値入力 | 常に | 8 |
| 3 | 不良数 | 数値入力 | 常に | 7 |
| 4 | 加工温度 | 数値入力 | 常に | 7 |
| 5 | 作業者 | ログイン情報 | 常に | 0 |
| 6 | 不良理由 | 条件付き選択 | 不良10個以上 | 10 |
想定入力時間の削減: 32秒/件(47.1%)
結果の読み取り
初期値と条件付き表示により、1件当たりの操作を大きく減らせる想定です。件数を掛ければ日次の削減時間を試算できます。ただし自動設定した値は非表示にせず、誤った作業指示を選択していないか確認できる読み取り専用表示にします。
No.046:フォームのバリデーションを行う
実務での意味
バリデーションは入力を厳しくするためではなく、誤ったデータが後工程へ流れる前に、修正方法を伝える仕組みです。良品数が負、良品数と不良数の合計が計画数を超える、管理範囲外の温度、重大不良なのに理由がない、といった不整合を登録前に検知します。
分析・モデル化の考え方
各ルールの検知率と重複を確認します。フロントエンド検証は迅速なフィードバックに有効ですが、APIを直接呼ぶ経路もあるため、同じ業務ルールをサーバー側でも必ず検証します。
function validate(values, plannedQty) {
const errors = {};
if (values.goodQty < 0) errors.goodQty = "0以上で入力してください";
if (values.goodQty + values.defectQty > plannedQty) errors.qty = "合計を計画数以下にしてください";
return errors;
}
Pythonで確認する
validation_counts = forms[["qty_invalid", "temperature_invalid", "reason_invalid", "operator_invalid"]].sum()
validation_counts.index = ["数量不整合", "温度範囲外", "不良理由なし", "作業者なし"]
validation_table = pd.DataFrame({
"検知件数": validation_counts,
"検知率": validation_counts / len(forms),
})
display(validation_table.style.format({"検知率": "{:.1%}"}))
ax = validation_table["検知率"].mul(100).sort_values().plot(kind="barh", color="#F2CC8F", figsize=(7, 4))
ax.set_title("バリデーションルール別の検知率")
ax.set_xlabel("検知率(%)")
ax.set_ylabel("検証ルール")
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.show()
print(f"少なくとも1件の不整合がある入力: {(~forms['is_valid']).mean():.1%}")
| 検知件数 | 検知率 | |
|---|---|---|
| 数量不整合 | 174 | 54.4% |
| 温度範囲外 | 14 | 4.4% |
| 不良理由なし | 58 | 18.1% |
| 作業者なし | 15 | 4.7% |
少なくとも1件の不整合がある入力: 65.3%
結果の読み取り
どのルールで入力が止まりやすいかを把握すると、単なる利用者ミスではなく、初期値、単位表示、選択肢、作業手順の改善対象を見つけられます。エラーメッセージは「不正です」ではなく、「良品数と不良数の合計を計画数以下にしてください」のように修正行動まで示します。
No.047:ReactからAPIを呼び出す
実務での意味
Reactはブラウザ上の画面を担い、作業指示の取得や実績登録はAPIへ依頼します。API呼び出しではURLとHTTPメソッドだけでなく、タイムアウト、キャンセル、再試行、二重登録防止まで設計対象です。
分析・モデル化の考え方
エンドポイント別に中央値、95パーセンタイル、失敗率を確認します。平均値だけでは一部の長い待ち時間を見落とすため、95パーセンタイル を併記します。登録APIの自動再試行には冪等性キーなど、重複登録を防ぐ仕組みが必要です。
async function fetchOrders(signal) {
const response = await fetch("/api/orders", { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
Pythonで確認する
api_summary = (
api_logs.groupby("endpoint")
.agg(
呼出件数=("latency_ms", "size"),
中央値_ms=("latency_ms", "median"),
p95_ms=("latency_ms", lambda s: s.quantile(0.95)),
失敗率=("failed", "mean"),
)
.round({"中央値_ms": 0, "p95_ms": 0, "失敗率": 3})
)
display(api_summary.style.format({"中央値_ms": "{:.0f}", "p95_ms": "{:.0f}", "失敗率": "{:.1%}"}))
fig, ax = plt.subplots(figsize=(8, 4))
for endpoint, group in api_logs.groupby("endpoint"):
ax.hist(group["latency_ms"], bins=np.arange(0, 3100, 150), alpha=0.45, label=endpoint)
ax.set_title("APIエンドポイント別の応答時間分布")
ax.set_xlabel("応答時間(ms)")
ax.set_ylabel("呼び出し件数")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
| 呼出件数 | 中央値_ms | p95_ms | 失敗率 | |
|---|---|---|---|---|
| endpoint | ||||
| GET /orders | 398 | 430 | 1184 | 3.5% |
| GET /orders/:id | 245 | 429 | 1118 | 2.0% |
| POST /results | 157 | 418 | 1245 | 2.5% |
結果の読み取り
中央値が許容範囲でも、裾の長い分布では一部利用者が長時間待ちます。React側では通信開始時に状態をloadingへ変え、一定時間を超えたら待機中であることを明示します。取得処理の再試行と、登録処理の再試行は重複の影響が異なるため、同じ実装にしないことが重要です。
No.048:APIレスポンスを画面に表示する
実務での意味
APIレスポンスをそのまま表へ流し込むと、コード値、欠損値、日時、単位が利用者に分かりにくくなります。APIデータを画面用の表示モデルへ変換し、表示規則を一か所へ集約します。
分析・モデル化の考え方
データの取得層と表示変換層を分けます。例えば進捗率と設備状態から表示ラベルを作り、予定日時を現場の表記へ変換します。欠損値は空文字にせず、未登録とデータ取得失敗を区別します。
const toOrderViewModel = order => ({
id: order.order_id,
schedule: new Intl.DateTimeFormat("ja-JP", { dateStyle: "short", timeStyle: "short" }).format(new Date(order.planned_at)),
progress: `${order.progress_pct}%`,
});
Pythonで確認する
api_response = orders.head(8)[["order_id", "line", "product", "planned_at", "progress_pct", "machine_status", "needs_attention"]].copy()
status_label = np.select(
[api_response["machine_status"].eq("異常停止"), api_response["progress_pct"].eq(100), api_response["progress_pct"].gt(0)],
["停止・要対応", "完了", "製造中"],
default="未着手",
)
view_model = pd.DataFrame({
"作業指示": api_response["order_id"],
"ライン・品番": api_response["line"] + " / " + api_response["product"],
"予定": api_response["planned_at"].dt.strftime("%m/%d %H:%M"),
"進捗": api_response["progress_pct"].map(lambda x: f"{x:.0f}%"),
"表示状態": status_label,
"対応": np.where(api_response["needs_attention"], "確認が必要", "通常"),
})
print("APIレスポンス(内部表現)")
display(api_response.head(4))
print("画面表示用モデル")
display(view_model)
APIレスポンス(内部表現)
| order_id | line | product | planned_at | progress_pct | machine_status | needs_attention | |
|---|---|---|---|---|---|---|---|
| 0 | WO-0001 | 第1ライン | CZ-300 | 2026-06-13 04:00:00 | 73.0 | 稼働 | True |
| 1 | WO-0002 | 第3ライン | BZ-110 | 2026-06-10 13:00:00 | 56.0 | 稼働 | True |
| 2 | WO-0003 | 第2ライン | CZ-300 | 2026-06-12 20:00:00 | 60.0 | 稼働 | True |
| 3 | WO-0004 | 第2ライン | BZ-110 | 2026-06-08 07:00:00 | 19.0 | 稼働 | True |
画面表示用モデル
| 作業指示 | ライン・品番 | 予定 | 進捗 | 表示状態 | 対応 | |
|---|---|---|---|---|---|---|
| 0 | WO-0001 | 第1ライン / CZ-300 | 06/13 04:00 | 73% | 製造中 | 確認が必要 |
| 1 | WO-0002 | 第3ライン / BZ-110 | 06/10 13:00 | 56% | 製造中 | 確認が必要 |
| 2 | WO-0003 | 第2ライン / CZ-300 | 06/12 20:00 | 60% | 製造中 | 確認が必要 |
| 3 | WO-0004 | 第2ライン / BZ-110 | 06/08 07:00 | 19% | 製造中 | 確認が必要 |
| 4 | WO-0005 | 第3ライン / CZ-300 | 06/09 11:00 | 78% | 製造中 | 確認が必要 |
| 5 | WO-0006 | 第1ライン / AX-100 | 06/07 12:00 | 53% | 製造中 | 確認が必要 |
| 6 | WO-0007 | 第1ライン / CZ-300 | 06/28 03:00 | 30% | 製造中 | 通常 |
| 7 | WO-0008 | 第3ライン / AX-100 | 06/26 11:00 | 100% | 完了 | 通常 |
結果の読み取り
表示用モデルに変換すると、APIの列名やコード体系を画面全体へ漏らさずに済みます。Reactでは変換関数をテスト可能な純粋関数として切り出し、日時・単位・欠損値・ラベルの規則を確認します。ただし「停止」と「完了」の優先順位などは業務担当者と合意し、画面だけの独自ルールにしません。
No.049:検索・絞り込みUIを作成する
実務での意味
検索UIは条件を多く並べるほど便利になるわけではありません。班長が朝会前に「第2ラインの未完了かつ要注意」を探す、といった主要タスクを短い操作で完了できることが重要です。
分析・モデル化の考え方
絞り込み条件を段階的に適用し、母集団が何件から何件へ減るかを確認します。検索結果が0件の場合は、データが存在しないのか、条件が厳しすぎるのか分かるよう、適用中条件と解除操作を表示します。
const visibleOrders = useMemo(() => orders.filter(order =>
(line === "all" || order.line === line) &&
(!attentionOnly || order.needsAttention)
), [orders, line, attentionOnly]);
Pythonで確認する
filter_steps = []
filtered = orders.copy()
filter_steps.append(("全作業指示", len(filtered)))
filtered = filtered[filtered["line"] == "第2ライン"]
filter_steps.append(("第2ライン", len(filtered)))
filtered = filtered[filtered["status"] != "完了"]
filter_steps.append(("未完了", len(filtered)))
filtered = filtered[filtered["needs_attention"]]
filter_steps.append(("要注意", len(filtered)))
filtered = filtered[filtered["priority"] == "高"]
filter_steps.append(("優先度:高", len(filtered)))
filter_funnel = pd.DataFrame(filter_steps, columns=["条件", "該当件数"])
filter_funnel["初期件数比"] = filter_funnel["該当件数"] / len(orders)
display(filter_funnel.style.format({"初期件数比": "{:.1%}"}))
display(filtered[["order_id", "product", "planned_at", "progress_pct", "defect_rate_pct", "machine_status"]].head(10))
ax = filter_funnel.plot(x="条件", y="該当件数", marker="o", color="#3D405B", legend=False, figsize=(8, 4))
ax.set_title("検索条件を適用したときの該当件数")
ax.set_xlabel("適用済み条件")
ax.set_ylabel("該当件数")
ax.grid(alpha=0.3)
plt.xticks(rotation=15)
plt.tight_layout()
plt.show()
| 条件 | 該当件数 | 初期件数比 | |
|---|---|---|---|
| 0 | 全作業指示 | 360 | 100.0% |
| 1 | 第2ライン | 127 | 35.3% |
| 2 | 未完了 | 98 | 27.2% |
| 3 | 要注意 | 67 | 18.6% |
| 4 | 優先度:高 | 13 | 3.6% |
| order_id | product | planned_at | progress_pct | defect_rate_pct | machine_status | |
|---|---|---|---|---|---|---|
| 11 | WO-0012 | AX-200 | 2026-06-17 00:00:00 | 40.0 | 1.44 | 段取 |
| 21 | WO-0022 | CZ-300 | 2026-06-29 18:00:00 | 78.0 | 2.13 | 稼働 |
| 45 | WO-0046 | BZ-110 | 2026-06-11 07:00:00 | 70.0 | 1.02 | 稼働 |
| 68 | WO-0069 | BZ-110 | 2026-06-12 18:00:00 | 68.0 | 0.46 | 点検 |
| 69 | WO-0070 | AX-100 | 2026-06-03 19:00:00 | 74.0 | 0.32 | 稼働 |
| 108 | WO-0109 | AX-200 | 2026-06-05 15:00:00 | 60.0 | 0.61 | 稼働 |
| 140 | WO-0141 | BZ-110 | 2026-06-13 19:00:00 | 76.0 | 0.81 | 段取 |
| 171 | WO-0172 | CZ-300 | 2026-06-20 19:00:00 | 87.0 | 0.43 | 稼働 |
| 201 | WO-0202 | AX-200 | 2026-06-11 16:00:00 | 51.0 | 0.45 | 段取 |
| 228 | WO-0229 | AX-100 | 2026-06-10 17:00:00 | 46.0 | 0.77 | 稼働 |
結果の読み取り
主要な条件を順に適用すると、確認対象を現実的な件数まで減らせます。UIでは頻用条件をプリセットとして保存し、条件変更のたびに件数を表示すると、0件になる前に絞り込みの強さを把握できます。検索語入力ではAPIの過剰呼び出しを避けるため、一定時間入力を待つデバウンスも検討します。
No.050:ローディング・エラー表示を実装する
実務での意味
画面には成功だけでなく、初期表示、読み込み中、空データ、通信失敗、権限不足、再試行後の成功があります。これらを明示的な状態として設計しないと、白い画面や古いデータが正常に見えてしまいます。
分析・モデル化の考え方
応答時間と失敗有無からUI状態を分類し、状態別に利用者の完了率を仮定したシミュレーションを行います。ローディング表示と具体的な再試行案内により完了率が変わるか、固定シードで比較します。
if (state === "loading") return <LoadingPanel message="作業指示を読み込んでいます" />;
if (state === "error") return <ErrorPanel message="取得できませんでした" onRetry={loadOrders} />;
if (orders.length === 0) return <EmptyPanel message="条件に一致する作業指示はありません" />;
return <OrderTable orders={orders} />;
Pythonで確認する
ui_events = api_logs.copy()
ui_events["状態"] = np.select(
[ui_events["failed"], ui_events["latency_ms"] >= 1200, ui_events["latency_ms"] >= 500],
["エラー", "長い待機", "短い待機"],
default="即時表示",
)
state_summary = ui_events.groupby("状態").agg(件数=("endpoint", "size"), 平均応答_ms=("latency_ms", "mean"))
state_summary["構成比"] = state_summary["件数"] / len(ui_events)
display(state_summary.style.format({"平均応答_ms": "{:.0f}", "構成比": "{:.1%}"}))
completion_prob = {
"表示なし": {"即時表示": 0.98, "短い待機": 0.90, "長い待機": 0.60, "エラー": 0.18},
"状態表示あり": {"即時表示": 0.98, "短い待機": 0.96, "長い待機": 0.84, "エラー": 0.58},
}
simulation_rng = np.random.default_rng(SEED + 50)
rates = {}
for design, probs in completion_prob.items():
completed = [simulation_rng.random() < probs[state] for state in ui_events["状態"]]
rates[design] = np.mean(completed)
completion = pd.Series(rates)
ax = completion.mul(100).plot(kind="bar", color=["#D1495B", "#2E86AB"], figsize=(7, 4))
ax.set_title("状態表示設計による操作完了率のシミュレーション")
ax.set_xlabel("UI設計")
ax.set_ylabel("操作完了率(%)")
ax.set_ylim(0, 100)
ax.grid(axis="y", alpha=0.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
print(completion.map(lambda x: f"{x:.1%}"))
| 件数 | 平均応答_ms | 構成比 | |
|---|---|---|---|
| 状態 | |||
| エラー | 23 | 594 | 2.9% |
| 即時表示 | 457 | 299 | 57.1% |
| 短い待機 | 283 | 717 | 35.4% |
| 長い待機 | 37 | 1518 | 4.6% |
表示なし 91.9%
状態表示あり 95.6%
dtype: str
結果の読み取り
この結果は仮定に基づくシミュレーションであり、効果の実測値ではありません。ただし、通信状態を明示し、エラー時に「再試行」「入力内容を保持」「問い合わせ時に伝える番号」を用意することが、業務継続へどうつながるかをKPI化する例になります。本番導入後は操作ログから完了率、再試行率、二重送信率を計測し、仮定を更新します。
対象ノックを通して見える実務上の示唆
-
コンポーネント境界は業務責務の境界でもある
ステータス表示、一覧、詳細、実績入力を分けることで、表示規則と変更影響を管理しやすくなります。 -
一覧と詳細は役割が異なる
一覧は対応対象の発見、詳細は判断根拠の確認と次の行動に集中させます。 -
入力品質は画面だけでは守れない
Reactで即時に検証しつつ、API側でも同じ業務ルールを検証し、監査可能な形で結果を残します。 -
API性能は利用者の業務完了率へ翻訳する
応答時間や失敗率だけでなく、離脱、再試行、二重登録、問い合わせへの影響を追います。 -
正常・空・待機・失敗を明示的に設計する
例外状態を後付けにせず、画面設計と受入テストのシナリオへ最初から含めます。
実務導入する場合に必要なこと
1. 利用状況と業務ルールの確認
利用者、端末、手袋の有無、ネットワーク品質、ピーク時の件数、判断期限を現場観察とヒアリングで確認します。バリデーションや優先度スコアは、品質保証・生産管理・情報システムの合意事項として管理します。
2. API契約と状態遷移の定義
リクエスト・レスポンスの型、エラーコード、タイムアウト、再試行可否、冪等性を定義します。画面状態は最低でもidle / loading / success / empty / errorを区別し、各状態の表示と操作を受入条件にします。
3. アクセシビリティと現場端末での検証
色だけに依存しない表示、十分な文字・ボタンサイズ、キーボード操作、読み上げ可能なラベルを確認します。実際のタブレット、照明、騒音、ネットワーク環境でユーザビリティテストを行います。
4. 運用KPIと改善サイクル
対象発見時間、入力完了率、バリデーション発生率、APIの、再試行率、問い合わせ件数を継続計測します。個人評価に流用せず、画面・業務手順・教育・APIのどこを改善すべきか判断する材料にします。
5. セキュリティと監査
画面で非表示にするだけでは権限制御になりません。APIで認可を実施し、変更前後の値、操作者、時刻、理由を監査ログへ残します。生産・品質データの保持期間と閲覧範囲も定めます。
まとめ
No.041〜No.050では、Reactの基本構成からエラー表示までを、工場作業指示アプリの一連の設計として確認しました。重要なのは、Reactの機能を使うこと自体ではなく、現場の判断単位に合わせて状態とコンポーネントを分け、APIの不確実性を含む画面状態を説明可能にすることです。
Pythonによる架空データ分析から、一覧の要注意率、入力不整合、APIの応答分布、絞り込み件数、操作完了率のように、画面設計を業務KPIへ接続できることも確認しました。実務では実際の操作ログと利用者観察で仮定を更新し、小さな画面から段階的に改善することが有効です。
法人向けのご相談
数理工房では、製造業の業務整理、データ分析、AI・数理モデル開発に加えて、それらを現場で継続利用するためのWebシステム・業務画面の設計開発をご支援しています。
- 紙・Excel業務をWebシステムへ移行したい
- 生産・品質・設備データを現場で見やすく可視化したい
- 分析モデルや最適化結果を業務画面へ組み込みたい
- API、フロントエンド、運用KPIを一体で設計したい
といった課題について、構想整理、プロトタイプ、実装、運用設計までご相談いただけます。
📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。