【ハンズオン資料】MonacaとNCMBで経県値風アプリを作る

経県値というアプリは知っていますか？各都道府県毎に、その地域で経験した内容を記録していくことで累計ポイントを獲得するアプリです。

[経県値](https://uub.jp/kkn/#app)

このハンズオンではNCMBとMonacaを使って経県値風のアプリ（地図タップアプリ）を作ります。SVGを使って日本地図を表示し、経験したことをチェックしていきます。得点は出していませんが、ちょっとカスタマイズすればできるでしょう。

## コードについて

今回のコードは https://github.com/NCMBMania/memory-map-monaca-handson にアップロードしてあります。実装時の参考にしてください。

## 利用技術について

今回は次のような組み合わせになっています。

- Monaca
- Framework7
- D3.js

NCMBにおいては以下の機能を利用しています。

- 会員管理
    - 匿名認証
- データストア
    - データの保存
    - データの更新
    - データの取得

## 仕様について

地図のデータは[D3.jsで日本地図を描き、都道府県別に色を塗る - Qiita](https://qiita.com/ran/items/d88c5126362576be3291)に従って作成しています。topojsonは最新版ではコマンドラインオプションが変わっているので、1系をインストールして利用しています。

この作業によってできあがった日本のSVGデータは `www/assets/japan.json` として保存しています。

## 画面について

今回は以下の2つの画面があります。

### www/pages/map.html

SVGを使って日本地図を表示します。都道府県をタップすると、該当する地域に対する経験を登録できます。

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/28768ab9-c14e-5095-90c9-178425bbe28c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/28768ab9-c14e-5095-90c9-178425bbe28c.jpeg)

### www/pages/list.html

経験した内容を一覧表示する画面です。

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/fc8e28f8-a921-a28a-effe-b3f5f31bf1a6.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/fc8e28f8-a921-a28a-effe-b3f5f31bf1a6.jpeg)

### SDKのインストール

今回はMonacaのJS/CSSコンポーネントの追加と削除より、NCMBを追加します。アプリのテンプレートはFramework7のJavaScript版（VueやReactではなく）を選択しています。

## NCMBのAPIキーを取得

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)にてアプリを作成し、アプリケーションキーとクライアントキーを作成します。

## js/config.jsonの作成

js/config.jsonに、先ほど取得したNCMBのAPIキーを設定します。デフォルトの内容は次のようになっています。

```json
{
	"applicationKey": "YOUR_APPLICATION_KEY",
	"clientKey": "YOUR_CLIENT_KEY"
}
```

## 初期化

初期化は `js/app.js` にて行います。config.jsonを読み込む関係上、非同期処理内にて行います。cordovaの有無（アプリまたはプレビューの違いを検知）によって初期化時のイベント処理を変えています。

```jsx
// 記述済み
// NCMBの初期化用
const event = window.cordova ? 'deviceready' : 'DOMContentLoaded';
document.addEventListener(event, async (e) => {
	// この中に処理を書きます
});
```

config.jsonの内容を読み込んで、NCMBとFramework7の初期化を行います。

```jsx
// 記述済み
window.config = await (await fetch('./js/config.json')).json();
window.ncmb = new NCMB(config.applicationKey, config.clientKey);
window.app = new Framework7({
	name: 'My App', // App name
	theme: 'auto', // Automatic theme detection
	el: '#app', // App root element
	// App store
	store: store,
	// App routes
	routes: routes,
});

```

これでNCMBの初期化が完了します。

## ルーティング設定

今回は音声入力・出力用の1画面になります。これを `js/routes.js` に定義します。

```jsx
// 記述済み
const routes = [
  {
    path: '/',
    url: './index.html',
  },
  // 地図画面
  {
    path: '/map/',
    componentUrl: './pages/map.html',
  },
  // 一覧画面
  {
    path: '/list/',
    componentUrl: './pages/list.html',
  },
  // Default route (404 page). MUST BE THE LAST
  {
    path: '(.*)',
    url: './pages/404.html',
  },
];

```

匿名認証を有効にする

匿名認証とは、デバイスで生成したUUIDを使って認証を行う機能になります。自動生成した文字列なので、ユーザーがパスワードを入力したりする必要がなく、手軽に使えます。

