コミュニティを支える技術

この記事は [GMOペパボ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/pepabo) 12日目の記事です。

皆さん、コミュニティ活動していますか？
僕はやっています。

思い返せば僕は今年、

- [NaITE 実行委員](https://nagasaki-it-engineers.connpass.com/)
    - [Agile Japan 2019 長崎サテライト with NaITE 実行委員](https://nagasaki-it-engineers.connpass.com/event/125262/)
    - [4th 長崎 Software Quality and Development Gathering 実行委員](https://nagasaki-it-engineers.connpass.com/event/122763/)
    - もくもく会 主催
- [JaSST'19 Kyushu 実行委員](http://www.jasst.jp/symposium/jasst19kyushu.html)
- [PHPカンファレンス福岡2019 実行委員](https://phpcon.fukuoka.jp/2019/)
- [Laravel.tenjin 主催](https://laravel-tenjin.connpass.com/)
- [RailsGirls Fukuoka 2nd コーチ](http://railsgirls.com/fukuoka.html)

などなど、いろいろなコミュニティ活動に参加してきました。
今回はそんなコミュニティに運営・参加する中でこういう取り組みをしたよ！みたいなことをアウトプットできればなぁと思って記事をしたためました。
よろしければご覧ください。

## ランチマップ
大きめのイベントを開催する場合、県外の方が来られることもあるかと思います。
せっかく来ていただいたからには、地元が誇る美味しいものをたくさん食べてもらいたい！そう思いますよね！

しかしながらランチマップを作るにも、色々と手がかかることは間違いありません。
マップの情報を保持しておくデータベースを用意し、APIのエンドポイントを準備し、js側でAPIのレスポンスを元にマップをレンダリングする…ちょっとマップを公開したいだけなのに、構築がとても大変ですよね。

誰でも簡単にメンテできて、お店の追加が簡単で、なおかつ使い回せる…そんなランチマップがあればいいのに！ということで作りました。

### スクリーンショット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/36823188-5fcb-db5c-5897-e13f46b97d58.png)
https://jasst-kyushu-lunch.tosite.work/

### 概略
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/61991b90-8250-2bd4-3830-d87af25f8687.png)

1. あらかじめスプレッドシートに店情報（店名・タグ・店舗位置情報）を入力しておく
2. GASを用いてシート内のレコードをJSON形式に変換する
3. スプレッドシートをAPIサーバ化する
    4. https://qiita.com/howdy39/items/22068b3f768f0f9a757d
    4. https://qiita.com/takatama/items/7aa1097aac453fff1d53
4. js側で受け取ったJSONを元にマップをレンダリングする
    5. https://www.tam-tam.co.jp/tipsnote/javascript/post12851.html

🤪注意：Google側で **100秒間に500リクエスト** というレートリミットが設けられています🤪

### 実装

**GAS** 

```js
function getSheetData(sheetName) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  const rows = sheet.getDataRange().getValues();

  var ary = [];
  rows.map(function(row, index) {
    const obj = {};
    obj["name"] = row[0];
    obj["tags"] = row[1].split(',');
    obj['url'] = row[2];
    obj['lat'] = row[3];
    obj['lng'] = row[4];
    ary.push(obj);
  });
  return ary;
}

function doGet(e) {
  const data = getSheetData('publish');
  return ContentService.createTextOutput()
    .setMimeType(ContentService.MimeType.JAVASCRIPT)
    .setContent(JSON.stringify(data));
}
```

**GitHub**
https://github.com/tosite0345/jasst18kyushu-lunch

### 所感
特にjs側の実装がかなり前に作ったものなので、やり変えたいなあ、と思っています。
レートリミットのこともあるのでゆくゆくはサーバ立てるほうがいいのかなぁ、という感じですね。
とは言えGASって強力だなぁ…と改めて痛感しました。
スプレッドシートに権限さえ与えておけば、コードをいじらずに誰でもランチマップの修正ができるようになったのは大きいですね。

## アンケートフォーム自動生成
イベントを開催する上で大事なものは参加された皆様からのフィードバック。
生の声に耳を傾け、次回はもっといいイベントにしていきたい！誰もがそう思うことでしょう。

経験則でお話しすると、アンケートは紙のほうが回収率が高いような気がします。そのため、紙フォームは必須でした。
しかしながらGoogleフォームも使いたいと思いました。
アンケート項目は1つですが、回答フォームを別々に作るのは効率が悪いですよね。
人間なので入力間違いやレイアウト修正の手間などもありますし。

そんな課題を解決するべく、設定をスプレッドシートに書くことで紙・フォームどちらも自動で生成してくれるくんを作りました。

### スクリーンショット
**生成ボタン**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/4a7512d8-4014-0de4-5e98-a90788a17d9c.png)

**設定シート**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/ba1a33fc-a8c8-102f-ddad-d87a26be029a.png)

**アンケート**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/8fa73fce-adf4-8fc4-94a3-c0b8cd922d8d.png)

**フォーム**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/1cf4c24d-3dcb-1135-af15-e9ee8b3bf7fe.png)

### 概略
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/9826ad38-46b7-2dbe-89a6-406e5997e29b.png)

### 実装

**GAS**

```js
var folderId = 'folderId'; // e.g. https://drive.google.com/drive/folders/folderId

function getFolder(id) {
  return DriveApp.getFolderById(id);
}

// ドキュメント生成
function createDoc(body) {
  var document = DocumentApp.create("アンケート");
  var lines = "";
  var cnt = 1;
  body.forEach(function (line) {
    var text = "";
    var prefix = "";
    if (line.prefix.length !== 0) {
      prefix = line.prefix + cnt + '.';
      cnt++;
    }
    lines += "## " + prefix + line.title;
    if (line.required === true) {
      lines += " *必須";
    }
    lines += "\n";
    if (line.description.length !== 0) {
      lines += line.description + "\n";
    }
    lines += "<" + line.type + ">\n";
    if (line.option.length > 0) {
      if (line.type === 'radio') {
        line.option.forEach(function (o) {
          lines += o + " - ";
        });
        lines.split(0, -4) + "\n";
      }
      if (line.type === 'checkbox') {
        line.option.forEach(function (o) {
          lines += "□" + o + " ";
        });
        lines += "□その他（\t\t）";
      }
    }
    lines += "\n";
  })
  document.getBody().setText(lines);
  targetFolder = getFolder(folderId);
  
  //指定したフォルダに移動させる
  var docFile = DriveApp.getFileById(document.getId());
  targetFolder.addFile(docFile);
  DriveApp.getRootFolder().removeFile(docFile);
}

// フォーム生成
function createForm(body) {
  var form = FormApp.create("フォームアンケート");
  var targetFolder = getFolder(folderId);

  body.forEach(function(line) {
    if(line.type === 'text') {
      item = form.addTextItem();
    }
    if (line.type === 'header') {
      item = form.addSectionHeaderItem();
    }
    if(line.type === 'textarea') {
      item = form.addParagraphTextItem();
    }
    if(line.type === 'radio') {
      item = form.addMultipleChoiceItem().setChoiceValues(line.option);
    }
    if(line.type === 'checkbox') {
      item = form.addCheckboxItem().setChoiceValues(line.option).showOtherOption(true);
    }
    if (line.required === true && line.type !== 'header') {
      item.setRequired(true);
    }
    if (line.description.length !== 0) {
      item.setHelpText(line.description);
    }
    item.setTitle(line.title);
  })
 
  var formFile = DriveApp.getFileById(form.getId());
  targetFolder.addFile(formFile);  
  DriveApp.getRootFolder().removeFile(formFile);
}

function makeDocuments() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getSheetByName('publish');
  var count = sheet.getLastRow();
  var body = [];
  for(i = 2; i <= count; i++){    
    var prefix = sheet.getRange(i, 1).getValue();
    var title = sheet.getRange(i, 2).getValue();
    var description = sheet.getRange(i, 3).getValue();
    var type = sheet.getRange(i, 4).getValue();
    var required = sheet.getRange(i, 5).getValue();

    var lastCol = sheet.getRange(i, sheet.getMaxColumns()).getNextDataCell(SpreadsheetApp.Direction.PREVIOUS).getColumn();
    var option = [];
    if (lastCol > 5) {
      for (j = 6; j <= lastCol; j++) {
        option.push(sheet.getRange(i, j).getValue());
      }
    }
    body.push({prefix: prefix, title: title, description: description, type: type, required: required, option: option});
  }
  createDoc(body);
  createForm(body);
  Browser.msgBox("処理が完了しました。");
}
```

**参考**

- https://developers.google.com/apps-script/reference/forms

### 所感
突貫で実装したのでまだまだ回収の余地はありそうです。
特に紙のアンケート出力を全てマークダウンにしてCSSでスタイル当てるようにすればもっと手間を削減できそうなので、暇を見つけて改修する予定です。
フォームの自動生成、用途は限られそうだけど使い勝手はいいなぁという印象でした。

## アンケート結果通知
せっかく答えてもらったアンケート、すぐにでも見たい！と思うのが人情だと思います。
そこで、回答があったタイミングでSlackに通知するような仕組みを作りました。

### 概略
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165355/d5178191-c9da-3a7c-3f42-70c962c46325.png)

