kintoneアプリを管理するアプリ

#はじめに
kintonehive2020 Fukuokaに登壇した際、紹介したアプリを管理するアプリについてご紹介します。
もっと早く公開する予定でしたが、だいぶ遅くなってしまいました(;'∀')

##この仕組みを作った経緯
前からアプリ多いなとは感じていましたが、いざ確認してみると約800個のアプリが！！

上限の1000個までもうすぐやないかい！
ということで、社内で使っていないアプリの削除を投げかけたんです。

しかし、現状は40個ほど削除されただけ。。。

果たして、700個以上使っているのか？
そんなわけあるかい！(ノ ﾟДﾟ)ノ　＝＝＝＝　┻━━┻

そんな状況で、これはなんとかしければ！と思ったのがキッカケです。

##そして考える
削除依頼かけても全然減らないし、でもこのまま放置してても上限に達するのは目に見えてるorz
噂に聞く、アプリ作成権限を縛ろうか。。。

でも、業務効率化するためのkintoneなのに、
いちいち承認しないとアプリ作れないっていう非効率さ。。元も子もないのでは、、、？

使っていないアプリを勝手にこちらがピックアップして証拠を突き出せば
皆が良く言う「いや、使ってるんだよ(｀･ω･´)ｷﾘｯ」という謎の回答を一網打尽にできるのではないか。

でもどうやって、使ってないって証明するのか。

ん～そうだ！
アプリの閲覧数を取得すれば、使っていないアプリが分かるじゃね！？
こうして、閲覧数を取得する仕組みを考えました。


##仕組み解説
まず、アプリの詳細画面を開いたら
そのアプリが管理アプリにあるか確認し、あった場合はそのレコードの閲覧数やレコード番号を取得。
(詳細画面にするのか、一覧画面にするのかはあなた次第！)

そして、閲覧数を＋１してレコードを更新。ただこれだけの仕組み。
管理アプリにアプリ情報がない場合は、POSTして新しくレコード登録をします。

”1アプリで1日に実行できるAPIリクエスト数”に引っかかるのでは？というご意見をいただきましたが
弊社では、今のところ結構余裕があって全く問題なさそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583205/9640f2cc-bb59-95ba-34a3-35c0f37c58ed.png)



##アプリの準備
管理アプリの中身はこんな感じです。
閲覧数を取得するだけでなく、他にもアプリの一覧も兼ねているので
アプリの説明や検索しやすいように、タグを入れるフィールドもあったりしますが、今回はそれを省きます。

####フィールド情報

|フィールド名   |フィールドコード |フィールドタイプ |
|--------------|---------------|----------------|
|アプリ名       |appName        |文字列（1行）    |
|アプリID       |appId          |文字列（1行）    |
|リンク         |link           |リンク or 文字列（1行）|
|アイコン       |icon           |添付ファイル     |
|アプリ管理者   |adminUsers     |ユーザー選択     |
|アプリ管理組織 |adminOrgs      |組織選択        |
|閲覧数        |viewsNumber     |数値　　        |

####APIトークンの設定
歯車＞設定＞APIトークン＞生成する
生成されたAPIトークンを控えておいてください。
レコード閲覧、レコード追加、レコード編集にチェックを入れてください。

##JSコード
これは、アイコンを登録する際、kintone標準のアイコンを使用してた場合に活用します。
全部ではないのでご了承くださいm(__)m
※switchでもifでもどちらでも大丈夫です。