管理画面のアプリ設定にして、匿名認証を有効化します。デフォルトでは無効なので注意してください。

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/667535ea-99ec-0632-c738-79064bc7e6b2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/667535ea-99ec-0632-c738-79064bc7e6b2.png)

## 匿名認証を利用する

匿名認証はアプリの起動時に利用するので `js/app.js` にて実行します。NCMBを初期化した後、まずログイン判定を行います。これはログインユーザー情報を取得し、 `null` 判定を行うことで可能です。 `null` ならばログインしていない状態です。

```jsx
// NCMBの初期化
window.ncmb = new NCMB(config.applicationKey, config.clientKey);
// ↑の下に↓を記述
// ログインユーザーを取得
const user = ncmb.User.getCurrentUser();
// ログイン判定
if (!user) {
  // ログインしていない場合は匿名ログイン
}
```

そしてログインしていない場合は以下のコードで匿名認証を実行します。たった1行のコードで認証処理が完了します。

```jsx
// ↓のコメントがあるところの下に記述
// ログインしていない場合は匿名ログイン
await ncmb.User.loginAsAnonymous();
```

## 地図を表示する

ここからはNCMBは関係なく、D3.jsなどの話になります。まず必要なライブラリを `www/index.html` にて読み込みます。位置情報を扱うためのD3 Geoなども読み込みます。

```html
<!-- 記述済み -->
<script src="<https://cdn.jsdelivr.net/npm/d3-array@3>"></script>
<script src="<https://cdn.jsdelivr.net/npm/d3-geo@3>"></script>
<script src="<https://cdn.jsdelivr.net/npm/d3@7>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.min.js>" integrity="sha512-4UKI/XKm3xrvJ6pZS5oTRvIQGIzZFoXR71rRBb1y2N+PbwAsKa5tPl2J6WvbEvwN3TxQCm8hMzsl/pO+82iRlg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
```

### ストアの作成

`japan.json` で取得できる都道府県一覧は、一覧画面でも利用します。そこでストアを作成しておきます。これは `www/js/stores.js` の内容です。

```jsx
// 記述済み
const createStore = Framework7.createStore;
const store = createStore({
  state: {
    prefectures: [], // 都道府県一覧
  },
  getters: {
    // 都道府県一覧を返却
    prefectures({ state }) {
      return state.prefectures;
    }
  },
  actions: {
    // 都道府県一覧をセット
    setPrefectures({ state }, prefectures) {
      state.prefectures = prefectures;
    },
  },
})
```

### www/pages/map.htmlの処理

では画面を作成します。地図を表示するのは `#map` になります。

```html
<!-- 記述済み -->
<div class="page-content">
	<svg id="map"></svg>
</div>

```

ここからはJavaScriptのコードです。画面がマウントされた際のイベント `$onMounted` に実装します。

```jsx
// 記述済み
$onMounted(async () => {
			await showMap();
｝）；
```

`showMap` 関数では、まずD3.jsでの描画部分を作成します。地図データなので、緯度経度を使って中心を指定できます。

```jsx
// 記述済み
const showMap = async () => {
	const map = document.querySelector('#map');
	const width = map.clientWidth;
	const height = map.clientHeight;
	const svg = d3.select("#map");
	const projection = d3.geoMercator()
		.center([136, 35.5])
		.scale(1000)
		.translate([width / 2, height / 2]);

```

そして `japan.json` から都道府県一覧を取得し、それをストアにも保存します。

```jsx
// 記述済み
const japan = await d3.json("../assets/japan.json");
pref = japan.objects.pref.geometries.map(d => d.properties.name_local);
$store.dispatch('setPrefectures', pref);
```

その `pref` を使ってTopoJSONを作り、D3.jsで描画します。

```jsx
// 記述済み
const topo = topojson.feature(japan, japan.objects.pref).features;
const view = svg.selectAll(".pref")
	.data(topo)
	.enter()
	.append("path")
	.attr("class", function(d) {
			return "pref pref" + pref.indexOf(d.properties.name_local);
	})
	.attr("d", path);

```

