オタク向け蔵書管理アプリを35分で作ってみる #Appsheet

# オタクは蔵書管理アプリが欲しい

「あれ？　来月発売の〇〇って予約してたっけ。てか前巻は持ってるの？
電書も買ってるから紙媒体がどうだったかわからなくなってきたな……」
二次元オタクにはそういう日があります。
少なくとも私にはある。

**と、いうことで蔵書管理アプリを無料で簡単に作ってみました。**

↓アプリ画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/b9deef2d-8502-8717-2fab-751c30863d78.png)

まず初めに、今回の記事のはこちらの、 [AppSheetと20行のGoogle Apps Scriptで作るバーコード蔵書管理アプリ](https://qiita.com/takatama/items/fa141cb13ccc7324f887) を参考にしています。ありがとうございます！

基幹の内容はほとんど上記記事に依存しているので、普通のQiitaユーザはそちらを参考にした方がスマートです。
本記事はオタクが欲しかった追加機能の実装がメインとなります。

　 

:::note warn
「スプレッドシートってなに…？」ってレベルにいる人は下記の記事に移動してください！
:::

https://qiita.com/mhwtss/private/f87d407f852dea3ed81d

# アプリ基本仕様

管理したいものから、ある程度アプリの仕様を考えておきます。

## 1．機能設計と画面設計

今回管理したいのはとあるシリーズの書籍のみ、
そのシリーズの書籍は大きくわけて以下のような体系になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/ba0dcee0-a03c-2723-682e-2d8068f7a648.png)

これらのすべてが本好きの下剋上～から始まるタイトル、かつコミカライズに（コミックス）などの表記もないため、蔵書一覧を表示しただけでは何が何だか分からなくなることが予想されます。
せめて小説とコミカライズくらいは別の画面で確認したい……ということで、作ることにした初期の画面構想はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/4aa3aba9-2823-8bab-e03d-4a3a0325b8cd.png)

※データベースに入ってた情報の兼ね合いで、最終的に実装できたのは以下でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/75f96831-02b8-36f6-3e32-84cf2298dc3f.png)


## 2．使用サービスとデータ管理

基本的にGoogleの無料で使える範囲のサービスを利用しています。

画面の設計はAppSheet
データベースはスプレッドシート
書籍情報を検索してくるところだけコードを書いています。

書籍にはISBNコードという管理番号が振られているので、それを基に検索していきます。
（書籍にはバーコードが2段ありますが、それの上段です）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/9b60333a-d440-6a5a-0cc7-5d4e1bf31d8d.png)

# アプリを作る
実際に作っていきます。

だいたいがぽちぽちクリックで（わたしたち初心者には）難しいコードはコピペで済ませるので、**35分くらいでアプリ作成ができます！**
※スクリーンショットを撮るときにストップウォッチで測りました。

【アプリ作成手順】
1. スプレッドシートの用意
1. GoogleAppsScriptの用意
1. トリガを設定
1. AppSheetで基本のデータ設定をする
1. AppSheetで各画面用のデータセットを作る
1. AppSheetで各画面を作る
1. スマホアプリで確認

## 1．スプレッドシートの用意

まずはグーグルドライブにスプレッドシートを用意します。

スプレッドシートを新規作成し以下の作業を行います。
* ブックに名称をつける
* 1行目に必要な列名を記述
* シート名を'Books'に変更。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/62a51801-a433-e7d3-31da-d1661fb4db6e.png)


以下の列を用意しました。

```
ID　(アプリが作成するKEY)
ISBN　(ISBNコード)
Title　(書名)
Volume	(本来は巻号が入るが本好きの下剋上シリーズは未定義)
Series	(本来はシリーズ名が入るが本好きの下剋上シリーズは未定義)
Publisher	(出版社)
Pubdate　(出版年月)
Cover　(書影URL)
Author	(著者名)
Created at	(データ作成日時)
Updated at	(データ更新日時)
Purchased	(自分の購入状況)
PartNumber	(巻数)
ProductFormDetail　(版型)
```

これでスプレッドシート側の準備は終了です。

## 2．GoogleAppsScriptsの用意

次は検索機能をGoogle Apps Scriptというサービスを使って、検索機能を作っていきます。
スプレッドシートの[拡張機能]>[Apps Script]を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/221b8641-0d43-a479-6640-0ef34a742986.png" width=100%>

以下のような画面が開くので、
1. メインのコードを書くエリアにもともと書いてある内容を削除して、後ほど記載するコードをまるごとコピペ
2. 好きな名前を付ける
3. 保存

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/f2727db4-68a3-27c7-47e1-8704189e441a.png" width=100%>

