IBM Bob と一緒にオフィス在席確認アプリ「ここいる」を作った話【実装編】
はじめに
仕様編 では、IBM Bob と会話しながら仕様やシステム構成を決めていったプロセスを紹介しました。
この記事では、実際にコードを書いてデプロイするまでの過程を紹介します。
この記事のポイント
- プロンプトを投げるだけでバックエンド・フロントエンドが一気に生成された
- エラーや問題が出ても会話で解決できた
- IBM Cloud へのデプロイもコマンド1つで完了した
開発環境
| 項目 | 内容 |
|---|---|
| OS | Windows 10 |
| エディタ | IBM Bob (VS Code 拡張) |
| Node.js | v20 以上 |
| IBM Cloud CLI | v2.33.1 |
Step 1:プロジェクト構成の生成
Bobに仕様を伝えると、まず以下のディレクトリ構成とファイルを一気に生成してくれました。
Imhere/
├── server.js # Express + WebSocket サーバー
├── package.json
├── manifest.yml # IBM Cloud CF 用
├── public/
│ ├── index.html # ログイン画面
│ ├── map.html # メイン画面(マップ + 在席一覧)
│ ├── app.js # GPS/気圧取得 + WebSocket クライアント
│ └── style.css
└── office-layout.pdf # オフィスレイアウト(後から差し替え)
ファイルを1つずつ書いてもらうのではなく、「こういうものを作りたい」と伝えるだけで構成ごと提案してくれたのは驚きでした。
Step 2:バックエンドの生成(server.js)
Bobが生成した server.js の主な構成はこちらです。
// Express + WebSocket サーバー
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// 位置情報を受信してブロードキャスト
wss.on('connection', (ws) => {
ws.on('message', async (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'location') {
await saveLocation(msg);
const all = await getAllLocations();
broadcast({ type: 'update', users: all });
}
});
});
ポイントは以下の3点です:
- IBM Cloudant に接続できない場合はインメモリで動作するフォールバック付き
- VCAP_SERVICES(IBM Cloud CF のサービスバインド)から自動で接続情報を取得
- WebSocket 切断時は5秒後に自動再接続
Cloudant パッケージの問題
最初に生成された package.json では古い cloudant パッケージが使われていました。Bobが自分でそれに気づき、最新の公式パッケージ @ibm-cloud/cloudant に更新してくれました。
# 古いパッケージを削除して最新に更新(Bobが自動で実施)
npm uninstall cloudant
npm install @ibm-cloud/cloudant
結果、脆弱性がゼロになりました。
Step 3:フロントエンドの生成
ログイン画面(index.html)
シンプルな名前入力フォームです。ログインボタンを押した際に位置情報の許可を確認してから map.html に遷移します。
<form id="loginForm">
<input type="text" id="nameInput" placeholder="例:田中 太郎" required />
<button type="submit">ログイン</button>
</form>
マップ画面(map.html / app.js)
Bobが生成した app.js の主な処理はこちらです。
GPS・気圧センサーの取得
// GPS(緯度・経度・高度)
navigator.geolocation.watchPosition((pos) => {
lastPos.lat = pos.coords.latitude;
lastPos.lng = pos.coords.longitude;
lastPos.altitude = pos.coords.altitude;
lastPos.floor = altitudeToFloor(pos.coords.altitude);
updateMyInfo();
}, options);
// 気圧センサー(HTTPS環境のみ動作)
const sensor = new AbsolutePressureSensor({ frequency: 0.1 });
sensor.addEventListener('reading', () => {
const alt = pressureToAltitude(sensor.pressure);
lastPos.floor = altitudeToFloor(alt);
});
sensor.start();
高度→階数変換
実測値ベースの閾値判定です。
function altitudeToFloor(altitudeM) {
if (altitudeM >= 49.0) return 6;
if (altitudeM >= 43.0) return 5;
if (altitudeM >= 39.1) return 2;
return 1;
}
WebSocket で5秒ごとに送信
const SEND_INTERVAL_MS = 5_000; // 5秒ごと
ws.onopen = () => {
sendLocation(); // 接続直後に即送信
sendTimer = setInterval(sendLocation, SEND_INTERVAL_MS);
};
Step 4:オフィスレイアウトの表示
当初はPDF.jsでPDFを表示する予定でしたが、PNG画像を用意したため画像表示に変更しました。
Bobが実装したバイリニア逆変換により、4隅の緯度経度から正確にキャンバス座標を計算します。
// 4隅の緯度経度(ダミー値)
const OFFICE_CORNERS = {
topLeft: { lat: 35.xxxxx, lng: 139.xxxxx },
topRight: { lat: 35.xxxxx, lng: 139.xxxxx },
bottomLeft: { lat: 35.xxxxx, lng: 139.xxxxx },
bottomRight: { lat: 35.xxxxx, lng: 139.xxxxx }
};
// Newton法反復でキャンバス座標を計算
function latLngToPixel(lat, lng, canvasW, canvasH, corners) {
let s = 0.5, t = 0.5;
for (let iter = 0; iter < 20; iter++) {
// ヤコビアンを使ってs,tを更新
s += ...; t += ...;
}
return { x: Math.round(s * canvasW), y: Math.round(t * canvasH) };
}
フロアごとに異なる緯度経度セットを持てる構造になっています:
const FLOORS = [
{ floor: 6, label: '6F', img: 'layout-6f.png', corners: DEFAULT_CORNERS },
{ floor: 5, label: '5F', img: 'layout-5f.png', corners: DEFAULT_CORNERS },
{ floor: 2, label: '2F', img: 'layout-2f.png', corners: DEFAULT_CORNERS },
{ floor: 1, label: '1F', img: 'layout-1f.png', corners: CORNERS_1F },
];
Step 5:IBM Cloud へのデプロイ
Cloud Foundry プラグインがない問題
最初は manifest.yml でCloud Foundryへのデプロイを想定していましたが、CFプラグインが未インストールだとわかりました。
BobはすぐにCode Engineへの切り替えを提案し、プラグインのインストールからデプロイまで一気に実施してくれました。
私のプロンプト:
リージョンは東京リージョンでリソースグループは「xxx-dev-default」を使用してください。
Bobが実行したコマンド:
# Code Engine プラグインのインストール
ibmcloud plugin install code-engine
# ターゲット設定
ibmcloud target -r jp-tok -g xxx-dev-default
# プロジェクト作成
ibmcloud ce project create --name kokoiru
# アプリのビルド&デプロイ(ソースから直接)
ibmcloud ce app create --name kokoiru \
--build-source . \
--build-dockerfile Dockerfile \
--port 3000 \
--min-scale 1 --max-scale 1
Dockerfileはシンプルな内容です:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
コマンド1つでビルドからデプロイまで完了し、URLが発行されました。
https://kokoiru.xxxx.jp-tok.codeengine.appdomain.cloud
Step 6:動作確認とフィードバック
PCからのアクセス
ローカルでも確認できましたが、PCではGPSの精度が低く(±63m)、高度情報も取得できませんでした。Bobが最初から言っていた通りです。
そこでBobが提案したのが手動フロア設定UIです。
階数を手動設定: [ 6F ▼ ] [ 送信 ]
PCでも「送信」ボタンを押すだけで自分のピンをマップに表示できるようにしました。
スマホからのアクセス
IBM CloudにデプロイされたURLにスマホでアクセスすると:
- ✅ HTTPS で気圧センサーが動作
- ✅ GPS精度が大幅に向上(±5m 程度)
- ✅ フロア自動判定が動作
- ⚠️ 位置に5m程度の誤差あり(屋内GPS限界)
Step 7:継続的な改善
デプロイ後も会話でどんどん改善を重ねました。代表的なものをご紹介します。
送信間隔の変更(30秒→5秒)
私のプロンプト:
サーバへの送信を5秒毎に変更した場合、スマホへの負荷がかかりますか?
Bobの回答:「5秒間隔でも通信量は約144KB/時、GPS連続取得の方がバッテリーへの影響が大きいため実用上問題ありません」
変更はコード1行だけでした:
const SEND_INTERVAL_MS = 5_000; // 30_000 から変更
変更後は ibmcloud ce app update --name kokoiru --build-source . で再デプロイ完了。
セッションタイムアウトの追加
古いセッションが残る問題を相談したところ、サーバー側に自動クリーンアップ処理を追加してくれました。
const SESSION_TIMEOUT_MS = 2 * 60 * 1000; // 2分
setInterval(async () => {
const now = Date.now();
const all = await getAllLocations();
const expired = all.filter(u =>
u.ts && (now - new Date(u.ts).getTime()) > SESSION_TIMEOUT_MS
);
for (const u of expired) {
await deleteLocation(u.name);
console.log(`[Cleanup] タイムアウト削除: ${u.name}`);
}
if (expired.length > 0) {
broadcast({ type: 'update', users: await getAllLocations() });
}
}, 30_000);
スマホUI対応
スマホでサイドバーが見えない問題を伝えたところ、タブ切り替えUIを実装してくれました。
[ 🗺 マップ ] [ 👥 メンバー ]
PC(幅700px以上)では従来の左右2カラム表示のまま、スマホではタブで切り替えるレスポンシブ対応です。
Step 8:ドキュメント・QRコードの生成
アプリが完成したので、Bobにドキュメント作成も依頼しました。
私のプロンプト:
一旦今の内容で仕様書とユーザ向けの操作手順書をmdファイルで作成してください。
すぐに以下が生成されました:
-
docs/specification.md:システム仕様書 -
docs/user-guide.md:ユーザー操作手順書
さらに:
QRコードはBobが外部APIを呼び出して自動生成してくれました。
// QR コード生成(Bob が Node.js スクリプトで実行)
const url = 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=' +
encodeURIComponent('https://kokoiru.xxxx.jp-tok.codeengine.appdomain.cloud');
https.get(url, res => { res.pipe(fs.createWriteStream('public/qr.png')); });
最終的なファイル構成
Imhere/
├── server.js # バックエンド(Express + WebSocket)
├── package.json
├── Dockerfile
├── manifest.yml
├── .env.example
├── public/
│ ├── index.html # ログイン画面
│ ├── map.html # マップ画面
│ ├── app.js # クライアントロジック
│ ├── style.css
│ ├── layout-1f.png # 各フロアのレイアウト画像
│ ├── layout-2f.png
│ ├── layout-5f.png
│ ├── layout-6f.png
│ ├── qr.html # QRコードページ
│ └── qr.png # QRコード画像
└── docs/
├── specification.md
└── user-guide.md
実装を通じて感じたこと
Bobが特に優秀だった点
1. エラーを自分で検知・修正してくれる
古いパッケージの脆弱性など、指示していないことも自発的に対応してくれました。
2. デプロイまで一貫してサポート
コードを書くだけでなく、IBM Cloud CLIのコマンド実行・エラー対応・再デプロイまで全部会話でできました。
3. 「1行の変更」で済む修正は1行だけ変える
送信間隔の変更のように、変更箇所が1行で済む場合はファイル全体を書き直さず最小限の修正をしてくれます。無駄がありません。
4. ドキュメントも書いてくれる
コードだけでなく、仕様書・操作手順書・スライド・QRコードまで、依頼したものはすべて生成してくれました。
苦労した点
1. スマホ実機での確認が必要
PCのブラウザではGPSや気圧センサーが動作しないため、IBM Cloudにデプロイしてスマホで確認するサイクルが必要でした。
2. ユーザー視点のフィードバックは自分でする
「フロアが自動で切り替わると他の階の人が見えなくなる」といった使い勝手の問題は、実際に動かしてみないとわかりません。Bobは言われたことを正確に実装してくれますが、何を作るかの判断は自分がする必要があります。
続編:IBM Bobと一緒にオフィス在席確認アプリ「ここいる」を作った話【実行編】
ぜひ皆さんも IBM Bob を試してみてください!
IBM Bobに興味がある方は、是非以下の弊社サイトからお問い合わせください。
https://www.ait-solution.jp/ai365/ibm_bob/
参考
本記事に記載のURL・アカウント情報はマスクしています。