100本ノック / システム開発 / システム開発100本ノック
製造業のシステム開発入門|設備保全アプリで学ぶWeb・API・DB設計
設備停止を減らす保全依頼管理システムの設計入門
100本ノック「システム開発」No.001〜No.010
本記事は、製造業の業務課題を題材に、システム開発の全体像を10個のノックで段階的に理解する実践編です。題材は、紙・電話・表計算ファイルに分散した設備停止と保全依頼の管理です。システムの目的、Webアプリの構造、HTTP・API、要件定義、設計間の整合、業務フロー、全体構成までを一つの架空ケースでつなぎます。
100本ノック全体では、開発環境、データベース、API、フロントエンド、認証、テスト、CI/CD、業務機能、AI・データ活用へ進みます。最初の10本では、その後の実装で迷わないための「地図」を作ります。重要なのは技術を導入すること自体ではなく、現場の判断速度、記録品質、停止損失をどう改善するかです。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
1. はじめに:この記事で扱う製造業の実務課題
架空の「数理精機工場」では、設備異常を発見した作業者が班長へ連絡し、班長が保全担当へ電話します。処置内容は紙の日報と表計算ファイルへ後から転記されます。緊急案件への対応力は高い一方、受付時刻・担当・復旧時刻の記録が揃わず、月末になっても「どの設備へ予防保全を優先すべきか」を比較できません。
ここで開発する業務アプリの経営目的を、次のように置きます。
- 異常発見から保全着手までの待ち時間を短縮する
- 未対応・対応中・完了を一つの画面で把握する
- 設備別の停止時間と原因を蓄積し、保全投資の判断材料にする
- 現場が入力を続けられる最小限の操作にする
システム開発は、画面を作る作業だけではありません。業務ルールを言語化し、データの意味を揃え、関係者が同じ状態を見て判断できる仕組みへ変換する活動です。
2. 現場でよくある状況
設備異常の情報は、口頭、内線、チャット、紙の日報、個人管理の表計算ファイルに分散しがちです。その結果、同じ案件の二重登録、担当者の認識違い、復旧後の記録漏れが起きます。管理者は月次報告のたびにデータを集め直し、速報値と確定値が一致しません。
特に難しいのは、現場の応急処置が速いほど「記録は後でよい」となりやすい点です。入力項目を増やしすぎると利用されず、減らしすぎると改善分析に使えません。したがって、業務の流れ、画面、API、データベースを同時に考える必要があります。
3. なぜこの問題は判断が難しいのか
設備保全のシステム化には、互いに競合する判断があります。
- 即時性と正確性:発見直後に少ない項目で登録したい一方、原因や処置も詳しく残したい。
- 標準化と例外対応:標準フローは必要だが、安全上の緊急停止や外注修理には例外がある。
- 現場最適と全体最適:各ライン固有の呼び方を尊重しつつ、工場横断で比較できるコード体系が必要である。
- 短期開発と将来拡張:まず小さく始めたい一方、将来はセンサー、在庫、予知保全とも連携したい。
このため、最初から大規模な機能一覧を作るのではなく、意思決定に必要な最小データと業務上の責任分界を明確にしてから技術へ落とし込みます。
4. 今回扱うノックの全体像
| No. | テーマ | このケースで答える問い |
|---|---|---|
| 001 | システム開発とは何か | 何を、どの順序で決めるのか |
| 002 | 業務システムとWebアプリ | 業務目的と提供方式をどう区別するか |
| 003 | フロント・バック・DB | 各層は何に責任を持つか |
| 004 | クライアント・サーバー | 端末とサーバーはどう協調するか |
| 005 | HTTP | データ交換の単位は何か |
| 006 | API | 機能の窓口をどう定義するか |
| 007 | 要件定義 | 開発前に何を合意するか |
| 008 | 画面・API・DB設計 | 設計間の抜け漏れをどう防ぐか |
| 009 | 業務フロー | 現場の動きから機能をどう抽出するか |
| 010 | 小規模アプリ設計 | 全要素をどう一つの構成へまとめるか |
各ノックでは、固定シードで生成した架空データを使い、概念を表、集計、グラフ、簡単なシミュレーションへ置き換えます。
5. Python 環境の準備
外部データや外部サービスには接続しません。numpy、pandas、matplotlib を使い、日本語ラベルの表示には japanize_matplotlib を使用します。乱数シードは 42 に固定し、再実行しても同じ結果になるようにします。
from dataclasses import dataclass, asdict
from datetime import datetime, timedelta
import json
import platform
from urllib.parse import urlencode
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import display
SEED = 42
rng = np.random.default_rng(SEED)
pd.set_option("display.max_columns", 20)
pd.set_option("display.width", 120)
plt.rcParams["figure.figsize"] = (8, 4.5)
print("Python :", platform.python_version())
print("numpy :", np.__version__)
print("pandas :", pd.__version__)
print("matplotlib :", matplotlib.__version__)
print("random seed:", SEED)
Python : 3.11.9
numpy : 1.26.4
pandas : 2.2.2
matplotlib : 3.9.2
random seed: 42
6. 架空データの作成
3ライン・8設備を対象に、120件の保全依頼を生成します。reported_at は異常発見・受付時刻、started_at は保全着手時刻、closed_at は復旧・完了時刻です。主要KPIは次の通りです。
停止損失は比較用の簡易指標として、停止時間と設備ごとの1分当たり限界利益から計算します。実導入時には、生産計画、仕掛品、代替設備、品質損失なども考慮する必要があります。
equipment = pd.DataFrame({
"equipment_id": [f"EQ-{i:03d}" for i in range(1, 9)],
"equipment_name": ["プレス1", "プレス2", "旋盤1", "旋盤2", "研削1", "洗浄1", "検査1", "搬送1"],
"line": ["A", "A", "B", "B", "B", "C", "C", "C"],
"loss_yen_per_min": [12000, 10000, 8000, 7500, 9000, 5000, 6500, 4000],
})
n = 120
reported = pd.Timestamp("2026-04-01 08:00") + pd.to_timedelta(
np.sort(rng.integers(0, 60 * 24 * 60, n)), unit="m"
)
priority = rng.choice(["高", "中", "低"], n, p=[0.22, 0.50, 0.28])
wait_base = np.select([priority == "高", priority == "中"], [12, 28], default=55)
initial_response = np.maximum(2, rng.gamma(shape=2.0, scale=wait_base / 2))
repair_time = rng.gamma(shape=2.2, scale=28, size=n) + np.where(priority == "高", 25, 0)
requests = pd.DataFrame({
"request_id": [f"MR-{i:04d}" for i in range(1, n + 1)],
"equipment_id": rng.choice(equipment["equipment_id"], n, p=[.16, .13, .14, .12, .13, .10, .12, .10]),
"reported_at": reported,
"priority": priority,
"cause_category": rng.choice(["摩耗", "調整ずれ", "電気系", "異物", "不明"], n, p=[.28, .24, .18, .16, .14]),
"report_channel": rng.choice(["電話", "紙", "チャット"], n, p=[.45, .30, .25]),
})
requests["started_at"] = requests["reported_at"] + pd.to_timedelta(initial_response, unit="m")
requests["closed_at"] = requests["started_at"] + pd.to_timedelta(repair_time, unit="m")
requests["status"] = "完了"
requests = requests.merge(equipment, on="equipment_id", how="left")
requests["response_min"] = (requests["started_at"] - requests["reported_at"]).dt.total_seconds() / 60
requests["downtime_min"] = (requests["closed_at"] - requests["reported_at"]).dt.total_seconds() / 60
requests["estimated_loss_yen"] = requests["downtime_min"] * requests["loss_yen_per_min"]
print(f"保全依頼: {len(requests)}件 / 対象設備: {requests['equipment_id'].nunique()}台")
display(requests.head().round({"response_min": 1, "downtime_min": 1, "estimated_loss_yen": 0}))
保全依頼: 120件 / 対象設備: 8台
| request_id | equipment_id | reported_at | priority | cause_category | report_channel | started_at | closed_at | status | equipment_name | line | loss_yen_per_min | response_min | downtime_min | estimated_loss_yen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | MR-0001 | EQ-006 | 2026-04-01 18:36:00 | 中 | 摩耗 | チャット | 2026-04-01 19:02:20.766500160 | 2026-04-01 20:28:50.649064020 | 完了 | 洗浄1 | C | 5000 | 26.3 | 112.8 | 564221.0 |
| 1 | MR-0002 | EQ-005 | 2026-04-03 13:39:00 | 中 | 摩耗 | チャット | 2026-04-03 14:07:53.397734982 | 2026-04-03 15:08:05.648771814 | 完了 | 研削1 | B | 9000 | 28.9 | 89.1 | 801847.0 |
| 2 | MR-0003 | EQ-003 | 2026-04-03 23:04:00 | 中 | 調整ずれ | チャット | 2026-04-03 23:31:31.636614581 | 2026-04-04 01:32:48.822951995 | 完了 | 旋盤1 | B | 8000 | 27.5 | 148.8 | 1190510.0 |
| 3 | MR-0004 | EQ-008 | 2026-04-05 03:53:00 | 低 | 摩耗 | 電話 | 2026-04-05 05:30:23.773472670 | 2026-04-05 05:58:24.436947378 | 完了 | 搬送1 | C | 4000 | 97.4 | 125.4 | 501629.0 |
| 4 | MR-0005 | EQ-004 | 2026-04-05 09:48:00 | 中 | 摩耗 | 電話 | 2026-04-05 10:51:33.109732547 | 2026-04-05 11:40:09.319322723 | 完了 | 旋盤2 | B | 7500 | 63.6 | 112.2 | 841165.0 |
7. No.001:システム開発とは何かを整理する
実務での意味
システム開発とは、現場課題をソフトウェアへ置き換えるだけの作業ではありません。「誰が、いつ、何を判断するか」を定め、それに必要な業務、データ、操作、運用を一貫させる活動です。本ケースでは、目的を「登録件数の増加」ではなく、初動時間と停止損失の低減に置きます。
分析・モデル化の考え方
企画、要件定義、設計、実装、テスト、運用の各段階で手戻り確率と影響額が異なります。簡易的に、段階 の期待手戻り損失を
とします。 は要件誤解などが残る確率、 は後工程で発覚した場合の修正費用です。数値自体は架空ですが、早期の合意に時間を使う理由を可視化できます。
Pythonで確認する
development_phases = pd.DataFrame({
"工程": ["企画", "要件定義", "設計", "実装", "テスト", "運用"],
"主な成果物": ["目的・KPI", "業務・機能要件", "画面/API/DB設計", "動く機能", "品質確認", "監視・改善"],
"誤解残存確率": [0.30, 0.22, 0.16, 0.10, 0.06, 0.04],
"発覚時修正費用_万円": [20, 45, 90, 180, 320, 500],
})
development_phases["期待手戻り損失_万円"] = (
development_phases["誤解残存確率"] * development_phases["発覚時修正費用_万円"]
)
display(development_phases.round(1))
fig, ax = plt.subplots()
ax.bar(development_phases["工程"], development_phases["期待手戻り損失_万円"], color="#4472C4")
ax.set_title("開発工程別の期待手戻り損失(架空シナリオ)")
ax.set_xlabel("工程")
ax.set_ylabel("期待手戻り損失(万円)")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
| 工程 | 主な成果物 | 誤解残存確率 | 発覚時修正費用_万円 | 期待手戻り損失_万円 | |
|---|---|---|---|---|---|
| 0 | 企画 | 目的・KPI | 0.3 | 20 | 6.0 |
| 1 | 要件定義 | 業務・機能要件 | 0.2 | 45 | 9.9 |
| 2 | 設計 | 画面/API/DB設計 | 0.2 | 90 | 14.4 |
| 3 | 実装 | 動く機能 | 0.1 | 180 | 18.0 |
| 4 | テスト | 品質確認 | 0.1 | 320 | 19.2 |
| 5 | 運用 | 監視・改善 | 0.0 | 500 | 20.0 |