```javascript:GAS
function onChangeSheet(e) {
  const sheet = SpreadsheetApp.getActive().getSheetByName('Books');

  //シートの記入されている範囲を1行目から順に最後まで処理
  sheet.getDataRange().getValues().forEach((row, i) => {
    const isbn = row[1], title = row[2];
    //ISBNがnot null かつ Titleがnullのとき　以外は次のForへ
    if (!isbn || title) return;
    //ISBNを基に情報を取得
    //sはsummary, tEはTitleElement, dDはDescriptiveDetail
    const [s, tE, dD] = fetchBookSummary(isbn);
    //情報をスプレッドシートに入力
    sheet.getRange(i + 1, 1, 1, row.length).setValues([[row[0], isbn ,s.title, s.volume, s.series, s.publisher, getDate(s.pubdate), fetchAmazonImageURL(isbn), s.author.slice(0,s.author.indexOf("／")), row[9], row[10], row[11],"'"+ tE.PartNumber,dD.ProductFormDetail]]);
  });

  //出版年月で昇順に並び替え、副次的に空白行が詰められていく
  adjustrows()
}

function fetchBookSummary(isbn) {
  const url = 'https://api.openbd.jp/v1/get?isbn=' + isbn;
  const res = UrlFetchApp.fetch(url);
  Logger.log(res);

  if (res=="[null]"){
    //ISBNで検索結果が得られなかった時、ダミーのJSON文字列を生成
    res2 = ['[{\
      "summary" : {"title":"ISBN見つかりません","volume":"","series":"","publisher":"","pubdate":"","author":""}, \
      "onix":{ProductFormDetail": "","TitleDetail":{TitleElement":{"PartNumber": "undefined"}}}}]'];
    var json = JSON.parse(res2);
  }else{
    //resが文字列のためJSONにParse
    var json = JSON.parse(res.getContentText());
  }

  const su = json[0].summary;
  var tiEle = json[0]["onix"]["DescriptiveDetail"]["TitleDetail"]["TitleElement"];
  var pFormDetail = json[0]["onix"]["DescriptiveDetail"];

  return [su, tiEle, pFormDetail];
}

function getDate(strDate) {
  return new Date(strDate.slice(0, 4), strDate.slice(4, 6), strDate.slice(6, 8));
}

function fetchAmazonImageURL(isbn){
  //本好きの下剋上はopenDBに書影が登録されていなかったので、ISBNコードを基にAmazonから書影を持ってくる
  const s = "http://images-jp.amazon.com/images/P/"+ toISBN10(isbn.toString()) +".09.THUMBZZZ.jpg"
  return s
}

function toISBN10(isbn13){
  // 1. 先頭３文字と末尾１文字を除く
  const src = isbn13.slice(3, 12);
  // 2. 先頭の桁から順に10、9、8…2を掛けて合計する
  const sum = src.split('').map(s => parseInt(s))
    .reduce((p, c, i) => (i === 1 ? p * 10 : p) + c * (10 - i));
  // 3. 合計を11で割った余りを11から引く（※引き算の結果が11の場合は0、10の時はアルファベットのXにする）
  const rem = 11 - sum % 11;
  const checkdigit = rem === 11 ? 0 : (rem === 10 ? 'X' : rem);
  // 1.の末尾に3.の値を添えて出来上がり
  return `${src}${checkdigit}`;
}

function adjustrows(){
  //スクリプトが紐づくスプレッドシートのアクティブなシートを読み込む
  let spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  let mySheet = spreadsheet.getSheetByName('Books');
  var lastRow = mySheet.getLastRow();
  var lastColumn = mySheet.getLastColumn();

  //シート内でソートしたいセル範囲をgetRangeで指定する
  let data = mySheet.getRange(2, 1, lastRow-1, lastColumn);
  //列Eを基準に降順でソートする
  data.sort({column: 7, ascending: true});

  Logger.log("整形しました");
}
```

## 3．トリガを設定

さきほどコピペしたコードが、スプレッドシートが変更されたときに実行されるようにトリガを設定します。

Apps Scriptの画面左側に時計のマークがあるのでそこをクリックすると以下のような画面になります。
画面右下にある [＋トリガーを追加] から新しいトリガを作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/7a6e34d2-2ecb-7d5c-2663-7faa661d4cc2.png" width=70%>

実行する関数を選択が「onChangeSheet」になっていることを確認し、
イベントの種類を選択を「変更時」に変更します。
エラー通知設定はお好きに決めていいと思います。

確認が終了したら右下の[保存]を押します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/e351b035-08f2-3b51-b044-665ced93a542.png" width =60%>

初めて保存を押したときに認証できませんでした～のようなポップアップが出て、何も起こらないことがありますが
諦めずにもう一度保存を押してください(笑)