```js:icon.js
function kintoneIcon(iconKey) {
    var iconUrl = "https://static.cybozu.com/contents/k/image/icon/app/";
    switch(iconKey) {
        case "APP4":
            iconUrl += "appOrder.png";
            break;
        case "APP5":
            iconUrl += "appReport.png";
            break;
        case "APP15":
            iconUrl += "appGood.png";
            break;
        case "APP18":
            iconUrl += "appSupport.png";
            break;
        case "APP19":
            iconUrl += "appTimesheet.png";
            break;
        case "APP20":
            iconUrl += "appUserList.png";
            break;
        case "APP21":
            iconUrl += "appArchive.png";
            break;
        case "APP26":
            iconUrl += "appCalendar.png";
            break;
        case "APP31":
            iconUrl += "appContact.png";
            break;
        case "APP46":
            iconUrl += "balloon.png";
            break;
        case "APP47":
            iconUrl += "bargraph.png";
            break;
        case "APP48":
            iconUrl += "binder.png";
            break;
        case "APP49":
            iconUrl += "businessbag.png";
            break;
        case "APP50":
            iconUrl += "calculator.png";
            break;
        case "APP51":
            iconUrl += "calendar.png";
            break;
        case "APP52":
            iconUrl += "cashbox.png";
            break;
        case "APP53":
            iconUrl += "chemistry.png";
            break;
        case "APP54":
            iconUrl += "clip.png";
            break;
        case "APP55":
            iconUrl += "clipboard.png";
            break;
        case "APP56":
            iconUrl += "clock.png";
            break;
        case "APP57":
            iconUrl += "cloudup.png";
            break;
        case "APP58":
            iconUrl += "creditcard.png";
            break;
        case "APP59":
            iconUrl += "cup.png";
            break;
        case "APP60":
            iconUrl += "diamond.png";
            break;
        case "APP61":
            iconUrl += "disk.png";
            break;
        case "APP62":
            iconUrl += "documet.png";
            break;
        case "APP63":
            iconUrl += "equipment.png";
            break;
        case "APP64":
            iconUrl += "fileholder.png";
            break;
        case "APP65":
            iconUrl += "flash.png";
            break;
        case "APP66":
            iconUrl += "gasstation.png";
            break;
        case "APP67":
            iconUrl += "gear.png";
            break;
        case "APP68":
            iconUrl += "glasses.png";
            break;
        case "APP69":
            iconUrl += "laptop.png";
            break;
        case "APP70":
            iconUrl += "linegraph.png";
            break;
        case "APP71":
            iconUrl += "lock.png";
            break;
        case "APP72":
            iconUrl += "mail.png";
            break;
        case "APP73":
            iconUrl += "megaphone.png";
            break;
        case "APP74":
            iconUrl += "mouse.png";
            break;
        case "APP75":
            iconUrl += "openmail.png";
            break;
        case "APP76":
            iconUrl += "payment.png";
            break;
        case "APP77":
            iconUrl += "pencil.png";
            break;
        case "APP78":
            iconUrl += "percent.png";
            break;
        case "APP79":
            iconUrl += "piegraph.png";
            break;
        case "APP80":
            iconUrl += "piggybank.png";
            break;
        case "APP81":
            iconUrl += "pin.png";
            break;
        case "APP82":
            iconUrl += "printer.png";
            break;
        case "APP83":
            iconUrl += "research.png";
            break;
        case "APP84":
            iconUrl += "smartphone.png";
            break;
        case "APP85":
            iconUrl += "soroban.png";
            break;
        case "APP86":
            iconUrl += "telephone.png";
            break;
        case "APP87":
            iconUrl += "thinkingballoon.png";
            break;
        case "APP88":
            iconUrl += "tool.png";
            break;
        case "APP89":
            iconUrl += "userpass.png";
            break;
        case "APP90":
            iconUrl += "wallet.png";
            break;
        default:
            iconUrl += "appTableBlue.png";
            break;
    }
    return iconUrl;
}
```

<br>
ゲストスペースのアプリから管理アプリを参照したり更新したりすることを考慮して
kintoneのREST APIではなくXMLHttpRequestを使用しています。