結果の読み取り
誤解の残存確率が下がっても、後工程ほど変更範囲が広がるため期待損失は大きくなります。したがって、設備名称、優先度、完了条件、KPIの定義は実装前に現場・保全・管理者で確認する価値があります。要件定義を長期化させるのではなく、損失の大きい曖昧さから短い周期で潰すことが実務的です。
8. No.002:業務システムとWebアプリの違いを理解する
実務での意味
「業務システム」は、保全依頼の受付、担当割当、履歴管理など業務目的による分類です。「Webアプリ」は、ブラウザとWeb技術を使う提供方式による分類です。両者は対立概念ではなく、保全依頼管理という業務システムをWebアプリとして提供できます。
分析・モデル化の考え方
提供方式は、現場適合性、更新容易性、オフライン耐性、端末機能連携、導入費用などの多基準で比較します。ここでは5点満点の仮評価と重み を用い、方式 の加重評価を
で計算します。評価値は選定手順を示すための架空値であり、実際には工場の通信環境とセキュリティ基準を反映します。
Pythonで確認する
delivery_options = pd.DataFrame({
"評価軸": ["複数端末対応", "更新容易性", "オフライン耐性", "カメラ連携", "初期導入の軽さ"],
"重み": [0.25, 0.25, 0.20, 0.10, 0.20],
"Webアプリ": [5, 5, 2, 3, 5],
"端末専用アプリ": [3, 2, 5, 5, 2],
"表計算ファイル": [2, 2, 4, 1, 4],
})
scores = {
col: float((delivery_options["重み"] * delivery_options[col]).sum())
for col in ["Webアプリ", "端末専用アプリ", "表計算ファイル"]
}
display(delivery_options)
display(pd.Series(scores, name="加重評価(5点満点)").sort_values(ascending=False).round(2).to_frame())
fig, ax = plt.subplots()
ax.bar(scores.keys(), scores.values(), color=["#4472C4", "#70AD47", "#A5A5A5"])
ax.set_title("保全依頼管理の提供方式比較(架空評価)")
ax.set_xlabel("提供方式")
ax.set_ylabel("加重評価(5点満点)")
ax.set_ylim(0, 5)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
| 評価軸 | 重み | Webアプリ | 端末専用アプリ | 表計算ファイル | |
|---|---|---|---|---|---|
| 0 | 複数端末対応 | 0.25 | 5 | 3 | 2 |
| 1 | 更新容易性 | 0.25 | 5 | 2 | 2 |
| 2 | オフライン耐性 | 0.20 | 2 | 5 | 4 |
| 3 | カメラ連携 | 0.10 | 3 | 5 | 1 |
| 4 | 初期導入の軽さ | 0.20 | 5 | 2 | 4 |
| 加重評価(5点満点) | |
|---|---|
| Webアプリ | 4.20 |
| 端末専用アプリ | 3.15 |
| 表計算ファイル | 2.70 |

