新卒2年目によるペットの体重管理アプリの作成記録

はじめに
---
当時のレベル感
* 新卒2年目半
* 理系卒（情報専攻ではないです）
* 大学生時代、授業で少しだけプログラミングしたことがある
* 新卒1年目の時から現在まででProcessing，openFrameworks，Pythonでコーディングしたことがある
* HTMLの\<p>って何だったっけ
* CSS，Javascriptはほぼ触ったことなし（名前はよく聞く）

メンバーとIoT機器を使ったプロダクトを作ろうということでアプリのコーディング部分担当に。
作成期間の昨年21年10月から今年22年3月までの約5か月間を振り返ります。

当初の計画
---
当初挙がった必要な機能とデータ
* 日付
* 個体番号
* 体重
* メモ書き
* 複数頭を管理できる
* それ以外にスマホで確認できると便利なデータがあれば


参考にしたものなど
---
始めに見た本は[できるポケット Web制作必携 HTML&CSS全事典](https://www.amazon.co.jp/%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89DL%E7%89%B9%E5%85%B8%E4%BB%98%E3%81%8D-%E3%81%A7%E3%81%8D%E3%82%8B%E3%83%9D%E3%82%B1%E3%83%83%E3%83%88-Web%E5%88%B6%E4%BD%9C%E5%BF%85%E6%90%BA-CSS%E5%85%A8%E4%BA%8B%E5%85%B8-Standard/dp/4295008281/ref=pd_sim_3/355-0056561-9543546?pd_rd_w=lzBuJ&pf_rd_p=ba248832-b816-4425-8851-893f63228155&pf_rd_r=GQC01661XQH3BNTVPYFC&pd_rd_r=81a18c7d-d8c0-45bf-ac34-3ff3370b2f16&pd_rd_wg=TgU8W&pd_rd_i=4295008281&psc=1)。
元々kindle unlimitedを利用していてkindle版は読み放題の対象でした。今もunlimitedで読めるようです。

色々出来ることがあるらしい程度しか理解できなかったので
ボタンのデザインなどサンプル集のようなサイトが出てくるものはそこからコピペ＋部分的にカスタマイズ。公式ドキュメント、作りたい機能に近いものを紹介しているサイトを都度参考にして進めました。



始めはHTML，CSS，Javascriptを並行してそれぞれの役割を理解した方が良いと思い
本を読んでみたものの、いざ打ってみようとしても全く手が進まなかったので
先輩方の助言を受けてソースのコピペでもいいからとりあえず部分的に機能を作成。


コピペしたソースを元にカスタマイズしていく中で
段々CSSとかHTMLのid,classでこんなことできるんだ～、となってきた感じでした。


Javascriptはとりあえずvarで宣言できる，DOM操作する感覚は新しかったけど
ループ文やif文は馴染みがあったのでHTML，CSSほど悩まなかったと思います。
一般的に良い書き方なのか分かってないのでJavascriptとjQuery混ぜて作成していたりとにかくgetElementIdしてます。





開発環境と作成した機能
---
* [Monaca](https://ja.monaca.io/)
* [ニフクラ mobile backend](https://mbaas.nifcloud.com/) (以下DBと呼びます)
[Monacaと連携するサンプルプログラム](https://github.com/NIFCLOUD-mbaas/monaca_login_template)があったのでインポートして、それを元に作成。
* 作成物

https://github.com/nanano08/petwait.git

作成した機能は以下。
* ログインに伴うDBからデータ取得機能
* DBとデータ連携
* グラフ機能
* メモ機能
* 全体的な画面のデザイン
* 新規登録に伴うデータ作成機能
* ペットのプロフィール情報登録機能



作成した機能の中からいくつか挙げていきます。

#### DBから体重，メモデータを取得する
ニフクラのドキュメントにある[基本的な検索の利用](https://mbaas.nifcloud.com/doc/current/datastore/basic_usage_javascript.html#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A4%9C%E7%B4%A2%E3%81%AE%E5%88%A9%E7%94%A8)を参考に作成。
DBのデータテーブルPetProfileClassから取得したpetNameなどの値を
getElementIdで取得したidのinnertextに値を入力。
一通り作成した後に見返すと、idと値が違う以外はほぼ同じ処理をしているので
ループ文の中にループ文書けばもっと短くかけそうな気がする。


```Javascript:app.js
//オブジェクトIDからペット情報を取得，情報を入力
var getObj = ncmb.DataStore("PetProfileClass");
getObj.equalTo("userObjectId", currentUserId)
      .order("createDate")
      .limit(2)
      .fetchAll()
      .then(function(results){
        //取ってきた値をlength分回して代入
        for (var i = 0; i < results.length; i++) {
             var object = results[i];

             var petName = document.getElementById("petName_" + i);
             var profileObjId = document.getElementById("profileObjId_" + i);
             var tagId = document.getElementById("tagId_" + i);
             var birth = document.getElementById("birth_" + i);
             var sex = document.getElementById("sex_" + i);
             var favorite = document.getElementById("favorite_" + i);
             var weak = document.getElementById("weak_" + i);

             var memopetName = document.getElementById("memoPetName_" + i);

            //if文で値がundifinedの場合は「未設定」と代入する
            if (object.petName === undefined) {
                petName.innerText = "名前未設定";
                memopetName.innerText= "名前未設定";
            } else {
            //PetProfileClassのpetNameの値を代入
            petName.innerText = object.petName;
            memopetName.innerText = object.petName;
            }

            if (object.objectId === undefined) {
                profileObjId.innerText = "未設定";
            } else {
            //PetProfileClassのobjectIdの値を代入
            profileObjId.innerText = object.objectId;
            }

            if (object.petTagId === undefined) {
                tagId.innerText = "未設定"
            } else {
            //PetProfileClassのpetTagIdの値を代入
            tagId.innerText = object.petTagId;
            }
                                    
            if(object.petBirth === undefined) {
               birth.innerText = "未設定";
            } else {
            //PetProfileClassのpetBirthの値を代入
            birth.innerText = object.petBirth;
            }

            if(object.petSex === undefined) {
               sex.innerText = "未設定";
            } else {
            //PetProfileClassのpetSexの値を代入
            sex.innerText = object.petSex;
            }

            if(object.petFavorite === undefined) {
               favorite.innerText = "未設定";
            } else {
            //PetProfileClassのpetFavoriteの値を代入
            favorite.innerText = object.petFavorite;
            }

            if(object.petWeak === undefined) {
               weak.innerText = "未設定";
            } else {
            //PetProfileClassのpetWeakの値を代入
            weak.innerText = object.petWeak;
            }

          }
         })
         .catch(function(err){
           var begugTest = document.getElementById("debugObjId");
           begugTest.innerText ="うまく表示できませんでしたもしくは該当なし";
         });
```




#### 体重データのグラフを描画する
[Chart.js](https://www.chartjs.org/)を使って体重データをペットごとに折れ線グラフで描画。
3.6.0バージョンのCDNを書いてもうまく動いていないようだったので2.7.1を使用。
縦軸は値に㎏の単位を付けて、DBにあるデータの作成日をlabelsdatesに入れているので横軸は年月日が入る。
実際は少ない頻度でも1か月に1回程度は体重を図ると思うので年は省いても良かったかも。その代わり、年はグラフの外に1箇所に書いておいた方が見やすいと思った。
2匹分のデータの書き方になっているので1，3匹の場合でも対応させるならまあまあ修正が必要そう。


```Javascript:app.js

  Chart.defaults.global.defaultFontColor = '#210D0D';
  Chart.defaults.global.defaultFontFamily = "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif";
  Chart.defaults.global.defaultFontSize = 12;

  const myChart = new Chart(ctx, {
    type: 'line',
    data: {
        labels: labelsdates,
        datasets: [{
            label: petNameLable_0,
            //pointHitRadius: 1,
            pointBackgroundColor: '#FFFFFF',
            data: getObjScore,
            backgroundColor: [
                'rgba(255, 255, 255, 0.1)',
            ],
            borderColor: [
                colorLabel_0,
            ],
            borderWidth: 2,
            lineTension: 0,
            spanGaps: true,
        },
        {
            label: petNameLable_1,
            //pointHitRadius: 1,
            pointBackgroundColor: '#FFFFFF',
            data: getObjScoreKuroSiro,
            backgroundColor: [
                'rgba(255, 255, 255, 0.1)',
            ],
            borderColor: [
                colorLabel_1,
            ],
            borderWidth: 2,
            lineTension: 0,
            spanGaps: true,
        }]
    },
    options: {
      responsive: true,
      //widthとheightサイズ変更が発生した時に元のキャンバスのアスペクト比を維持しない
      //maintainAspectRatio: false,
      layout: {
            padding: 5
        },
      scales: {
        yAxes:[ 
          {
            ticks:{
                beginAtZero: true,
                stepSize: 1.0,
                callback: function(value, index, values) {
                        return value + "Kg";
              }
            }
          }
        ],
        xAxes:[
          {
          }
        ]
    }
      },
      plugins: {
        legend: {
          labels: {
              // This more specific font property overrides the global property
              font: {
                  //size: 14
              }
          }
        },
        tooltips: {
        }
      } 
    });
}
```

#### 編集したメモをDBに送信する
メモデータをDBから取得したときにメモデータのオブジェクトIDも取ってきているので
そのオブジェクトIDを元に該当するメモデータを検索。
メモ編集画面のtextareaの値を、メモデータに上書き保存させている。
保存ボタン押した後に、保存出来た時とエラーが発生した時で表示するメッセージが違うように作った。


```Javascript:app.js
$("#sendMotal").click(function(){//クリックしたらメモ編集のtextarea内容をDBに送信
    //DBで検索するobjectIdを変数に格納する
    const searchObjId = document.getElementById("chooseObjId");
    //textareaの内容を変数に格納する
    const sendMemoText = document.getElementById("modalMotal_p_03").value;
    //DBへ送信する
    var sendMemo = ncmb.DataStore("PetRecordClass");
    sendMemo.equalTo("objectId", searchObjId.innerText)//objectIdで一発検索する
          .limit(1) //取ってくる値の数
          .fetchAll()
          //function(results)はJavaScriptの即時関数なのでequalToからcatchの;まで１つの動作
          .then(function(results){
              var object = results[0];
              object.set("dailyMemo", sendMemoText)
                    .update()
                    //.save()
                    .then(function(result){
                            //保存後の動作
                            $(function(){
                                $(".afterMotal").removeClass("hide--afterSendMotal");//表示できる
                                document.getElementById("afSendMsg_p1").innerText = "保存完了";
                                document.getElementById("afSendMsg_p2").innerText = "タップして閉じる";
                            });
                        })
                        .catch(function(error){
                            //エラー処理
                            $(function(){
                                $(".afterMotal").removeClass("hide--afterSendMotal");//表示できる
                                document.getElementById("afSendMsg_p1").innerText = "保存エラー";
                                document.getElementById("afSendMsg_p2").innerText = "もう一度お試しください";
                            });
                        });
          })

```

#### プロフィール編集の各項目に値が入力されているか確認する
値が入っていない場合は更新しない項目になる、値を空に更新しないための確認動作。
一般的にこういう確認動作が入るものなのかは分からない。が、データが入っていた箇所を空に更新するのを防いだ方がいいと思うので。ここら辺はjQueryで作成。
後半に手をつけたのでgetElementIdをプロフィールの項目数の分、書くのが面倒になりループ文にループ文入れてチェック回している。最後にこのあと紹介する実際の送信動作をする関数finalCheckSendに引数を渡している。


```Javascript:app.js
$("#sendProfile").click(function(){//クリックしたらプロフィール編集のinputに値が入っている項目を確認，値を別の関数（最終確認のポップアップと実際の送信動作をする）に入れる
    //DBで検索するobjectIdを変数に格納する
    var searchObjId = document.getElementById("profileObjId").innerText;
    //inputのタグ名を配列に入れておく
    var inputItemsArray = ["petNameInfo", "tagIdInfo", "birthInfo", "sexInfo", "favoriteInfo", "weakInfo"];
    //フラグを格納する配列
    var infoLengthArray = new Array(5);
    //文字列を格納する配列
    var updateProData = new Array(5);
    // inputItemsArray配列の長さ分for文回す
    for (var i = 0; i < inputItemsArray.length; i++) {
        if (document.getElementById(inputItemsArray[i]).value.length === 0) {
            //文字列の長さが０，値が入っていない場合はフラグを０とする
            infoLengthArray[i] = 0;
        } else {
            //文字列の長さが０でない，値が入っている場合はフラグを１とする
            infoLengthArray[i] = 1;
        }
        //文字列を配列に格納
        updateProData[i] = document.getElementById(inputItemsArray[i]).value;
        $("#" + inputItemsArray[i]).val("");
    }
    //実際の送信動作をする関数に，オブジェクトIDと２つの配列を引数として渡す
    finalCheckSend(searchObjId, infoLengthArray, updateProData);

    //デバッグ確認用
    //var debugCheck = document.getElementById("debugjsinside");
    //debugCheck.innerText = updateProData.length;

    $("#closeProfile").click();
  })

```
#### DBにプロフィール情報を送信する
引数の値を元にオブジェクトIDからプロフィールデータを検索。
フラグが立っている（値が入っている）ならデータをセット、フラグが立っていない（値が入っていない）なら何もしない。最後にデータを更新。
メモと同じように保存ボタン押した後に、保存出来た時とエラーが発生した時で表示するメッセージが違うように作った。

```Javascript:app.js
function finalCheckSend(sendObjId, infoFlags, sendProDatas) {
    //DBへ更新する情報を送信する
    var sendUpdateData = ncmb.DataStore("PetProfileClass");
    sendUpdateData.equalTo("objectId", sendObjId)//objectIdで一発検索する
                  .limit(1) //取ってくる値の数
                  .fetchAll()
                  //function(results)はJavaScriptの即時関数なのでequalToからcatchの;まで１つの動作
                  .then(function(results){
                      //フィールドの名前配列
                      const fieldNames = ['petName', 'petTagId', 'petBirth', 'petSex', 'petFavorite', 'petWeak'];
                      var object = results[0];
                      for (var i = 0; i < infoFlags.length; i++) {
                          if (infoFlags[i] === 0) {
                              //フラグ０の場合は何もしない
                          } else {
                              //フラグ１の場合はobjectにセットする
                              object.set(fieldNames[i], sendProDatas[i]);
                                    //.save()
                          }
                      }
                      object.save();
                      //保存したobjectオブジェクトを更新
                      object.update();
                  })
                  .then(function(result){
                      //更新後の処理
                      $(function(){
                        $(".afterMotal").removeClass("hide--afterSendMotal");//表示できる
                        document.getElementById("afSendMsg_p1").innerText = "保存完了";
                        document.getElementById("afSendMsg_p2").innerText = "タップして閉じる";
                      });
                  })
                  .catch(function(error){
                      //エラー処理
                      $(function(){
                        $(".afterMotal").removeClass("hide--afterSendMotal");//表示できる
                        document.getElementById("afSendMsg_p1").innerText = "保存エラー";
                        document.getElementById("afSendMsg_p2").innerText = "もう一度お試しください";
                      });
                  });



}

```



苦労した点
---
* 序盤（10~11月くらい）
ソースコピペしてなんとなくそれっぽい見た目を作れてもCSSの background-color を変えるぐらいしか分からない… \:cry: みたいな時期があったりしました、懐かしい。
あとは[ニフクラ mobile backendのドキュメント](https://mbaas.nifcloud.com/doc/current/datastore/basic_usage_javascript.html)を元にデータを送る書き方を別のスクリプトで試し書きして練習？確認のような事もしていた。

* 中盤（1~2月ぐらい）
CSSで見た目の調整、配色に悩み、とにかくgetElementIdしてループして必要があればif文で引っかけてるけどこれでいいのかと迷いがありながら作成してた時期だったと。技術的な苦労よりも方針で苦労していたような。もう少し頻繁にメンバーにフィードバックを求めてみた方が良かったかもしれない。

* 終盤（3月）
本番環境のニフクラとアプリ連携時に足りない機能が複数発覚（自分が気づいてなかった）。
成果物発表前日の夜に本番のコードに機能を書き足す、
前日夜に修正した一部の箇所が当日午前中に確認すると修正前に戻っていた（保存できてなかった？）のでまた修正。連携にここまで時間取られるとは思っていなかったので次回があったらスケジュールは今回の2倍は時間を確保しておこうと思いました。苦労というより反省点。




DBと連携させるとこんな感じ
---

##### ログイン画面
![スクリーンショット 2022-03-28 112109.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/5d0a2605-9de7-897d-5ac9-9362396c68d9.png)


##### 新規登録画面
![スクリーンショット 2022-03-28 113230.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/6a6b8260-30f5-6086-6348-7e8229e0a8fe.png)

##### ログインまたは新規登録後のトップページ
プロフィールページで名前を設定していればメモ管理に名前が表示される。
![スクリーンショット 2022-04-05 095857.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/044ab4ee-f05e-4496-62b0-ce89c790ee17.png)
または
![スクリーンショット 2022-03-28 114516.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/5bc9ba55-b7d7-f5bc-deb6-bfc4b4f02730.png)

##### グラフ
![スクリーンショット 2022-04-05 095933.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/0baabeca-db91-d9d7-79cc-ae856384a609.png)


##### メモ管理
名前をタップして表示の切り替えができる。
![スクリーンショット 2022-04-05 095958.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/7971e686-1042-1526-dcc1-5136140d9e4e.png)
![スクリーンショット 2022-04-05 100017.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/a57fb00c-9e1d-4f67-07ce-31f3172fbccd.png)


##### メモの編集画面
![スクリーンショット 2022-04-04 101529.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/1c5da568-2842-d099-eb0b-5de774a35cd1.png)


##### ペットのプロフィールページ
プロフィール情報が入っていればこんな風に表示される。
![スクリーンショット 2022-04-05 100054.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/f552e400-d83d-fb17-675a-fe6508de6f2f.png)
![スクリーンショット 2022-04-05 100122.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/2fcb4659-47a5-24b8-ed7e-a5c6c3d5c553.png)


##### プロフィール編集画面
プロフィール情報の左下「編集」押すと編集画面になる。
![スクリーンショット 2022-04-05 100148.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/ecb85da9-e9f4-5ea2-f4db-980b098f1c38.png)
下までスクロールすると保存ボタンあり
![スクリーンショット 2022-04-05 100212.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/33029243-be9f-0a77-74f1-465dba7413d0.png)



まとめ
---
そういえば心残りあるな～という点を挙げると
* Chart.jsの新しいバージョンで書きたかった（今回使ったバージョンよりも機能が充実しているので）
* jQueryとJavascriptを整理する（次回があるならメジャーなフレームワークを使ってみたいしgetElementId祭りを回避できるのでは）
* 1匹や3匹以上でも対応できるレイアウト・機能の作りにする（今回は管理できるペットデータの数を２匹分に固定して作成していた）

なので機能を作ったものの途中で
この処理もう少し短く書けるのではと思う所も多々あり、
終盤のあたりから一から作り直したい気持ちもあったけどアプリの作り方の勉強になったし HTML，CSS，Javascript が身近なツールになった気がします。