```js:potal.js
(function() {
	'use strict';

	// 管理アプリID
	const POTAL_APP_ID = 9;
	// APIトークン
	const POTAL_API_TOKEN = 'pEobFKPJhW2vGCF28wy89yvE7GpViTzKbwgKDd3R';

	kintone.events.on("app.record.detail.show", function(e) {
		const APP_ID = kintone.app.getId();
		const APP_URL = location.href.split('show')[0];

		// ゲストスペース内アプリの場合
		const GUEST = APP_URL.match("guest");
		const GUEST_SPACE_ID = APP_URL.split('/')[5];
		const REST_API_URL = GUEST ? '/k/guest/' + GUEST_SPACE_ID + '/v1/' : '/k/v1/';

		// 管理アプリはカウントしない
		if (POTAL_APP_ID == APP_ID) return;

		let potalRecordId;
		let viewsCount = 0;

		let functions = {
			init: () => {
				/** url設定 */
				const query = encodeURIComponent('appId = "' + APP_ID + '"');
				const url = '/k/v1/records.json?app=' + POTAL_APP_ID + '&query=' + query;
				functions.getPotalData(url);
			},
			getPotalData: (url) => {
				/**管理アプリからアプリを取得
				 * @param url: リクエストURL
				 */
				let xhr = new XMLHttpRequest();
				xhr.open("GET", url);
				xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhr.setRequestHeader("X-Cybozu-API-Token", POTAL_API_TOKEN);
				xhr.onload = function() {
					if (xhr.status === 200) {
						let records = JSON.parse(xhr.responseText).records;
						// レコードがない
						if (records.length == 0) {
							functions.getAppData();
						}
						// ある
						else {
							// レコード番号保存
							potalRecordId = records[0].$id.value;
							// カウントを取得＆+1
							viewsCount = Number(records[0].viewsNumber.value) + 1;
							// アイコンの有無
							records[0].icon.value.length ? functions.putRecord() : functions.iconDownload();
						}
					}
				}
				xhr.send();
			},
			getAppData: () => {
				/**このアプリの情報を取得 */
				let appName, creator;
				return kintone.api(kintone.api.url("/k/v1/app", true), "GET", {
					id: APP_ID
				}).then(function(resp1) {
					// アプリ名とアプリ作成者を取得
					appName = resp1.name;
					creator = {code: resp1.creator.code};
					return kintone.api(kintone.api.url("/k/v1/app/acl", true), "GET", {
						app: APP_ID
					});
				}).then(function(resp2) {
					// アプリ管理者とアプリ管理組織を取得
					let adminUsers = [];
					let adminOrgs = [];
					resp2.rights.forEach(function(r) {
						if (r.appEditable) {
							if (r.entity.type == "USER") {
								adminUsers.push({code: r.entity.code});
							}
							if (r.entity.type == "ORGANIZATION") {
								adminOrgs.push({code: r.entity.code});
							}
							if (r.entity.type == "CREATOR") {
								adminUsers.push(creator);
							}
						}
					});
					functions.postRecord(appName, adminUsers, adminOrgs);
				}).catch(function(error) {
					functions.postRecord(appName);
				});
			},
			putRecord: () => {
				/**更新 */
				let body = {
					app: POTAL_APP_ID,
					id: potalRecordId,
					record: {
						link: {value: APP_URL},
						viewsNumber: {value: viewsCount}
					},
					__REQUEST_TOKEN__: kintone.getRequestToken()
				};
				let xhr = new XMLHttpRequest();
				xhr.open("PUT", "/k/v1/record.json");
				xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhr.setRequestHeader("Content-Type", "application/json");
				xhr.setRequestHeader("X-Cybozu-API-Token", POTAL_API_TOKEN);
				xhr.onload = function() {
					if (xhr.status === 200) {
						console.log(JSON.parse(xhr.responseText));
					}
				}
				xhr.send(JSON.stringify(body));
			},
			postRecord: (appName, adminUsers=[], adminOrgs=[]) => {
				viewsCount++;
				let body = {
					app: POTAL_APP_ID,
					record: {
						appName: {value: appName},
						link: {value: APP_URL},
						appId: {value: APP_ID},
						adminUsers: {value: adminUsers},
						adminOrgs: {value: adminOrgs}
					},
					__REQUEST_TOKEN__: kintone.getRequestToken()
				};
				let xhr = new XMLHttpRequest();
				xhr.open('POST', "/k/v1/record.json");
				xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhr.setRequestHeader("Content-Type", "application/json");
				xhr.setRequestHeader("X-Cybozu-API-Token", POTAL_API_TOKEN);
				xhr.onload = function() {
					if (xhr.status === 200) {
						potalRecordId = JSON.parse(xhr.responseText).id;
						functions.iconDownload();
					}
				}
				xhr.send(JSON.stringify(body));
			},
			iconDownload: () => {
				/**アイコンダウンロード */
				kintone.api(kintone.api.url("/k/v1/app/settings", true), "GET", {
					app: APP_ID
				}, function(resp) {
					let iconKey, iconName, url;

					// アップロードした画像アイコンの場合
					if (resp.icon.type == "FILE") {
						iconKey = resp.icon.file.fileKey;
						iconName = resp.icon.file.name;
						url = REST_API_URL + "file.json?fileKey=" + iconKey;

						let xhr = new XMLHttpRequest();
						xhr.open('GET', url);
						xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
						xhr.responseType = 'blob';
						xhr.onload = function() {
							if (xhr.status === 200) {
								let blob = new Blob([xhr.response]);
								functions.iconUpload(iconName, blob);
							}
						};
						xhr.send();
					}
					// kintoneに組み込みのアイコンの場合
					else {
						iconKey = resp.icon.key;
						iconName = iconKey + ".png";
						url = kintoneIcon(iconKey);

						let xhr = new XMLHttpRequest();
						xhr.open('GET', url);
						xhr.responseType = 'blob';
						xhr.onload = function() {
							if (xhr.status === 200) {
								var blob = new Blob([this.response]);
								functions.iconUpload(iconName, blob);
							}
						}
						xhr.send();
					}
				});
			},
			iconUpload: (iconName, blob) => {
				/**アイコンアップロード
				 * @param iconName: ファイル名
				 * @param blob: blob
				*/
				let formData = new FormData();
				formData.append("__REQUEST_TOKEN__", kintone.getRequestToken());
				formData.append("file", blob , iconName);

				let url = REST_API_URL + 'file.json';
				let xhr = new XMLHttpRequest();
				xhr.open('POST', url);
				xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
				xhr.send(formData);
				xhr.onload = function() {
					if (xhr.status === 200) {
						let fileKey = JSON.parse(xhr.responseText).fileKey;
						functions.putIcon(fileKey);
					}
				};
			},
			putIcon: (fileKey) => {
				let body = {
						app: POTAL_APP_ID,
						id: potalRecordId,
						record: {
								link: {value: APP_URL},
								icon: {value: [{fileKey: fileKey}]},
								viewsNumber: {value: viewsCount}
						},
						__REQUEST_TOKEN__: kintone.getRequestToken()
				};
				let xhr = new XMLHttpRequest();
				xhr.open("PUT", "/k/v1/record.json");
				xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhr.setRequestHeader("Content-Type", "application/json");
				xhr.setRequestHeader("X-Cybozu-API-Token", POTAL_API_TOKEN);
				xhr.onload = function() {
					if (xhr.status === 200) {
						console.log(JSON.parse(xhr.responseText));
					}
				}
				xhr.send(JSON.stringify(body));
			}
		}
		functions.init();
	});
})();
```

##コード保存
画面上の歯車＞kintoneシステム管理＞Javascript / CSSでカスタマイズから
この2つのファイルを添付して保存してください。

※kintonehiveではプラグインにしたものをご紹介しましたが
各アプリにプラグインを登録するのめんどくさくね？ということから
kintoneの自体に登録しちゃえばいいんじゃね？ということに気付きこのようになっております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583205/8ca2044e-b05c-f3ec-5d92-cdf804b4bcbc.png)


##最後に
普通はちゃんと設計して作るべきですが、ざっくり作った感じですいません(;'∀')
記事もダダダァっと書いたので読みにくいかもしれませんが、ご了承くださいm(__)m

ご参考程度になればという気持ちです。