結果の読み取り
この仮定ではWebアプリが有力です。複数の共有端末で利用でき、更新を一括配信しやすいためです。ただし、電波の届かない場所で即時登録が必須なら、Webアプリのオフライン対応や専用アプリも比較対象になります。「Webだから業務システムではない」「業務システムだから専用ソフトが必要」という判断は避けます。
9. No.003:フロントエンド・バックエンド・DBの役割を整理する
実務での意味
フロントエンドは作業者が見る画面と入力支援、バックエンドは優先度判定や権限確認などの業務ルール、データベース(DB)は設備・依頼・履歴の一貫した保存を担います。責任を分けると、画面を変更しても履歴の意味を壊しにくくなります。
分析・モデル化の考え方
応答時間を、画面処理 、通信 、バックエンド処理 、DB処理 の和
として分解します。遅いという印象だけでなく、どの層へ改善投資すべきかを判断できます。
Pythonで確認する
layer_roles = pd.DataFrame({
"層": ["フロントエンド", "バックエンド", "DB"],
"保全アプリでの責任": ["入力・一覧・警告表示", "認証・優先度・状態遷移", "設備・依頼・変更履歴の保存"],
"壊れたときの例": ["入力しづらい", "不正な状態変更を許す", "履歴が欠落・重複する"],
})
display(layer_roles)
routes = pd.DataFrame({
"操作": ["依頼一覧", "依頼登録", "担当割当", "月次集計"],
"画面": [35, 45, 30, 40],
"通信": [25, 30, 25, 30],
"バックエンド": [20, 40, 35, 75],
"DB": [30, 45, 40, 210],
}).set_index("操作")
display(routes.assign(合計ミリ秒=routes.sum(axis=1)))
ax = routes.plot(kind="bar", stacked=True, color=["#5B9BD5", "#ED7D31", "#70AD47", "#FFC000"])
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()
| 層 | 保全アプリでの責任 | 壊れたときの例 | |
|---|---|---|---|
| 0 | フロントエンド | 入力・一覧・警告表示 | 入力しづらい |
| 1 | バックエンド | 認証・優先度・状態遷移 | 不正な状態変更を許す |
| 2 | DB | 設備・依頼・変更履歴の保存 | 履歴が欠落・重複する |
| 画面 | 通信 | バックエンド | DB | 合計ミリ秒 | |
|---|---|---|---|---|---|
| 操作 | |||||
| 依頼一覧 | 35 | 25 | 20 | 30 | 110 |
| 依頼登録 | 45 | 30 | 40 | 45 | 160 |
| 担当割当 | 30 | 25 | 35 | 40 | 130 |
| 月次集計 | 40 | 30 | 75 | 210 | 355 |

