はじめに
こちらは個人開発アプリができるまで by am10 Advent Calendar 2024の 3 日目の記事です。
3 日目はスプレッドシートを使ったサーバーサイドの実装についてです。
データ定義
家計簿に必要なデータを定義します。
こんなもんでしょう。
- ID(とりあえず主キーは必要)
- 日付(購入日)
- 品名
- 金額
- 項目(食費など)
- サブ項目(食品、飲料など)
ファイル名は「家計簿_yyyy」で年ごとにつくりシートを月ごとに「1 ~ 12」とログ用に「log」の 13 こ作成します。
1 ファイルにまとめてもいいのですがアプリに飽きた場合に家計簿としてみやすいようにこのような形にしました。
こんな感じです。1 行目はカラム名で 2 行目からデータ入力していきます。ID は UUID で項目とサブ項目は扱いやすさを考えて数値にしています。(ex. 項目の 0 が食費、サブ項目の 1 が飲料)
API 作成
API を作成していきます。必要なのは下記 2 つです。
- データ取得(年月を指定し指定の 1 ヶ月のデータを取得)
- データ登録(複数件同時可)
スプレッドシートの拡張機能 > Apps Script からコード.gs ファイルを開いて GAS でコードを書いていきます。
データ取得
データ取得から作っていきます。年月を指定して GET でリクエストでいいでしょう。
こんな感じのリクエストが来る想定です。
1 |
https://example.com?year=2024&month=1 |
こんな感じのレスポンスが返ってくる想定です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[ { "id":"365741BF-4AAC-48A6-B316-8A533358859B", "date":"20240101", "name":"コーラ", "price":178, "category":0, "categorySub":1 }, { "id":"333333BF-4AAC-48A6-B316-8A533358859B", "date":"20240102", "name":"コーラ", "price":178, "category":0, "categorySub":1 } ] |
実装はこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// GET function doGet(e) { const param = e.parameter; writeLog(param); const json = getJsonByYearMonth(param.year, param.month); const output = ContentService.createTextOutput(); output.setMimeType(ContentService.MimeType.JSON); output.setContent(JSON.stringify(json)); return output; } // logシートにログを書き込む function writeLog(param) { const spreadsheet = getSpreadsheetByFileName('家計簿_2024'); // ファイルは2024で固定 const sheet = spreadsheet.getSheetByName('log'); sheet.getRange(1,1).setValue(JSON.stringify(param)); } // 指定のファイルを開く function getSpreadsheetByFileName(fileName) { const files = DriveApp.getFilesByName(fileName); if (files.hasNext()) { return SpreadsheetApp.open(DriveApp.getFileById(files.next().getId())); } else { return null; } } // 年月を指定してシートからJSONのリストを作成する function getJsonByYearMonth(year, month) { // 指定の年月のシートを取得 const spreadsheet = getSpreadsheetByFileName('家計簿_' + year); const sheet = spreadsheet.getSheetByName(month); // データが入力されてる最終行までデータ取得を繰り返す const lastRow = sheet.getLastRow(); const json = []; // 1行目はカラム名なので2行目から取得する for (let row = 2; row < lastRow + 1; row++) { const data = getJsonByRow(row, sheet); json.push(data); } return json; } // 指定のシートの行を JSON にする function getJsonByRow(row, sheet) { const json = {}; json['id'] = sheet.getRange(row,1).getValue(); // 日付はDate型なのでyyyyMMddの文字列に変換する const dateValue = sheet.getRange(row,2).getValue(); json['date'] = Utilities.formatDate(dateValue, 'JST', 'yyyyMMdd'); json['name'] = sheet.getRange(row, 3).getValue(); json['price'] = sheet.getRange(row, 4).getValue(); json['category'] = sheet.getRange(row, 5).getValue(); json['categorySub'] = sheet.getRange(row, 6).getValue(); return json; } |
doGet(e)
が GET リクエストを受ける場所なのでここでデータ取得の処理を行います。
全体的な処理の流れは下記です。
- GET で年と月を受け取る
- 指定の年のファイルを開く
- 指定の月のシートを取得する
- シートの入力データを取得して JSON のリストで返却する
writeLog(param)
はなくてもいいのですがリクエストでエラーになったときに原因特定がむずかしいのでリクエストのパラメータを log シートに書き込むようにしています。
これでデータ取得の API は完成です。
データ登録
次にデータ登録です。データを JSON 形式のリストにして POST でリクエストでいいでしょう。
こんな感じのリクエストが来る想定です。
1 |
https://example.com |
body
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[ { "id":"365741BF-4AAC-48A6-B316-8A533358859B", "date":"20240101", "name":"コーラ", "price":178, "category":0, "categorySub":1 }, { "id":"333333BF-4AAC-48A6-B316-8A533358859B", "date":"20240102", "name":"コーラ", "price":178, "category":0, "categorySub":1 } ] |
こんな感じのレスポンスが返ってくる想定です。レスポンスは不要なのですがデータの整合性を保つために登録後に再度スプレッドシートから値を取得して返却します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[ { "id":"365741BF-4AAC-48A6-B316-8A533358859B", "date":"20240101", "name":"コーラ", "price":178, "category":0, "categorySub":1 }, { "id":"333333BF-4AAC-48A6-B316-8A533358859B", "date":"20240102", "name":"コーラ", "price":178, "category":0, "categorySub":1 } ] |
実装はこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
// POST function doPost(e){ const param = JSON.parse(e.postData.getDataAsString()); writeLog(param); return register(param); } // 登録処理 function register(param) { // 年月ごとにグルーピング const groupingObj = param.reduce(function (acc, obj) { // yyyyMがキーになる let key = Utilities.formatDate(convertToDate(obj['date']), 'JST', 'yyyyM'); if (!acc[key]) { acc[key] = []; } acc[key].push(obj); return acc; }, {}); const json = []; for (const key in groupingObj) { const items = groupingObj[key]; // 指定の年月のシート取得 const dateValue = convertToDate(items[0].date)); const year = dateValue.getFullYear(); const month = dateValue.getMonth() + 1; const spreadsheet = getSpreadsheetByFileName('家計簿_' + year); const sheet = spreadsheet.getSheetByName(month); // データが入力されている最終行を取得 let newRow = sheet.getLastRow(); for (const index in items) { const param = items[index]; // 最終行の1つ下の行にデータを追加 newRow = newRow + 1; sheet.getRange(newRow,1).setValue(param.id); sheet.getRange(newRow,2).setValue(convertToDate(param.date)); sheet.getRange(newRow,3).setValue(param.name); sheet.getRange(newRow,4).setValue(param.price); sheet.getRange(newRow,5).setValue(param.category); sheet.getRange(newRow,6).setValue(param.categorySub); // レスポンス用のデータ作成 const item = {}; item['id'] = param.id; item['date'] = Utilities.formatDate(new Date(param.date), 'JST', 'yyyyMMdd'); item['name'] = param.name; item['price'] = param.price; item['category'] = param.category; item['categorySub'] = param.categorySub; json.push(item); } } const output = ContentService.createTextOutput(); output.setMimeType(ContentService.MimeType.JSON); output.setContent(JSON.stringify(json)); return output; } // yyyyMMdd形式の文字列をDate型に変換する function convertToDate(yyyyMMdd) { // yyyyMMdd を年、月、日ごとに分割 var year = parseInt(yyyyMMdd.substring(0, 4), 10); // 年 var month = parseInt(yyyyMMdd.substring(4, 6), 10) - 1; // 月(0-11の範囲) var day = parseInt(yyyyMMdd.substring(6, 8), 10); // 日 return new Date(year, month, day); } |
doPost(e)
が POST リクエストを受ける場所なのでここでデータ登録の処理を行います。
全体的な処理の流れとしては下記です。
- POST でデータのリストを受け取る
- 各年月ごとにデータをグルーピングする
- 指定の年のファイルを開く
- 指定の月のシートを取得する
- シートのデータの最終行に新規データを追加する
- 入力したデータを JSON 形式で取得する
- 3~6 を繰り返す
- JSON 形式のリストを返却する
データ取得同様 writeLog(param)
で log シートにリクエストを書き出しています。
これで登録 API も完成です。
API を公開する
作った API を iOS アプリで使えるように公開します。本来であれば「実行可能 API」を選択するのですがお手軽なので今回は「ウェブアプリ」を選択します(今回の家計簿アプリはあくまで自分用なのでそこまでセキュリティを気にする必要はないかなと)。
- デプロイ > 新しいデプロイ > 種類の選択でウェブアプリを選択する。
- アクセスできるユーザーを「自分のみ」から「全員」に変更する
- 右下のデプロイ押下
- URL が表示されるのでメモする
注意:わりと強い権限を持っているのでこの URL は他人に見られないように保管しましょう。
これで公開は完了です!
修正する場合は「新しいデプロイ」をすると URL が変わってしまうのでデプロイの管理 > 編集 > バージョンで新バージョンを選択してデプロイします。
API を試す
完成した API を試してみます。
まずはデータ取得です。以下 URL をブラウザに入力してみてください(example.com の部分は公開した URL に変えてください)。
1 |
https://example.com?year=2024&month=1 |
添付のような表示になったら成功です。
次にデータ登録です。ターミナルを開いて以下コマンドを入力してみてください。
1 |
curl -X POST -H "Content-Type: application/json" -d '[{"id":"333333BF-4AAC-48A6-B316-8A533358859B", "date":"20240102", "name":"コーラ", "price":178, "category":0, "categorySub":1}]' https://example.com |
2024_家計簿ファイルのシート 1 にデータが追加されていれば成功です。
おわりに
これでサーバーサイドの実装は完了です!
今回は自分専用の家計簿アプリということでセキュリティ面をあまり考慮していません。ストアで公開する場合はもう少しセキュリティの考慮が必要です。指定のファイルが無かった場合や不正なリクエストの場合などエラー処理も今回は簡略化のため書いていません。
明日からはいよいよ iOS アプリを作っていきます。
コメント