本記事は Googleスプレッドシート&Excelアドベントカレンダー2020 の14日目です。
たまたま空きがあることに気付いたため、せっかくでしたらと急遽参加させていただきます! よろしくお願いいたします 🙇
こんにちは。トゥギャッター株式会社でインフラ周りのエンジニアをしている @MintoAoyama です。
Togetter はツイートを始めとした様々な情報を組み合わせてコンテンツを作り出すキュレーションサービスです。
2009年に誕生してから今年で12年目に突入し、現在も月間PV約1億、月間UU約2000万という規模感で成長を続けております。
リモートワークにおける "雑談"
皆さん!!! リモートワーク してますか???
コロナ禍の中、トゥギャッター社も全従業員がリモートワークに移行しておりました。気が付けば10ヶ月以上が経とうとしております。
上の記事でも触れられていますが、リモートワークにおいても続けられている慣習の1つが『カフェタイム』、いわゆる雑談の時間です。
- 決められた職種・チームとのやりとりしか行われない
- 同じチーム内であっても業務以外のコミュニケーションが行われない
このような状態が続くと、仕事上でのやり取りも徐々に課題が発生しやすくなります。
共通の関心事や繋がりのある状態が維持できると自然な気遣いが育まれ、「心理的安全性」も確立しやすくなります。
雑談、重要ですよ!
話すネタに困った時
雑談。メンバーによってはちょっとした相談事だったり、共有済みの話題があった場合は自然に始まりだすこともあります。
ただ、ネタに困ることはままあります。
「初対面の人にはありふれた天気の話題」と言われることもありますが、それ以外にもネタは用意しておきたいものです。
そこで、簡単なネタ提供システムを作りました。
リモートワーク下でも行ってる雑談タイム(通称"カフェタイム")。ネタに困ることがままあるので Spreadsheets + GAS + Webアプリ公開機能 で簡単なネタ提供システムを作った。チームによってネタに向き不向きがあるのでシート管理と相性が良い。 ※ 画面はサンプル(ごきげんようの"サイコロの目") pic.twitter.com/DLXqaHqHUz
2020-12-13 23:49:58GASなら簡単なWebアプリも実装できる
doGetメソッドなどを実装しアクセス許可などの設定を行うことで、GASはWebアプリとして公開できます。インフラの知識は不要です 👍
G Suite(Google Workspace)と連携していれば、シートだけでなくWebアプリ自体も組織権限下に置くことができます。
取り扱うデータなど、社外からのアクセスを考慮する必要が無くなるので便利! しかも稼働に当たり追加費用も不要 👌 すごいですね。
実装内容的には最終的に HTML を構成した文字列を return するわけですが、GASには生成に便利なインターフェースが用意されています。
テンプレート的に HTML / JS / CSS も別ファイルとして切り出せますし、スクリプトレットも用意されています。便利!
詳細はこの後説明します。
実装内容は「ランダムに出力する」だけ…
上にあるスクショのように、シートの内容はシンプル。
列状にネタを自由に書いてもらうことを想定しています。
そして実装内容もシンプル。列の内容をそのまま読み取り、ランダムに選び出しHTMLとして出力するだけ。
// main.gs
function doGet() {
// スプレッドシートIDで取得
let ssId = 'xxxxxxxxxxxxxxxx'
let ss = SpreadsheetApp.openById(ssId)
// 任意のシート名で取得
let sheet = ss.getSheetByName('テーマ')
// タイトル列を除き最後まで取得
let lastRow = sheet.getLastRow()
let values = sheet.getRange(2,1, lastRow).getValues()
// 2次配列で取れちゃうので1次配列に置き換え
let themes = []
for (let i = 0; i < values.length; ++i) {
themes.push(values[i][0])
}
// 空っぽいのは除いておく
themes = themes.filter(checkEmpty)
// ランダムで引く!
let theme = themes[Math.floor(Math.random() * themes.length)]
// 引いた結果を返す(正確には HtmlService などを活用しそれっぽくする)
return theme
}
function checkEmpty(element) {
return element !== undefined && element !== null && element !== '';
}
本来であればこれでほぼ完成です。
ただ、実際に色々触ってみると、気になる問題が出てきます。
引き直す時に速くしたい!(連打しても良い感じにしたい!)
イマイチだな〜と思うなどネタを引き直そうと思った時にリクエストが走るのもしんどい(特にGASのそれはシート読み込みも絡むからかレイテンシが高め)ので、ネタのリストはまるごとJS(HTML)に書き出しておく。最新のリストを読み込みたいときだけリロードするくらいの想定。 pic.twitter.com/1cHc8bTO76
2020-12-13 23:50:04そうです。今回のような実装でもレイテンシは 2-3 s 程度。
気にならない人もいるかもしれませんが、複数回引くことがあれば少し億劫になる人もいるでしょう。
不必要にリクエストを読んで、GASに負担を掛けるのも微妙ですしね。
リスト自体の量が許容範囲なら、そのままJS(HTML)に取り込んでしまいましょう。気軽に連打できるようになります 👍
以下は先程のコードに HtmlService
の利用を組み込んだバージョン。コメントが追加要素です。
// main.gs
function doGet() {
// "index.html" をテンプレートとする
let templete = HtmlService.createTemplateFromFile("index")
let ssId = 'xxxxxxxxxxxxxxxx'
let ss = SpreadsheetApp.openById(ssId)
let sheet = ss.getSheetByName('テーマ')
let lastRow = sheet.getLastRow()
let values = sheet.getRange(2,1, lastRow).getValues()
let themes = []
for (let i = 0; i < values.length; ++i) {
themes.push(values[i][0]);
}
themes = themes.filter(checkEmpty);
// テンプレート内の変数 "themes" に改行コード区切りにしつつ設定
templete.themes = themes.join('\n')
let htmlOutput = templete.evaluate()
// ページタイトルやviewportを設定
htmlOutput
.setTitle('雑談くん')
.addMetaTag('viewport', 'width=device-width')
return htmlOutput
}
function checkEmpty(element) {
return element !== undefined && element !== 0 && element !== null && element !== '';
}
以下がHTMLテンプレートの index.html
になります。
// index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
</head>
<?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
<body onload="updateTitle('<?= themes ?>')">
<div>
<div class="inner">
<h1> </h1>
<div class="choose">
<button class="choose" onClick="updateTitle('<?= themes ?>')">テーマを変更する</button>
</div>
<div>
</div>
</body>
</html>
<?= themes ?>
という形で先程設定した変数を読み込む位置を定義しています。
アクセス・リロード時やボタンを押した際に updateTitle()
を実行していることも分かると思います。
HtmlService.createHtmlOutputFromFile
で更にCSSとJSを読み込んでいるところにも注目ですね。
以下はCSSを定義した css.html
です(テンプレートとして読み込ませる上で便宜上 .html
になってます)。
<style>
div.inner {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translateY(-50%) translateX(-50%);
transform: translateY(-50%) translateX(-50%);
padding: 20px;
}
h1 {
font-size: 3.80vw;
font-family: "SF Pro JP", "SF Pro Display", "SF Pro Icons", "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", メイリオ, Meiryo, "MS Pゴシック", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div.choose {
text-align: center;
}
button.choose {
text-align: center;
font-size: 1.80vw;
display: inline-block;
padding: 0.3em 1em;
text-decoration: none;
color: #67c5ff;
border: solid 2px #67c5ff;
border-radius: 3px;
transition: .4s;
}
button.choose:hover {
cursor: pointer;
background: #67c5ff;
color: white;
}
</style>
なんとなーく見栄えを気にして色々書いていますが、システム要件上は最優先事項でない場合はお好みで実装してください。
そして以下が js.html
。
<script>
function updateTitle(lists){
let oldTheme = document.getElementsByTagName('h1')[0].childNodes[0].nodeValue
let themes = lists.split('\n')
let theme = choose(oldTheme, themes)
document.getElementsByTagName('h1')[0].childNodes[0].nodeValue = theme
}
function choose(oldTheme, themes){
while(true) {
let theme = themes[Math.floor(Math.random() * themes.length)]
if (oldTheme !== theme) {
return theme
}
}
}
</script>
updateTitle(lists)
は改行コード区切りの文字列から配列に戻し、ランダムで引いた結果を <h1>
の value
の書き換えに使ってます。
choose(oldTitle, titles)
では「ランダムで引く」、いわゆる抽選部を切り出したもので、同じものを連続で引かないような判定も入れていたりします。(見た目の値が変わってないと、実行できてないように見えちゃいますからね)
本当は何世代分か覚えていたほうが尚良いんでしょうけど、データの持ち方が更に複雑になりますし、リストの数が増やせればカバーできそうなので割り切ってる感じです。
こんな感じでどうでしょうか。割とお手軽な感じで実装できますよね。
データの扱いを含めて、様々な可能性を持っていることもお分かりいただけるかと思います。
今回のようなシステムの改善案を始めとして、他にもアイディアが出てくるのではないでしょうか?
ご存じなかった方は是非ご活用ください 💪💪💪