結果の読み取り
月次集計はDB処理が支配的です。画面だけを軽量化しても改善幅は限られ、集計方法、索引、事前集計などを検討すべきだと分かります。一方、依頼登録では各層へ時間が分散しています。層の責任分担は、開発チームの分担だけでなく、障害調査と性能改善の単位にもなります。
10. No.004:クライアント・サーバー構成を理解する
実務での意味
現場のPCやタブレット上のブラウザがクライアントです。クライアントは依頼一覧を要求し、サーバーは権限と条件を確認してDBからデータを取得し、結果を返します。データと重要な業務ルールをサーバー側へ集約すると、端末ごとの不整合を抑えられます。
分析・モデル化の考え方
クライアント・サーバー間では、同時利用数、ネットワーク遅延、処理時間、失敗率を考えます。今回は要求の往復時間を模擬し、95パーセンタイル(p95)を確認します。平均だけでは、繁忙時に一部の作業者が感じる遅さを見落とすためです。
Pythonで確認する
request_log = pd.DataFrame({
"network_ms": rng.lognormal(mean=np.log(35), sigma=0.35, size=500),
"server_ms": rng.lognormal(mean=np.log(90), sigma=0.45, size=500),
})
request_log["round_trip_ms"] = request_log.sum(axis=1)
latency_summary = request_log["round_trip_ms"].agg(["mean", "median", lambda s: s.quantile(.95), "max"])
latency_summary.index = ["平均", "中央値", "p95", "最大"]
display(latency_summary.round(1).to_frame("往復時間(ミリ秒)"))
fig, ax = plt.subplots()
ax.hist(request_log["round_trip_ms"], bins=25, color="#4472C4", edgecolor="white")
ax.axvline(request_log["round_trip_ms"].quantile(.95), color="#C00000", linestyle="--", label="p95")
ax.set_title("クライアントから見た要求の往復時間")
ax.set_xlabel("往復時間(ミリ秒)")
ax.set_ylabel("要求数")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
| 往復時間(ミリ秒) | |
|---|---|
| 平均 | 129.7 |
| 中央値 | 123.2 |
| p95 | 209.7 |
| 最大 | 386.1 |

結果の読み取り
分布には右裾があり、平均よりp95が大きくなります。業務画面の性能目標は「平均1秒未満」だけでなく、「通常負荷でp95が何秒以内」のように定めると検証可能です。また、工場内Wi-Fiの遅延とサーバー処理を分けて計測すれば、ネットワーク増強とアプリ改善のどちらを優先するか判断できます。
11. No.005:HTTPリクエストとレスポンスを理解する
実務での意味
Webアプリでは、クライアントがHTTPリクエストを送り、サーバーがHTTPレスポンスを返します。リクエストにはメソッド、URL、ヘッダー、必要に応じて本文が含まれます。レスポンスには状態コード、ヘッダー、本文が含まれます。状態コードを正しく扱うことで、利用者へ「入力不備」「権限不足」「サーバー障害」を区別して伝えられます。
分析・モデル化の考え方
正常・異常の割合を操作別に監視します。成功率は
です。ただし、400系は入力や権限、500系はサーバー内部の問題なので、改善担当が異なります。
Pythonで確認する
http_samples = pd.DataFrame([
["GET", "/api/requests?status=open", 200, "一覧取得成功"],
["POST", "/api/requests", 201, "依頼登録成功"],
["POST", "/api/requests", 400, "必須項目不足"],
["PATCH", "/api/requests/MR-0001", 403, "更新権限なし"],
["GET", "/api/requests/MR-9999", 404, "対象なし"],
["GET", "/api/summary", 500, "集計処理で障害"],
], columns=["method", "path", "status", "業務上の意味"])
display(http_samples)
status_log = rng.choice([200, 201, 400, 403, 404, 500], 1000, p=[.70, .20, .035, .015, .035, .015])
status_class = pd.Series(status_log).map(lambda x: f"{x // 100}xx").value_counts().sort_index()
display(status_class.rename("件数").to_frame().assign(比率=lambda x: (x["件数"] / x["件数"].sum()).round(3)))
fig, ax = plt.subplots()
ax.bar(status_class.index, status_class.values, color=["#70AD47", "#ED7D31", "#C00000"])
ax.set_title("HTTP状態コード区分別の件数(架空ログ)")
ax.set_xlabel("状態コード区分")
ax.set_ylabel("件数")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
| method | path | status | 業務上の意味 | |
|---|---|---|---|---|
| 0 | GET | /api/requests?status=open | 200 | 一覧取得成功 |
| 1 | POST | /api/requests | 201 | 依頼登録成功 |
| 2 | POST | /api/requests | 400 | 必須項目不足 |
| 3 | PATCH | /api/requests/MR-0001 | 403 | 更新権限なし |
| 4 | GET | /api/requests/MR-9999 | 404 | 対象なし |
| 5 | GET | /api/summary | 500 | 集計処理で障害 |
| 件数 | 比率 | |
|---|---|---|
| 2xx | 902 | 0.902 |
| 4xx | 74 | 0.074 |
| 5xx | 24 | 0.024 |