最後にズームイベント追加します。ただし、この実装では任意の場所をズームできず、使い勝手が悪いです。皆さんのアプリでは修正してください。

```jsx
// 記述済み
function zoomed() {
	const transform = d3.zoomTransform(this);
	this.setAttribute("transform", transform);
}
const zoom = d3.zoom()
	.scaleExtent([1, 5])
	.translateExtent([
		[-10, -10],
		[width + 90, height + 100]
	])
	.on("zoom", zoomed);
svg.call(zoom);
```

これで画面上に日本地図が表示されます。

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/1fdea388-a0f1-921b-ea96-48ac175e9b8a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/1fdea388-a0f1-921b-ea96-48ac175e9b8a.png)

## 都道府県をタップした際のイベント

各都道府県はSVGで描画されており、 `pref(都道府県ID)` という形式で描画されています。以下はその該当部分 `map.html` で、 `attr` の処理がそれに当たります。

```jsx
// 記述済み
const view = svg.selectAll(".pref")
	.data(topo)
	.enter()
	.append("path")
	.attr("class", function(d) {
			return "pref pref" + pref.indexOf(d.properties.name_local);
	})
	.attr("d", path);

```

この `.pref` クラスのタップ時のイベントを取得します。

```jsx
// 記述済み
// 地図をタップした際のイベント
$('.pref').on('click', (e) => {
	// タップした対象都道府県のID
	prefId = $(e.target).attr('class').match(/pref([0-9]+)/)[1];
	// 都道府県名
	prefecture = pref[prefId];
	// 表示更新
	$update();
	// アクションシートを開く
	sheet.open();
});

```

## シートモーダルについて

画面下などからシートを表示する機能はFramework7のものを使っています。まず、テンプレートになる部分をHTMLで作成します。

```html
<!-- 記述済み -->
<div class="sheet-modal sheet-modal-push" style="height: auto">
	<div class="sheet-modal my-sheet">
		<div class="toolbar">
			<div class="toolbar-inner">
				<div class="title">${ prefecture }</div>
				<div class="right"><a class="link sheet-close" href="#">保存</a></div>
			</div>
		</div>
		<div class="sheet-modal-inner list">
			<form id="status">
				<input type="hidden" name="prefId" value="${ prefId }" />
				<ul>
					${ status_values.map(value => $h`
						<li>
							<label class="item-radio item-content">
								<input type="radio" name="status" value="${ value }" />
								<i class="icon icon-radio"></i>
								<div class="item-inner">
									<div class="item-title">${ value }</div>
								</div>
							</label>
						</li>
					`)}
				</ul>
			</form>
		</div>
	</div>
</div>

```

これをシートモーダルとして定義します。

```jsx
// 記述済み
const sheet = app.sheet.create({el: '.my-sheet'});
```

`status_values` は経験したことのリストです。

```jsx
// 記述済み
const status_values = ['なし', '通過', '降りた', '歩いた', '泊まった', '住んだ'];
```

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9e38109c-550a-ae6b-c197-bde2265f0d73.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9e38109c-550a-ae6b-c197-bde2265f0d73.jpeg)

### 既存データをNCMBより取得する

すでに既存データがある場合に備えて、NCMBのデータストアからデータをダウンロードします。これは画面が表示された際のイベントで実行します。

`fetch` 関数は最初の1件だけを取得する関数です。今回はACLを使って自分のデータだけを表示・更新するよう制御しているので、この方法で問題ないでしょう。

```jsx
// 既存データ用オブジェクト
let prefectureStatus = {};
// 画面が表示された際のイベント
const beforeIn = async (e) => {
	// 既存データを取得
	prefectureStatus = await PrefectureStatus.fetch();
	// データがなければ新規作成
	if (!prefectureStatus.objectId) prefectureStatus = new PrefectureStatus();
	// 既存の入力情報を取得
	const status = prefectureStatus.status || {};
	// 都道府県の色を変更する
	Object.keys(status).forEach(prefId => {
		changeColor(prefId, status[prefId]);
	});
};
$on('page:beforein', beforeIn); // 画面が表示される前に実行
$on('page:tabshow', beforeIn); // タブ切り替え時に実行

```