### 実装

```js
var postUrl = 'https://hooks.slack.com/services/xxxx/xxxx/xxxx';

function sendToSlack(message, channel)
{
  var username = 'IKENBAKO';
  var icon = ':smirk_cat:';
  var jsonData = {
    "channel" : channel,
    "username" : username,
    "icon_emoji": icon,
    "text" : message
  };
  
  var payload = JSON.stringify(jsonData);
  var options = {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };
  UrlFetchApp.fetch(postUrl, options);
}

function onFormSubmit(e)
{
  var body = "<!here> 意見が届きましたよ〜 \n\n";
  var itemResponse = e.response.getItemResponses();
  
  for (var j = 0; j < itemResponse.length; j++){
    var formData = itemResponse[j];
    var title = formData.getItem().getTitle();
    body += `*${title}:* ` + formData.getResponse();
  }
  sendToSlack(body, "#channel");
}
```

### 所感
この辺はググれば他にもいっぱい記事が出てくるのでそちらを参考にしていただけるとよさそうです。
https://qiita.com/pchan52/items/574e930a3cc42cf7f8b9
通知が来るとテンション上がりますね。

## その他構想など

- **定期通知くん**
    - GASで動くcronみたいなものを実装しようと思っています
    - リマインダーや定期通知をスプレッドシートに書くだけで実行するようにしたいと思っています
- **議事録自動生成くん**
    - 前回の議事録を自動コピーしつつ、過去の議事録をアーカイブするような何かを作ろうと思っています

## 終わりに

いかがでしたでしょうか？
GASやSlackなどを活用して、少ない工数でコミュニティ活動をもっとよいものにしていけるといいですね。
他にもこんな工夫をしているよ！こうすればもっといいよ！など、ご意見ありましたらお待ちしております☺️