結果の読み取り
2xxが大半でも、4xxと5xxをまとめて「エラー」と扱うと対策を誤ります。400なら入力ガイド、403なら権限設計、404なら画面遷移やデータ同期、500ならサーバーログと例外処理を確認します。現場へは状態コードそのものではなく、次に取るべき行動を示すメッセージへ変換します。
12. No.006:APIとは何かを理解する
実務での意味
APIは、フロントエンドや他システムがバックエンドの機能を利用するための契約です。たとえば「未完了の保全依頼を取得する」「新しい依頼を登録する」という窓口を定義します。APIを明確にすると、将来センサーや生産管理システムから同じ機能を再利用しやすくなります。
分析・モデル化の考え方
API契約では、入力、出力、状態コード、認証、業務制約を定義します。ここでは関数でローカルAPIを模擬し、クエリ条件による絞り込みとJSON形式のレスポンスを確認します。実際の通信は行わず、データ契約の形に焦点を当てます。
Pythonで確認する
def get_maintenance_requests(data: pd.DataFrame, line: str | None = None, priority: str | None = None) -> dict:
# GET /api/maintenance-requests を模擬する純粋なローカル関数
filtered = data.copy()
if line is not None:
filtered = filtered.loc[filtered["line"] == line]
if priority is not None:
filtered = filtered.loc[filtered["priority"] == priority]
records = filtered[["request_id", "equipment_id", "line", "priority", "status"]].head(5).to_dict("records")
return {"status": 200, "count": len(filtered), "items": records}
query = {"line": "A", "priority": "高"}
print("request : GET /api/maintenance-requests?" + urlencode(query))
api_response = get_maintenance_requests(requests, **query)
print("response:")
print(json.dumps(api_response, ensure_ascii=False, indent=2))
api_catalog = pd.DataFrame([
["GET", "/maintenance-requests", "一覧・検索", "作業者/保全/管理者"],
["POST", "/maintenance-requests", "新規登録", "作業者/保全"],
["PATCH", "/maintenance-requests/{id}", "担当・状態更新", "保全"],
["GET", "/equipment/{id}", "設備情報参照", "全員"],
["GET", "/reports/downtime", "停止時間集計", "管理者"],
], columns=["メソッド", "パス", "用途", "利用者"])
display(api_catalog)
request : GET /api/maintenance-requests?line=A&priority=%E9%AB%98
response:
{
"status": 200,
"count": 10,
"items": [
{
"request_id": "MR-0009",
"equipment_id": "EQ-002",
"line": "A",
"priority": "高",
"status": "完了"
},
{
"request_id": "MR-0052",
"equipment_id": "EQ-001",
"line": "A",
"priority": "高",
"status": "完了"
},
{
"request_id": "MR-0055",
"equipment_id": "EQ-001",
"line": "A",
"priority": "高",
"status": "完了"
},
{
"request_id": "MR-0068",
"equipment_id": "EQ-001",
"line": "A",
"priority": "高",
"status": "完了"
},
{
"request_id": "MR-0069",
"equipment_id": "EQ-001",
"line": "A",
"priority": "高",
"status": "完了"
}
]
}
| メソッド | パス | 用途 | 利用者 | |
|---|---|---|---|---|
| 0 | GET | /maintenance-requests | 一覧・検索 | 作業者/保全/管理者 |
| 1 | POST | /maintenance-requests | 新規登録 | 作業者/保全 |
| 2 | PATCH | /maintenance-requests/{id} | 担当・状態更新 | 保全 |
| 3 | GET | /equipment/{id} | 設備情報参照 | 全員 |
| 4 | GET | /reports/downtime | 停止時間集計 | 管理者 |
結果の読み取り
同じ一覧APIでも、ラインと優先度を条件として明示すれば、画面側が独自に全件取得して絞り込む必要がありません。一方で、APIが画面の都合だけに密結合すると再利用しにくくなります。「設備停止案件」という業務資源を中心に、命名、検索条件、更新可能な状態を定義することが重要です。
13. No.007:要件定義で確認すべき項目を整理する
実務での意味
要件定義では、対象業務、利用者、機能、データ、性能、可用性、セキュリティ、移行、運用、受入条件を確認します。「一覧画面が欲しい」だけでは、誰が何を判断する一覧か分かりません。「班長が始業時に未対応の高優先度案件を30秒以内で確認できる」のように業務場面と評価可能な条件を結びます。
分析・モデル化の考え方
候補要件を価値、緊急度、リスク低減、作業規模で評価します。簡易な優先度として
を用います。数値は議論の代替ではなく、部門ごとの暗黙の重みを見える化する道具です。
Pythonで確認する
requirements = pd.DataFrame({
"要件": ["依頼の即時登録", "未対応一覧", "担当者への通知", "写真添付", "月次停止分析", "部品在庫連携"],
"価値": [10, 10, 9, 6, 8, 7],
"緊急度": [10, 9, 9, 5, 6, 4],
"リスク低減": [8, 9, 8, 7, 8, 6],
"作業規模": [3, 3, 5, 5, 4, 9],
})
requirements["優先度スコア"] = (
requirements[["価値", "緊急度", "リスク低減"]].sum(axis=1) / requirements["作業規模"]
)
requirements = requirements.sort_values("優先度スコア", ascending=False)
display(requirements.round(2))
fig, ax = plt.subplots()
ax.barh(requirements["要件"][::-1], requirements["優先度スコア"][::-1], color="#4472C4")
ax.set_title("候補要件の優先度スコア(架空評価)")
ax.set_xlabel("優先度スコア")
ax.set_ylabel("要件")
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.show()
| 要件 | 価値 | 緊急度 | リスク低減 | 作業規模 | 優先度スコア | |
|---|---|---|---|---|---|---|
| 0 | 依頼の即時登録 | 10 | 10 | 8 | 3 | 9.33 |
| 1 | 未対応一覧 | 10 | 9 | 9 | 3 | 9.33 |
| 4 | 月次停止分析 | 8 | 6 | 8 | 4 | 5.50 |
| 2 | 担当者への通知 | 9 | 9 | 8 | 5 | 5.20 |
| 3 | 写真添付 | 6 | 5 | 7 | 5 | 3.60 |
| 5 | 部品在庫連携 | 7 | 4 | 6 | 9 | 1.89 |