すると、以下のような画面が起動します。
（この画面が出ずに普通に保存できた場合は次のステップ「4．AppSheetで各画面向けのデータを作る」に進んで構いません）

[詳細]を押して、詳細を表示し、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/5fe705f3-0bb9-5953-adae-0ed4a90766ea.png" width=70%>

[今回作ったスクリプト名]（安全ではないページ）に移動 をを押します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/c3473a5f-3a2e-fd25-50f2-a2e2a5246618.png" width=50%>

そうするとリクエストの許可を求めてくるので [許可] を押してください。

これで小難しいコードの設定は終わりです。
ちゃんと動いているか一応確認してみます！

試しにスプレッドシートのISBNの列に`9784864724241`と入力してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/0ebf8830-dff0-811e-2ce4-21a18181bff7.png)

数秒程度で本の情報が適応されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/e522bc26-ce7f-1532-04d0-4827723cc709.png)

ここでスプレッドシートの中身が空だと後でエラーが出ることがあるのでこのまま入力しておいてください。


## 4．AppSheetで基本のデータ設定をする
ここからはAppSheetというサービスでアプリを作ります。
もう一度スプレッドシートに戻って、今度は拡張機能からAppSheetの「アプリを作成」を読み出します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/439112d0-e5ab-78ca-4f12-6b2fda546743.png" width=80%>

まずはデータの設定です。
Dataメニューから[Columns]を選択すると[Books]が既にあるので選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/151ac857-4bae-c0cb-6d08-c2a1e628f795.png" width=50%>

開くと各列の設定画面が表示されるので、下記に記載するテーブルに合わせて設定をします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/23792e62-4df0-c770-2aa9-8962d6ace303.png" width = 80%>

| NAME | TYPE | KEY? | LABEL? | FORMULA | SHOW? | EDITABLE? | REWUIRE? | INITIALVALE | DISPLAY NAME | DESCRIPTION | SEARCH? | SCAN? | NFC? | PII? |
|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|
| _RowNumber| Number|    |    | =  |    |    |    | =        | = | =Number of this row      |    |    |||
| ID     | Text     | ☑ |    | =  |    | ☑ | ☑ |=UNIQUEID()| = | =  | ☑ |    |||
| ISBN   | Text     |    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ | ☑ |||
| Title  | Text     |    | ☑ | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
| Volume | Text     |    |    | =  |    | ☑ |    | =        | =  | =  | ☑ |    |||
| Series | Text     |    |    | =  |    | ☑ |    | =        | =  | =  | ☑ |    |||
|Publisher|Text     |    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
| Pubdate| Date     |    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
| Cover  |Thumbnall |    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
| Author | Text     |    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
|Created at|DateTime|    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
|Updated at|DateTime|    |    | =  | ☑ | ☑ |    | =NOW()   | =  | =  | ☑ |    |||
|Purchased| Enum    |    |    | =  | ☑ | ☑ |    | ="所持"  | =  | =  | ☑ |    |||
|PartNumber| Text   |    |    | =  | ☑ | ☑ |    | =        | =  | =  | ☑ |    |||
|ProductFormDeatail|Text||    | =  | ☑ | ☑ |    |="undefined"| = | = | ☑ |    |||


Purchesedだけは列の左の鉛筆のマークを押して詳細画面を開いて、リストを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/95983db0-dd39-8eee-ab27-9446301ba28c.png" width=60%>

この辺で一度画面右上の[Save]で保存しておきましょう。
エラーが出たらどこかで間違えていると思うので確認してください。
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/1717143b-bf43-93ea-bf9e-3abfef0afd7b.png" width=40%>

## 5．AppSheetで各画面用のデータセットを作る

画面設計をしたときに全件表示画面, 小説一覧, コミカライズ一覧, 予約済・購入予定一覧の4画面を作ることしていました。
全件表示の画面はそのままBooksのデータを表示しておけばいいですが、その他3画面はデータでフィルターをかけて表示する必要があります。

それぞれにフィルターをしたデータを作っておきます。

Slicesタブに移動して＋NewSliceをクリック
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/b1426c52-a0c8-e8a9-81fd-3b5b1b94f97c.png" width =50%>

スライスの名前をNovelsに設定し、
[Row Filter condition]のボックスを選択して出てくる[Create a custom expression]を選択してください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/90d47d28-0aad-952d-de6e-bccf6f906192.png" width =50%>

テキストボックスに`[Author] = "香月美夜"`と入力すると、Authorの項目が香月美夜先生のデータのみに絞られます。
右下の[Save]で保存してください。
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/68aa2845-0725-00c2-296d-dcc3225ff359.png" width =80%>

