この記事はニフティグループ Advent Calendar 2020の15日目の記事です。
昨日は @kanishionori さんのリモートワーク下におけるスクラム導入でした。
ちょうどプランニングポーカーの話も出てましたね。今回自作したものは、実際に社内で使っているのでよかったら試してみてください。
2023/05/14 追記
手軽に使えるようにdockerで起動させるようにしました。
アプリの中身はほぼ変わっていませんが、ディレクトリ構成などはいろいろと変わったので、記事内容と違うところがありますが、ご容赦ください。
はじめに
今年もニフティグループ Advent Calendar 2020に参加します。
もう社会人2年目も終盤。1日48時間にしてほしい。
プランニングポーカーとは?
アジャイル開発などで工数を見積るときに使用するときに使います。
画像:https://www.surviveplus.net/ja/archives/137
特徴としては
- チーム全員で見積もる。
- 相対見積もり。
- 専用のカードを用いる。
より工数見積の精度を上げるために使われているようです。
詳しくはこちらの記事を参考に
https://www.mof-mof.co.jp/blog/column/agile-estimation-planning-poker
私のチームでは、当初物理的なカードを使用してましたが、人数が増えたのと、リモートワークが増えたため、web上で無料で使えるFirePoker.ioを使ってました。
FirePoker.ioはとても素晴らしいサービスですが、毎回部屋を作る作業が必要なのと、作った部屋ごとにリンクが変わってしまうため、その都度共有する必要があります。
(めんどくさがりでごめんなさいw)
なので、よりシンプルなものを自作しました。
こんな感じにできた
動作はシンプルです。
- 名前を入力して、部屋に入る。
- カードを選択。
- 全員がカードを出したらオープンボタン。
- 再見積もりするときはリセットボタン。
使用中の部屋は「IN USE」と表示。
入室後は45分後にデータが消去されるようにしました。
退出ボタンでも退出できる。
設計
使ったもの
- Node.js
- Express
- ejs
- Socket.IO
- Mongo.db
- TTLを設定して、時間経過したらデータを自動削除するようにしました。
社内ではEC2の中でforeverを使ってnodeをデーモン化してます。
ソース
公開するつもりなかったので、かなり雑に作ってます。お許しを。
はじめにで追記しましたが、dockerで起動させるようになっています。
ソースの説明
ディレクトリ
.
├── README.md
├── app.js
├── node_modules
├── package-lock.json
├── package.json
├── public
│ └── javascripts
│ │ ├── entrance_script.js
│ │ └── script.js
│ └── stylesheets
├── routes
│ └── index.js
└── views
├── entrance.ejs
└── index.ejs
- app.jsにサーバ側の処理を書いています。
- イメージ的には各クライアントからのデータをみんなに丸投げするのと、DB処理です。
- 部屋入室前
- テンプレートがentrance.ejs
- 処理はentrance_script.js
- 部屋入室後(プランニングポーカーするとこ)
- テンプレートがindex.ejs
- 処理がscript.js
名前と部屋名をセッションで保持するようにしたので、テンプレートは2個だけです。
カードまわりの処理の説明
全部説明すると膨大になるので、メイン部分のカードを選択するところからリセットまでの部分を説明したいと思います。
websocketを使う場合の処理の流れはこんな感じです。
例えば、カードを選択してから画面に反映されるまで。
信じられないかもしれませんが、自分ができるだけきれいに書いてこれ。自分でもびびってる。
websocket触る前のイメージは送信、受信基本1回ずつで処理をかけると思いましたが、サーバ側、クライアント側、それぞれ送信、受信をしてから画面描写などの処理をします。
カードをクリックしたときの処理
$('.card').on({
'click': function() {
card_num = $(this).attr('id');
// 自分の選んだ番号をセッションに入れる
window.sessionStorage.setItem('select_num',card_num);
// 名前と番号と部屋名をサーバに送信
socketio.emit('result_card_list', [card_num, name, room]);
// 自分の選んだ番号を表示
$(".select_num").remove();
var select_num_div = document.createElement('div');
select_num_div.className = 'select_num';
select_num_div.innerHTML = 'Your Choice < ' + card_num + ' >';
// 要素の先頭に追加
$('#select_panel').prepend(select_num_div);
}})
カード情報を受信して、クライアント側に送信するまで
socket.on('result_card_list', async function(result_arr){
var card_num = result_arr[0];
var name = result_arr[1];
var room = result_arr[2];
var result_number_arr = [];
var result_user_arr = [];
let client;
try {
client = await MongoClient.connect(url, connectOption);
const db = client.db(pokerdb);
const collection = db.collection(room);
var where = {name: name};
var set = {$set: {choice: card_num}};
// DBに書き込む
await collection.updateMany(where, set);
// カードを選択済みの人だけの情報を取得
const result = await collection.find({}).toArray();
for (let i=0;i < result.length; i++){
if (result[i]["choice"] != "") {
result_number_arr.push(result[i]["choice"]);
result_user_arr.push(result[i]["name"]);
}
}
// 部屋にカード情報を送信
io.to(room).emit('result_card_list', [result_number_arr, result_user_arr, result.length]);
} catch (err) {
console.log(err);
} finally {
if (client) client.close();
}
});
クライアント側でカード情報受信して画面に反映するまで
// カード情報受信
socketio.on('result_card_list',function(result_arr){
var result_user = result_arr[1];
var number_of_people = result_arr[2];
// カード(裏表示)を表示する
// カードオープン前なので、名前と人数だけでいい。
selectCardLineUp(result_user, number_of_people);
});
オープン、リセットも同じ流れ。
オープン
カード選択時とほぼ同じです。
DB書き込みはせずに、カードを表側で描写します。
オープンボタンを押して、クライアント側で送信
↓
サーバ側でオープン情報を受信
↓
部屋の中の情報を部屋の全員に送信
↓
クライアント側でオープン情報を受信
↓
カードを画面に反映
リセット
リセットボタンを押して、クライアント側で送信
↓
サーバ側でリセット情報を受信
全員のchoiceだけ空に
↓
リセット情報を部屋の全員に送信
↓
クライアント側でリセット情報を受信
↓
カードを画面から消す
MongDB
実際に使うドキュメントはこんな感じです。
{ "_id" : ObjectId("5fa950e0f1f3fb6568bdcfd3"), "name" : "さとう", "choice" : "100", "time" : ISODate("2020-11-09T14:23:28.903Z") }
- name
- ユーザー名
- choice
- 選択したカード
- time
- 入室時間
- MongoDBでTTLを設定するにはこのデータが必須です。
- 入室時間
TTLの設定
TTLの設定はコレクション(テーブル)ごとに設定が必要です。
例えば、room1で45分(2700秒)後にドキュメントを削除する設定する時。
# ターミナルで
mongo
> use testdb
> db.room1.ensureIndex({time:1},{expireAfterSeconds:2700});
自作した感想
初めてnodeとwebsocketを触ったので、正直ここまでできるとは思ってもいませんでした。かなりいい勉強になりました。
私は勉強のための勉強は苦手です。何かを作るために調べるとかだったらモチベあるので、今後も何か作りながら勉強したいです。
苦労したこと
- ロジックを考えること
- 似たようなアプリのソースを見つけられなかったので、ほとんど自分で考えました。
- その分いいロジックではないと思いますが、黒歴史として残しておきます。
- 非同期処理
- これも初めてだったので苦労
- DBの中身をどう削除するか
- 当初はTTLを知らなかったのでかなり悩みました。
よかったこと
- 勉強になった
- Node.js
- websocket
- MongoDB
- 一人で一からアプリを作る経験
- 実際にチームで使ってもらえた
- 作ったもので反応もらえるときが一番うれしいですね。
- 実際にFirePokerよりお手軽に使えてます。
nodeとwebsocketの雰囲気を掴むときにつかったもの
この記事が良さげだったので、第3回までやりました。
https://www.atmarkit.co.jp/ait/articles/1603/14/news015.html
上の記事ではNode.jsを使っていたのですが、そもそもNode.jsの書き方がわからなかったので、
UdemyのNode.js + Express で作る Webアプリケーション 実践講座
を受講しました。
最後に
やっぱり、なにか作りながらのほうが勉強になる。
明日は@kasayuさんの記事です。お楽しみに!