結果の読み取り
初期版では即時登録と未対応一覧が優先候補です。部品在庫連携にも価値はありますが、規模が大きいため後続段階に分けられます。ただし、安全法規に関わる要件はスコアにかかわらず必須です。機能要件だけでなく、利用者、性能、監査ログ、バックアップ、問い合わせ対応、既存データ移行も合意します。
14. No.008:画面設計・API設計・DB設計の関係を理解する
実務での意味
画面の入力項目には受け取るAPIが必要で、保存すべき情報にはDBの列や関連が必要です。反対に、DBに項目があるだけでは、誰がいつ登録・参照するかは決まりません。3つの設計を別々に進めると、画面では入力できるのに保存できない、APIは返すのに画面で使わない、といった不整合が生じます。
分析・モデル化の考え方
要件から画面、API、DBまでのトレーサビリティ(追跡可能性)を行列で確認します。行に業務項目、列に設計要素を置き、対応があれば1とします。行の合計が不足する項目は設計漏れの候補です。
Pythonで確認する
traceability = pd.DataFrame({
"登録画面": [1, 1, 1, 0, 0, 0],
"一覧画面": [1, 1, 1, 1, 1, 0],
"登録API": [1, 1, 1, 0, 0, 0],
"更新API": [0, 0, 0, 1, 1, 1],
"依頼テーブル": [1, 1, 1, 1, 1, 1],
"履歴テーブル": [0, 0, 0, 1, 1, 1],
}, index=["設備ID", "異常内容", "優先度", "担当者", "状態", "更新理由"])
display(traceability)
fig, ax = plt.subplots(figsize=(9, 4.5))
im = ax.imshow(traceability.values, cmap="Blues", vmin=0, vmax=1, aspect="auto")
ax.set_xticks(range(len(traceability.columns)), traceability.columns, rotation=25, ha="right")
ax.set_yticks(range(len(traceability.index)), traceability.index)
for i in range(len(traceability.index)):
for j in range(len(traceability.columns)):
ax.text(j, i, "●" if traceability.iloc[i, j] else "-", ha="center", va="center")
ax.set_title("業務項目と画面・API・DBのトレーサビリティ")
ax.set_xlabel("設計要素")
ax.set_ylabel("業務項目")
ax.grid(False)
plt.tight_layout()
plt.show()
| 登録画面 | 一覧画面 | 登録API | 更新API | 依頼テーブル | 履歴テーブル | |
|---|---|---|---|---|---|---|
| 設備ID | 1 | 1 | 1 | 0 | 1 | 0 |
| 異常内容 | 1 | 1 | 1 | 0 | 1 | 0 |
| 優先度 | 1 | 1 | 1 | 0 | 1 | 0 |
| 担当者 | 0 | 1 | 0 | 1 | 1 | 1 |
| 状態 | 0 | 1 | 0 | 1 | 1 | 1 |
| 更新理由 | 0 | 0 | 0 | 1 | 1 | 1 |