これで小説一覧用のデータセットは完成です。

この調子でコミカライズ一覧用、予約済・購入予定一覧用のデータセットも作っておきます。

コミックス一覧として作者が香月先生ではないものに限定
* [SliceName]：Novels
* [Row Filter condition]：`[Author] <> "香月美夜"`

予約済み・購入予定一覧として保有状況が"所持"ではないものに限定
* [SliceName]：no collect
* [Row Filter condition]：`[Purchased] <> "所持"`

これでデータセットの用意は終わりです。
いよいよ画面を作ります。

## 6．AppSheetで各画面を作る

メニューからUXを選択すると、すでに[Books]の画面が作成されています。
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/f3661218-57c6-ed62-3afe-f72d01d76ae2.png" width =50%>

一先ずこれに関してはViewTypeを[deck]に変更して閉じます。
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/e301c274-0c98-acf0-5f64-8473e63e98a8.png" width =60%>

残りの3画面を作っていきます。

[＋New View]から新しい画面を作成します。
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/f1ef8ece-8ed3-a13e-1279-9d791b52f3d5.png" width =50%>

設定は以下のようにしてください。

<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/2dccd379-72ee-3afb-5b8e-d68746a58444.png" width =50%>

* [ViewName]：Novels
* [For this data]：Novels(slice)
* [ViewType]：deck
* [Position]：center
* [Group by]：ProductFromDetailをAscending(昇順)
* [Main Image]：Cover
* [Primary header]：Title
* [Secondary header]：PartNumber
* [Summarycolumn]：Purchased

これで小説一覧の画面ができました。
同じ要領でほかの画面も作っていきます。

コミックス一覧画面
* [ViewName]：Comics
* [For this data]：Comics(slice)
* [ViewType]：deck
* [Position]：center
* [Group by]：AuthorをAscending(昇順)
* [Main Image]：Cover
* [Primary header]：Title
* [Secondary header]：PartNumber
* [Summarycolumn]：Purchased

予約済み・購入予定一覧画面は以下の設定で、購入ステータスが所持じゃないもの
* [ViewName]：no collect
* [For this data]：no collect(slice)
* [ViewType]：deck
* [Position]：center
* [Group by]：PurchasedをAscending(昇順)
* [Main Image]：Cover
* [Primary header]：Title
* [Secondary header]：ISBN
* [Summarycolumn]：Purchased

**右上の[SAVE]を押して画面の完成です！！**

## 7．できた！スマホアプリで確認だ！

アプリストアからAppSheetのアプリをインストールして、スプレッドシートを作ったのと同じグーグルアカウントでログイン。
左上のメニューからCreated by meを選択し、作成したアプリを表示します。

**アプリからISBNコードを読み取ると、すぐに反映されるし**
![834073DE-027F-4AF5-B37B-43359978EA90.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/02a35c88-9f9d-0007-ce79-90211a26fab1.gif)

**購入予定だけどほかのものと一緒に注文しよ～って思ってたものはISBNコードを手入力しておけば忘れない！**
**やった～！**
![7240E0FD-AA51-4F8F-A68A-D1A9DC3FB7F0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2565307/64bc7f98-ab77-f380-dd49-9e3619791933.gif)



# 最後に

「オタクが快適に暮らすためのローコード・ローコストアプリ開発」ということで初投稿をしました。
技術的なところは省いて、初心者がコピペでとりあえずやってみるという方針で書いてみています。

JSON内のデータがとんでもなかったりとか、紆余曲折あったのでまたサブ記事として何か用意していきたいと思います。
でもその前に、 **本好きの下剋上シリーズはグッズという難関がある** のでそちらのアプリ化が目下の目標です。

間違いなどあればご指摘ください。

# 参考記事

コードの参考
[AppSheetと20行のGoogle Apps Scriptで作るバーコード蔵書管理アプリ](https://qiita.com/takatama/items/fa141cb13ccc7324f887) 

Amazonから書影画像を持ってくるコード
[本のバーコードを読み取ってNotionで読書録を作成するアプリを作ってみた](https://qiita.com/nyoroko/items/452a8db449862f31ab2b)

スプレッドシートを並び替える
[GoogleAppsScriptでスプレッドシートのセルをソートする方法(sortメソッド)](https://auto-worker.com/blog/?p=2037)

OpenDBの構造
[OpenBD 書誌APIデータ仕様 (v1)](https://openbd.jp/spec/)

画面参考
[Step up Appli](https://step-up-appli.com/category/appsheet/)

版型情報
[List 175: Product form detail](https://onix-codelists.io/codelist/175)

本好きの下剋上
[公式サイト](http://www.tobooks.jp/booklove/index.html)