`changeColor` 関数はステータスの値に応じて背景色を返す関数です。

```jsx
// 記述済み
const changeColor = (prefId, status) => {
	const dom = document.querySelector(`.pref${prefId}`);
	dom.style.fill = getColor(status);
};

const getColor = (status) => {
	switch (status) {
		case 'なし': return '';
		case '通過': return 'lightcyan';
		case '降りた': return 'lightskyblue';
		case '歩いた': return 'cornflowerblue';
		case '泊まった': return 'blue';
		case '住んだ': return 'navy';
	}
}

```

そして取得したデータを使って、シートがオープンした際に既存入力値を取得します。

```jsx
// シートを開いた際のイベント
sheet.on('open', (sheet) => {
	// 既存データの取得
	const values = prefectureStatus.status || {};
	const status = values[prefId] || 'なし';
	// ラジオボタンを選択状態にする
	$(`form#status [name="status"][value="${status}"]`).prop('checked', true);
});

```

## データを保存する

入力された後、シートを閉じるタイミングでデータを保存します。各都道府県ごとに1レコードではなく、同じレコードの中の `status` カラムにオブジェクト形式でデータを保存します。データの取得件数を減らすことで、ネットワークアクセスを高速化できます。

```jsx
// シートを閉じた際のイベント
sheet.on('close', async (sheet) => {
	// 入力データの取得
	const prefId = $('form#status [name="prefId"]').val();
	const value = $('form#status [name="status"]:checked').val();
	const params = { [prefId]: value };
	// 都道府県の色を変更する
	changeColor(prefId, value);
	// 既存データとのマージ
	const status = {...prefectureStatus.status, ...params};
	// データをセット
	prefectureStatus.set('status', status);
	// アクセス権限の設定
	const acl = new ncmb.Acl();
	const user = ncmb.User.getCurrentUser();
	acl.setUserReadAccess(user, true)  // 自分だけ読み込み可能
		.setUserWriteAccess(user, true); // 自分だけ書き込み可能
	prefectureStatus.set('acl', acl);
	// 新規保存または更新
	if (prefectureStatus.objectId) {
		await prefectureStatus.update();
	} else {
		await prefectureStatus.save();
	}
});

```

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/65099b69-cd5a-56f2-5b1c-69e3e235de5a.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/65099b69-cd5a-56f2-5b1c-69e3e235de5a.jpeg)

## データを一覧表示する

データの一覧表示は、地図と同じようにデータを取得するところからはじまります。

```jsx
// ストアから都道府県一覧を取得する
const { prefectures } = $store.getters;
// ステータスを定義する
let status = {};
$on('page:tabshow', async (e) => {
	// NCMBのデータストアを定義
	const PrefectureStatus = ncmb.DataStore('PrefectureStatus');
	// 自分のデータを取得する
	const prefectureStatus = await PrefectureStatus.fetch();
	// ステータスを取得する
	status = prefectureStatus.status || {};
	$update();
});

```

後は、この都道府県とステータスを使って一覧表示するだけです。

```html
<!-- 記述済み -->
<div class="page-content">
	<div class="data-table">
		<table>
			<thead>
				<tr>
					<th class="label-cell">都道府県</th>
					<th class="label-cell">経験</th>
				</tr>
			</thead>
			<tbody>
				${ Object.keys(prefectures.value).map(prefId => $h`
					<tr>
						<td class="label-cell">${ prefectures.value[prefId] }</td>
						<td class="label-cell">${ status[prefId] }</td>
					</tr>
				`)}
			</tbody>
		</table>
	</div>
</div>

```

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9591853e-04b0-8a0a-a248-fab099f25622.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9591853e-04b0-8a0a-a248-fab099f25622.jpeg)

## まとめ

今回のハンズオンでは、NCMBの以下の機能を利用しました。

- 会員管理
    - 匿名認証
- データストア
    - データの保存
    - データの更新
    - データの取得

NCMBには他にもファイルストアやソーシャル認証、スクリプト、プッシュ通知などの機能があります。ぜひそれらの機能も使って、素晴らしいアプリ開発にチャレンジしてください。