結果の読み取り
更新理由は履歴テーブルと更新APIにありますが、入力する画面がありません。このままではAPIを直接利用しない限り記録できないため、状態更新ダイアログなどの追加が必要です。この行列は設計の完全性を保証するものではありませんが、レビュー時に「どこで入力し、どこで検証し、どこへ保存し、どこで見るか」を揃えるのに役立ちます。
15. No.009:業務フローからシステム機能を洗い出す
実務での意味
機能一覧を会議室だけで考えると、現場の引継ぎ、待ち、例外が抜けます。まず現状業務(As-Is)を、開始条件、担当、作業、判断、引渡し、記録、例外に分解します。そのうえで、システム化後(To-Be)に残す人の判断と自動化する処理を分けます。
分析・モデル化の考え方
フロー各工程の処理時間と待ち時間を分けます。全体リードタイムは
です。システムは作業時間だけでなく、情報が人から人へ渡る待ち時間を減らせる場合があります。
Pythonで確認する
workflow = pd.DataFrame({
"工程": ["異常発見", "班長へ連絡", "保全受付", "担当割当", "現場確認", "修理", "完了記録"],
"担当": ["作業者", "作業者", "班長", "保全責任者", "保全担当", "保全担当", "保全担当"],
"処理時間_分": [2, 3, 4, 3, 10, 55, 8],
"待ち時間_分": [0, 8, 12, 18, 10, 5, 25],
"候補機能": ["依頼登録", "自動通知", "受付一覧", "担当割当", "設備履歴参照", "処置記録", "完了・履歴保存"],
})
workflow["リードタイム_分"] = workflow["処理時間_分"] + workflow["待ち時間_分"]
display(workflow)
fig, ax = plt.subplots()
ax.bar(workflow["工程"], workflow["処理時間_分"], label="処理時間", color="#5B9BD5")
ax.bar(workflow["工程"], workflow["待ち時間_分"], bottom=workflow["処理時間_分"], label="待ち時間", color="#ED7D31")
ax.set_title("現状業務フローの工程別リードタイム(架空観測)")
ax.set_xlabel("工程")
ax.set_ylabel("時間(分)")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.xticks(rotation=25, ha="right")
plt.tight_layout()
plt.show()
| 工程 | 担当 | 処理時間_分 | 待ち時間_分 | 候補機能 | リードタイム_分 | |
|---|---|---|---|---|---|---|
| 0 | 異常発見 | 作業者 | 2 | 0 | 依頼登録 | 2 |
| 1 | 班長へ連絡 | 作業者 | 3 | 8 | 自動通知 | 11 |
| 2 | 保全受付 | 班長 | 4 | 12 | 受付一覧 | 16 |
| 3 | 担当割当 | 保全責任者 | 3 | 18 | 担当割当 | 21 |
| 4 | 現場確認 | 保全担当 | 10 | 10 | 設備履歴参照 | 20 |
| 5 | 修理 | 保全担当 | 55 | 5 | 処置記録 | 60 |
| 6 | 完了記録 | 保全担当 | 8 | 25 | 完了・履歴保存 | 33 |

結果の読み取り
修理そのものは最長ですが、担当割当や完了記録にも大きな待ちがあります。修理技能の改善には教育や標準作業が必要ですが、受付一覧、自動通知、担当割当、完了リマインドはシステムで待ちを減らせる候補です。なお、緊急停止時に入力を強制して安全行動を遅らせてはいけません。例外フローと事後登録を要件へ含めます。
16. No.010:小さな業務アプリの全体構成を設計する
実務での意味
最初の9ノックを統合し、最小実用製品(MVP)を設計します。利用者は作業者、保全担当、管理者。主要機能は依頼登録、未対応一覧、担当・状態更新、設備履歴、停止時間集計です。ブラウザ、Web/APIサーバー、DBを基本構成とし、監視、バックアップ、認証も「小さいから不要」とはしません。
分析・モデル化の考え方
構成選定では、性能、可用性、保守性、セキュリティ、費用を要件へ対応づけます。ここでは同時利用者数に対するp95応答時間を簡易シミュレーションし、単一構成で試験導入の負荷に余裕があるかを確認します。これは厳密な待ち行列モデルではなく、負荷試験前の仮説づくりです。
想定構成は次の通りです。
[工場PC/タブレット]
│ HTTPS
▼
[Web/APIサーバー] ── [監視・操作ログ]
│
▼
[業務DB] ── [日次バックアップ]
Pythonで確認する
architecture = pd.DataFrame([
["ブラウザ", "登録・一覧・更新", "入力負荷を下げる", "共有端末で利用確認"],
["Web/APIサーバー", "認証・業務ルール・集計", "ルールを一元化", "p95応答時間"],
["業務DB", "設備・依頼・履歴保存", "整合性と追跡性", "復元テスト"],
["監視・ログ", "障害・操作の記録", "早期検知と監査", "通知訓練"],
["バックアップ", "障害時の復旧", "データ損失を限定", "定期復元"],
], columns=["構成要素", "責任", "業務上の狙い", "主な検証"])
display(architecture)
users = np.array([5, 10, 20, 30, 40, 50])
simulation = []
for concurrent_users in users:
samples = 110 + 2.2 * concurrent_users + rng.gamma(2.0, 18 + concurrent_users * 0.8, 1000)
simulation.append([concurrent_users, samples.mean(), np.quantile(samples, .95)])
capacity = pd.DataFrame(simulation, columns=["同時利用者数", "平均応答時間_ms", "p95応答時間_ms"])
display(capacity.round(1))
fig, ax = plt.subplots()
ax.plot(capacity["同時利用者数"], capacity["平均応答時間_ms"], marker="o", label="平均")
ax.plot(capacity["同時利用者数"], capacity["p95応答時間_ms"], marker="o", label="p95")
ax.axhline(500, color="#C00000", linestyle="--", label="仮目標 500ms")
ax.set_title("同時利用者数と応答時間の簡易シミュレーション")
ax.set_xlabel("同時利用者数")
ax.set_ylabel("応答時間(ミリ秒)")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
| 構成要素 | 責任 | 業務上の狙い | 主な検証 | |
|---|---|---|---|---|
| 0 | ブラウザ | 登録・一覧・更新 | 入力負荷を下げる | 共有端末で利用確認 |
| 1 | Web/APIサーバー | 認証・業務ルール・集計 | ルールを一元化 | p95応答時間 |
| 2 | 業務DB | 設備・依頼・履歴保存 | 整合性と追跡性 | 復元テスト |
| 3 | 監視・ログ | 障害・操作の記録 | 早期検知と監査 | 通知訓練 |
| 4 | バックアップ | 障害時の復旧 | データ損失を限定 | 定期復元 |
| 同時利用者数 | 平均応答時間_ms | p95応答時間_ms | |
|---|---|---|---|
| 0 | 5 | 165.1 | 224.6 |
| 1 | 10 | 185.8 | 261.8 |
| 2 | 20 | 219.9 | 307.0 |
| 3 | 30 | 259.1 | 374.6 |
| 4 | 40 | 298.0 | 441.8 |
| 5 | 50 | 336.5 | 500.8 |

結果の読み取り
この仮定では50人同時利用でもp95は500ミリ秒未満です。したがって、まず単純な構成で試験導入し、実測後に増強を判断する方針が候補になります。ただし、シミュレーションは実データではありません。実導入前には、ピーク時操作、集計処理、通信断、DB障害、バックアップからの復元、権限違反を試験します。
MVPの境界も重要です。センサー自動連携、部品在庫、予知保全は将来候補とし、初期版では保全依頼の状態と時刻を確実に残すことへ集中します。
17. 対象ノックを通して見える実務上の示唆
10本を通して最も重要なのは、技術構成より先に「改善したい判断」を定めることです。本ケースでは、管理者が停止時間の長い設備を特定し、班長が未対応案件を見落とさず、保全担当が担当と履歴を共有できる状態を目指しました。
架空データからは、次の示唆を得られます。
- 平均だけでなくp95を見ると、一部の遅い体験を把握できる
- HTTPの4xxと5xxを分けると、現場教育とシステム改修を切り分けられる
- 画面・API・DBの対応表により、記録できない項目や使われない項目を発見できる
- 業務フローを処理と待ちに分けると、通知・割当で短縮できる箇所が見える
- MVPの範囲を絞ることで、実測データを早く得て次の投資判断へ進める
システム導入のKPIはログイン数や登録数だけでなく、初動時間、停止時間、未対応滞留、記録完全率、利用継続率で追います。停止時間の変化は生産量や繁忙度の影響を受けるため、導入前後を単純比較せず、ライン・設備・優先度を揃えて評価します。
kpi = requests.groupby("line").agg(
依頼件数=("request_id", "count"),
初動時間中央値_分=("response_min", "median"),
停止時間合計_時間=("downtime_min", lambda s: s.sum() / 60),
推定停止損失_百万円=("estimated_loss_yen", lambda s: s.sum() / 1_000_000),
).round(1)
display(kpi)
fig, ax = plt.subplots()
ax.bar(kpi.index, kpi["推定停止損失_百万円"], color="#ED7D31")
ax.set_title("ライン別の推定停止損失(架空データ)")
ax.set_xlabel("ライン")
ax.set_ylabel("推定停止損失(百万円)")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
| 依頼件数 | 初動時間中央値_分 | 停止時間合計_時間 | 推定停止損失_百万円 | |
|---|---|---|---|---|
| line | ||||
| A | 39 | 27.8 | 62.3 | 41.6 |
| B | 49 | 26.3 | 78.2 | 37.8 |
| C | 32 | 26.1 | 58.5 | 18.0 |

18. 実務導入する場合に必要なこと
実務導入では、notebookの試算から業務システムへ移るために、少なくとも次を実施します。
- 現場観察と合意形成:各シフトの実業務、緊急時、外注修理、引継ぎを観察し、用語と完了条件を揃える。
- データ定義:設備ID、優先度、原因、状態、時刻の定義と入力責任者を決める。既存設備台帳も整備する。
- 受入基準:例として「高優先度の未対応案件を3操作以内で確認できる」「状態変更を全件追跡できる」を定める。
- 非機能要件:同時利用、応答時間、稼働時間、認証、権限、監査、保存期間、バックアップ、復旧目標を決める。
- 小規模試行:1ライン・限定ユーザーで試し、入力時間、記録完全率、初動時間、問い合わせ件数を測定する。
- 運用設計:設備マスター管理、アカウント棚卸し、障害連絡、変更承認、教育、問い合わせ窓口を決める。
- 効果検証:導入前の基準値を保存し、設備構成や生産負荷を考慮して継続評価する。
個人情報をほぼ扱わないシステムでも、作業者名や操作履歴は管理対象です。最小権限、通信暗号化、パスワード管理、監査ログ、脆弱性対応を初期設計に含めます。また、安全・品質の正式記録となる場合は、社内規程や関連法規に沿って電子記録の要件を確認します。
19. まとめ
No.001〜No.010では、設備保全依頼という一つの業務を通じて、システム開発の目的から小規模アプリの全体構成までをつなぎました。
- システム開発は、業務・データ・人の判断・運用を一貫させる活動である
- 業務システムは目的、Webアプリは提供方式を表す
- フロントエンド、バックエンド、DBには異なる責任がある
- クライアントとサーバーはHTTPで要求と応答を交換し、APIが契約になる
- 要件、業務フロー、画面、API、DBを追跡可能にすることで設計漏れを減らせる
- 小さく導入し、KPIと利用実態を測ってから拡張する
次のノック群では、これらの設計を実装・運用できる開発環境とGitへ進みます。
20. 法人向けのご相談
数理工房では、製造業における業務整理、要件定義、データ基盤、可視化、AI・数理最適化を含むシステム開発をご支援しています。「表計算ファイルが増え続けている」「現場データを改善判断へつなげたい」「小さく試して投資効果を確認したい」といった段階から、課題と実装範囲を一緒に整理します。
📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。