この記事では 24 新卒、25 卒内定者が開発に参加した『 日比谷音楽祭公式おさんぽアプリ 2024 』 の開発の裏側をクライアント編、サーバ編の 2 回に分けてご紹介します。 今回はクライアント編をお届けします。
クライアント編は、25 卒内定者の上野、川口、藤堂が本稿の執筆を担当します。
この記事の概要
- クライアントチームの技術的挑戦
- アプリをリリースするまでの道のり
日比谷音楽祭 2024 とおさんぽアプリ
『日比谷音楽祭おさんぽアプリ 2024』(以下、おさんぽアプリ) は、日比谷公園で 2024 年 6 月 8 日と 9 日の 2 日間で開催された「 日比谷音楽祭 」のために開発されたアプリです。
日比谷音楽祭では 「音楽の新しい循環をみんなでつくる、フリーでボーダーレスな音楽祭」 をコンセプトとしており、親子孫3世代、誰もが気持ちのよい空間と、トップアーティストのライブやさまざまな質の高い音楽体験を、 無料で楽しむことができます。
今年のおさんぽアプリでは以下の機能が提供されました。
- GPSを使用して公園内のマップ上に現在地を表示する機能
- ARで会場内の装飾を表示する機能
- トレジャーハント機能
クライアントチームについて
クライアントチームは、25卒インターン生3名と社員2名、および昨年開発に参加した24卒の先輩方3名の計8名で構成されています。当おさんぽアプリの開発において、クライアントチームはモバイルアプリの開発とARの実装を担当しました。
今年度の挑戦として、「トレジャーハント」というARを使用して宝箱を集める機能を開発し、アプリに組み込みました。そして、このAR領域は社員が担当しました。
インターン生は、デザイン・PRDをもとにアプリの開発に取り組みました。デザインは社内のデザイナーに Figma で用意してもらいました。エンジニアである私たちは、デザインをもとにFlutterで画面の実装を行ったり、サーバーチームが作ってくれたAPIを繋ぎ込んでデータをアプリに反映させたりすることを担当しました。
サーバーチームと協力して、アプリの申請までの約4ヶ月間にわたって作業しました。24卒メンバーは基本的に3月までのサポートとして配属されていましたが、分からないことがあればいつでもサポートしてくれました。
技術的挑戦
ここからはクライアントチームの技術的挑戦についてご紹介します。
アーキテクチャの改善
今回は、機能実装を進める前に、まずアーキテクチャを改善することから始めました。日比谷音楽祭のプロジェクトは、翌年にコードを引き継いで開発しているのですが、主要な機能が毎回大きく異なります。例えば、昨年度はチケット機能が含まれていましたが、今年度は含まれていません。そのため、機能実装を進める前に、不要な機能を全て削除する必要があります。その際、依存関係を考慮したアーキテクチャになっていないと、コードを削除するだけでも大変になってしまいます。
具体的な例を見てみましょう。以下のディレクトリ構成は、昨年度のものです。このような構成では、チケット機能に関連したファイルを見つけて削除するのが困難です。また、不要なコードを削除し忘れる可能性もあります。
├── widgets
│ ├── a.dart
│ ├── b.dart
│ ...
│ ├── ticket_1.dart
│ ├── ticket_2.dart
│ ...
│ ├── x.dart
│ ├── y.dart
│ ...
├── pages
│ ├── a_page.dart
│ ...
│ ├── ticket_page_1.dart
│ ├── ticket_page_2.dart
│ ...
│ ├── x_page.dart
│ ...
そこで、ディレクトリ構造を以下のように変更しました。 feature
ディレクトリの中に、機能ごとのディレクトリを作って、関連したファイルをまとめています。これにより、ファイルが機能ごとにまとまり、不要な機能を削除する際にディレクトリごと削除することで対応が可能になります。
├── feature
│ ├── home
│ │ ├── components
│ │ │ ├── home_button.dart
│ │ │ └── ...
│ │ ├── home_page.dart
│ │ └── ...
│ │
│ ├── map
│ ├── notification
│ ...
│ ├── ticket
│ ...
また、ファイルどうしの依存関係にも気を付ける必要があります。例えば、 map
に作成したコンポーネントを home
でimportしてしまうと、仮にマップ機能を削除するために map
ディレクトリを削除したとすると、 home
に必要なコンポーネントが削除されてしまいます。
これを防ぐために、 feature
を横断して利用されるような共通コンポーネントを、 shared
にまとめておくようにしました。これにより、 feature
間の依存関係をなくすことができます。
├── feature
│ ├── home
│ ├── map
│ ...
├── shared
│ ├── apis
│ ├── design_system
│ ...
このような feature
間の依存関係をなくすアーキテクチャは、機能実装の際にも役立ちます。なぜなら、複数人で機能開発をする場合、担当する feature
を分けることにより、コンフリクトが発生する可能性を大きく下げることができるからです。
また、状態管理パッケージとして、Flutter Hooksを導入しました。もともと状態管理にはRiverpodが使われていたので、RiverpodとFlutter Hooksを使い分ける運用にしました。具体的には、RiverpodがApp State(アプリ全体の状態)の管理をするのに対し、Flutter HooksはEphermal State(一時的な状態)の管理に利用しています。
デザインシステムの移行
日比谷音楽祭のアプリは、毎年デザインシステムが大きく変わります。コンポーネントの移行は、主に定義されたコンポーネントのデザインを変更するだけで済みましたが、配色を変更するのには非常に手間がかかりました。なぜなら、配色は、トークンの名前ごと大きく変更されたからです。
色のトークン名は、Figmaのものに従って忠実に移行しました。その際、ただ単に配色のデータを更新するだけでなく、デザイントークンを階層化する変更を行いました。
今までは、以下のように色をグローバルに定義していました。そのため、数多のサジェストの中から、使いたい色を探し出す必要がありました。
const Color textPrimary = black700;
const Color textSecondary = black500;
const Color textAccent = pink500;
const Color textLink = blue400;
const Color textNegative = red500;
const Color textNotEntered = warmGray400;
...
実はFigmaの配色定義は、グループごとにまとめて階層構造を作ることができます。そのため、この階層構造をコードの実装にも適用することで、開発者にとってトークン名の対応がわかりやすくなるようにしました。
実際にFigmaのデザインを実装したコードは以下のようになります。
class HibiyaColor {
static const contents = _Contents._();
static const text = _Text._();
...
}
class _Contents {
const _Contents._();
Color get accent => const Color(0xFF91C3DC);
...
}
これにより、探したい色のカテゴリーのみがサジェストされるため、コードの書き心地も圧倒的に良くなりました。
また、昨年度の使用しない色などは、 @Deprecated
アノテーションをつけて、実装と並行して段階的に削除していきました。
OpenAPI の導入
今年度はAPIの実装にOpenAPIを導入しました。自動生成にはOpenAPI Generatorのdart-dioを使用しました。OpenAPIとはRESTful APIを記述するためのフォーマットです。YAMLまたはJSONファイルにAPIのエンドポイントやパラメーター、レスポンス、セキュリティなどの定義を記述します。この定義ファイルを元にOpenAPI Generatorがアプリ側のAPI関連のソースコードやAPIのドキュメントファイルを自動生成してくれます。
ただ、メンバー各々で自動生成のコマンドを実行するのも少々煩わしいです。そこで、OpenAPIの設定ファイルを管理しているリポジトリに更新が入るとアプリ側のコードを自動生成するCIを組むことでさらなるソースコードの自動化を実現しておりました。
ここからはOpenAPIを導入した所感について紹介します。
OpenAPIを導入したことで以下の恩恵が受けられたと感じています。
- 開発をスムーズに進められた
- API周りの実装に時間が取られなかった。
APIの定義は開発初期に行いました。アプリ側とサーバー側でどのようなAPIがあるのか洗い出し、それをYAMLファイルに定義しました。そのおかげで各APIで送信すべき情報と返ってくる情報があらかじめ決まった状態となるため、モックサーバーを立てることで、バックエンドの実装を待たずにAPI周りの実装を行うことができました。このようにバックエンドを待たずに実装が進められたことはOpenAPIを導入して得られた恩恵の1つでした。
また、OpenAPIの導入によってAPI周りの実装やメンテナンスに時間が取られなかったことも恩恵の1つでした。最初にも触れましたが、YAMLファイルを定義してしまえば、OpenAPI generatorによってAPI周りのソースコードやドキュメントを自動生成してくれます。そのためアプリ側で直接APIを叩くようなコードは記述することがなく、APIが叩かれている関数を呼び出すだけでした。また、開発を進めて行く途中でAPIを変更することが何度かありました。本来であれば、ドキュメントやソースコードの修正が必要となる場面ですが、YAMLファイルを書き換えて自動生成するだけで、ドキュメントもソースコードも修正する必要がありませんでした。
一方で、開発期間が短かったためOpenAPIを使いこなすには至りませんでした。OpenAPIで自動生成されるモデルクラスは次のような特徴があります。
- ビルダーパターンで実装されている
- イミュータブル
OpenAPIのモデルクラスはビルダーパターンで実装されています。モデルクラスのインスタンスを生成するにはビルダーを介して生成する必要があるのですが、Flutter初学者でかつ、ビルダーパターンというものを知らなかったためこのインスタンスを生成することに苦戦しました。
また、OpenAPIのモデルクラスはイミュータブルで、値を変更しようとすると新しいインスタンスを生成する必要があります。新しいインスタンスを生成するためにモデルクラスにはrebuildという関数が用意されていますが、開発中はそれに気づかずに、値を更新したいモデルクラスはわざわざFreezedのクラスに変換してアプリ上で使用しておりました。
OpenAPIは学習コストが少々高く、なかなか使いこなすには至りませんでしたが、アプリ側のコードの自動生成やサーバー側とのコミュニケーションコストの低下という恩恵を得ることができました。もし来年度もあれば、今年度の知見もあることからOpenAPIを採用しても良いかと思いました。
ビルドの自動化
昨年度に引き続き、今年度もCI/CDツールのBitriseを用いてビルドからデプロイまで自動化しました。今年度はプルリクエストがMainブランチにマージされるとqa/staging/productionの各環境でiOSとAndroidのビルドが走り、バイナリファイルがDeployGateへアップロードされるようにしていました。
この自動化によって今年度はビルドやデプロイを手作業で行うことはほとんどありませんでした。しかし、Bitriseの費用が大変な事になっていたようです(噂では、おさんぽアプリを開発した5ヶ月ほどの間に、同じくFlutterで開発している別プロジェクトの1年分ほどコストがかかっていたとか)。ですので、Bitriseが実行されるタイミングはもっと改善の余地がありました。
またBitriseでの自動化はARを含んだビルドについては対応していません。後述しますが、AR機能はリリース直前まで別ブランチで開発していたことと、ARを含んだ自動化に時間がかかりそうだったことにより、ビルドとデプロイの完全自動化は後回しにしました。リリース直前でARを含んだバージョンの動作確認を行う際は手作業でビルドを行い、デプロイ作業も手作業で行いました。日比谷音楽祭が終わってから少々時間があったため、来年度に向けて自動化に取り組んでいます。
苦戦したデザインとアニメーション
アプリの実装にあたり、デザイナーが用意したデザインを端末に反映させる必要があります。今年度のアプリで一番時間を要したデザインは、「トレジャーハント達成状況画面」(トレジャーハントで獲得したスタンプが表示され、達成度を確認することができる画面)です。特に難しかったUIについて、難しかった点や実装方法について紹介します。
1. スタンプの表示
デザインと実装画面
トレジャーハントの達成状況は、以下のようなスタンプで確認できるようになっています。動画のように、中央部分を横方向にスクロールすることで全てのスタンプが確認できます。
前述のデザインの実装が難しかった点は主に下記の2点です。
- スタンプ・周囲のアイコンの配置と色の切り替え
- イラストのレスポンシブ対応
難しかった点1: スタンプ・周囲のアイコンの配置と色の切り替え
スタンプや周囲のアイコンは、APIで取得したゲームの達成状況によって色の有無を切り替えています。この実装で問題となったのが、スタンプの配置順序は不確定であるのに対し、アイコンの配置は固定でかつ近くのスタンプの達成状況に応じて色が変化するという点です。
前提として、背景の小さなアイコンは2個×6セットの計12個で、1セットが近くの1スタンプに対応づく形で色の有無が切り替わる仕様となっています。
このデザインを順当に Stack で実装しようとすると、全てのスタンプ・アイコンそれぞれに座標を紐付けたり、達成状況に応じてスタンプ・アイコンそれぞれに合わせた判別を行ったりする必要がありました。
そこで、アイコンは余白も含めたキャンパス全体で保存したイラストを用意し、座標指定やサイズ変更を行うことなく表示できるように工夫しました。2個で1セットとなっているのを活かし、2個まとめて1つのsvgとして保存することで、用意するsvgの数を減らし、処理もまとめて行えるようにしました。
また、未達成時はグレー・そうでない時は個別に指定された色というデザインだったので、未達成時にはsvgに colorFilter を当ててグレーにすることで、未達成時のイラストを別途用意せず切り替えられるようにしました。
難しかった点2: イラストのレスポンシブ対応
端末のレスポンシブ対応のため、達成状況のスタンプのイラスト部分は下記に示す画像のように、大きさを端末に合わせて表示する必要がありました。
しかしこの対応が難しかったため、小さめの端末に合わせてサイズを設定して余白だけ増やすという妥協案を提示したのですが、どうしても余白が目立ちすぎてしまったため、時間をかけて対応することにしました。
そもそもスタンプの実装は、「APIから取得した達成状況データの配列」と「アプリで定義しているスタンプの画像のパスを一覧にしている配列」を name
プロパティで紐付け、APIの配列の順序に基づいて紐づけられたスタンプのイラストを左から配置しています。
最初は周囲のアイコンと同様に余白込みのイラストとして保存しようとしていたのですが、それだと画像がイラスト全体を要素としてしまい、「未達成時にスタンプをタップするとスタンプ上部にヒントが表示される」という仕様を実装するのにタップ領域を新たに設ける必要が生まれてしまいます。イラスト全体のどこをタップしてもヒントを表示するという妥協案も出たのですが、そうなるとスタンプ上部のヒント表示もうまく行えません。また、余白込みでの保存には、位置を特定させて保存させる必要があるため、APIのデータ取得順での表示が行えません。
よって、スタンプはそれぞれのスタンプの座標を配列として定義しておき、それをもとに一つずつ座標指定をしながら配置することにしました。スタンプの画像サイズは固定で実装しています。
当初は Stack ・ Positioned を用いて実装していたため、座標を絶対値で保持して、イラストに使える縦幅を取得し割合をかけることで指定座標を動的に変えていたのですが、 Align だと相対的に -1 ~ 1 の割合で位置を指定できると教えてもらい、やっとスマートな実装ができました。「いかに、より適切なWidgetを見つけ出し(思い出し)て使うか」が「いかに、Flutterを使いこなせるか」なのかなと最近思っています。
2. ヒントの表示
デザインと実装画面
未達成の宝箱は、スタンプをタップすると下記のようにヒントが表示されるようになっています。
難しかった点: ヒントの配置
ヒントはスタンプの上に配置するようになっていましたが、スタンプ表示のループで同時に表示を行うと、位置調整が楽な代わりに他のスタンプでヒントが隠れてしましました。
デザインの都合上、右側のスタンプから順に表示するとヒントが重なることなく表示できました。よって、今回はforループを後ろから回すという簡略的な対処で対応しました。
※ OverlayPortal などを用いて他の要素より手前に表示する方法もあったかと思いますが、工数的には逆ループで対応した方が早くバグも起きないと判断したため、この実装方法に落ち着きました。
工夫: 表示・非表示を切り替えるアニメーション
最後に、こだわった点としてアニメーションを紹介させてください。Figma上では、下記のような表示・非表示時点でのデザインのみが共有されていました。
ですが、ただ表示・非表示を切り替えるとのっぺりしてしまいました。そこで、切り替えをフェードイン・フェードアウトで実装してみました。before・afterはこちらです。
デザイン上ではアニメーションがついていないことも多いかと思いますが、上記のようなほとんど変わらないような工夫がUXの向上につながると考えています。そして、このような工夫をデザイナーに提案できる環境で仕事をできたことが、とても素敵であり、楽しかったです。
AR 機能の実装と連携
例年のおさんぽアプリでは、会場内をめぐりながら楽しむことができるウォークラリー機能が提供されてきました。今年度はそれがARとなりました。会場内をバーチャルで装飾し、アプリ内のカメラを通してみると装飾された会場をみることができます。
また、装飾以外にも宝箱を会場内にバーチャルで配置し、ARカメラを通して宝箱を探すトレジャーハント機能が今年度は実装されました。
会場内には6つの宝箱が配置されており、全て集めると景品と交換することができました。インターン生も当日カメラをかざしながら宝箱を探しておりました。
このAR機能はFlutterアプリにUnityを埋め込む形で開発されました。ただ、あまりにも不確定要素が多く難易度が高いためインターン生ではなくDeNAのエンジニア2名によって開発されました。(ブログを執筆しているのはインターン生ですが…)
ここからはどのような技術を使ってAR機能が開発されたのかについて紹介いたします。
使用技術
ARCore・ARKit
ともに拡張現実アプリを作成するためのプラットフォームです。ARCoreはAndroidで動作し、ARKitはiOSで動作します。ともにデバイスのトラッキングや水平・垂直面の検出、光源の推定などの機能を提供してくれます。
Geospatial API・Geospatial Creator
Geospatial APIはGoogleが提供するAPIでスマートフォンの位置情報だけでなくGoogleストリートビューのデータとスマートフォンのカメラの画像を用いて、精密な位置情報を取得しています。また、事前に周りの景色をスキャンする必要がなく、屋外であればどこでも使うことができるようです。
Geospatial CreatorはUnityエディター上に実世界の地形や建物を表示してくれます。Google EarthがUnityに表示されるような感じです。
このエディター上に表示されている地形にオブジェクトを配置すると実世界でも同じ場所にバーチャルでオブジェクトが配置されます。この機能を用いて日比谷公園内の装飾や宝箱の配置を行いました。
Unity as a Library
Unity as a LibraryはUnityプロジェクトをLibraryとして扱えるようにすることができるプラグインです。これによってUnityをFlutterに埋め込むことができます。
このUnityプロジェクトをFlutterに埋め込む作業がかなり難航しました。この原因としてUaaLが依存しているライブラリのバージョンとFlutterが依存しているライブラリのバージョンで競合が発生したことがあげられます。そのため、ARはリリース直前までMainブランチに混ぜずに別ブランチでの開発を行いました。そのようにすることでAR以外のアプリの機能開発を阻害することなく進めることができました。
リリースまでの道のり
プロジェクト進行
私たちは、冬の入社から日比谷音楽祭が終わる6月までの半年弱にわたって行いました。在宅勤務のため基本的にオンラインでの作業が中心でしたが、Slackのハドルでたくさん会話したり、他職種とたくさん意見を交わしたりと、積極的にコミュニケーションをとった開発期間でした。その中でも、特によかったと感じた活動について、いくつか共有したいと思います。
Slackを活用したチーム内コミュニケーション
チーム内でのコミュニケーションがSlackを中心に活発に行われていました。毎日13:00から30分間の朝会を通して進捗等の共有・相談を行うことはもちろん、ハドルを活用してペアプロを行ったり、詰まった部分を質問し合ったり、作業通話(もくもく)を行ったりしていました。知識や思考の共有を行うことで理解が深まり、よりよい実装に繋げることができました。
ペアプロでは、教えてくれる人のコードの書き進め方や書いている際に何を考えているかなどを知ることができ、Flutter初心者であった私にとって非常に助けになりました。教えてもらって以降、開発が格段にスムーズに進むようになりました。また、もくもく中に質問をすることも多く、質問者・回答者でなくてもその会話を聞くことができました。このような時間をたくさん共有することで、チームメンバーそれぞれの個性や思考を知ることができ、結果的に個人・プロダクトの成長につながったとても有意義な時間だったと感じています。
チーム内共有ドキュメント
日比谷音楽祭のチームは歴代、チーム内共有ドキュメントを充実させていました。今年度も、オンボーディング資料やビルド・デプロイの方法、アーキテクチャなどについてドキュメントを丁寧に残しており、ある程度の作業方法はドキュメントを確認すれば確認できるという、とても開発効率の良い環境を整えることができていました。
他職種とのコミュニケーション
今年度特に印象的だったのは、デザイナーやPdMなどの他職種との積極的なコミュニケーションです。クライアントメンバーそれぞれが疑問に思ったことを各職種の担当者にそれぞれで聞いているというのが日常でした。特にデザイナー方とのやり取りは、Figma上にコメントで溢れかえるほどでした。
よりよいものを作るために各メンバーが自ら動き、それをしっかりとチーム内で共有し、実装に落とし込む。全力で走り切っている間はとても楽しく、達成感しかありませんでした。
デザインFIX後の修正
アプリの開発は比較的短期間で行われたため、デザインの制作とアプリへの実装を同時進行で進めました。すべての画面の開発を同時に進めていると、要件が複雑な画面の完成が間に合わなくなる可能性があります。そのため、機能が多く、開発工数のかかる画面を優先的に開発しました。デザインが確定した画面から、アプリへの反映を進めました。
その際、コンポーネントを切り出すことの重要性を感じました。コンポーネントが別ファイルに分割して共通化されていなければ、デザインの適用漏れや、コンフリクトの問題が生じやすくなります。そのため、早めにアーキテクチャを意識してコンポーネント化などを進めておいたことは、開発終盤の速度維持に役立ったと感じました。
Store 申請でのリジェクト
無事ストアに申請を出すことができましたが、すんなり通過というわけにはいきませんでした。特に、iOSアプリの審査がなかなか通過せず、複数箇所を指摘されてリジェクトされました。
実際にiOSのリジェクトで指摘された箇所の一つが、権限の許可についてです。アプリがユーザーに位置情報許可を求める際には、ダイアログが表示されます。ここに表示される「位置情報を許可してください。」のような文言は、開発者が自由に決めることができます。ただし、文言はAppleのガイドラインに準拠する必要があります。今回は、「位置情報の利用目的が明確に書かれていない」という理由でリジェクトされました。
リジェクトされるたびに社員の方々にも迅速にご対応いただき、最終的にはAndroid・iOSともに審査を通過してリリースすることができました!
当日の様子
当日は、25卒のインターン生とCG担当の社員、PdMの計6名で会場内のテントでアプリが上手くいくよう見守りました。
当日の仕事として、アプリの通知の発行を行なっていました。運営から届く入場規制などの速報を、文言を調整しつつ、管理ツールからアプリへ通知を飛ばしていました。また、実際に自分たちでトレジャーハントを操作してみたり、ユーザーの操作ログを見て、どの操作が多く行われているか・トレジャーハントはどれくらい達成されているか・バグなどは起きていないかなどを観察したりしていました。 また、仕事が特にない間もやり残した修正をしたり、リファクタリングをしたりと、とても楽しかったです。
バグとネットワークの不調
今年度、特に大きなバグのようなものは生まれなかったのですが、ネットワークの不調がとても目立ちました。宝箱の処理が上手くいかないユーザもいたようで、知り合いの端末でも宝箱の取得ができずアプリを再起動してようやく動いたりする状態でした。ネットワークエラーに対して、一定時間が経てば再度リクエストを送るなど、もう少しエラーハンドリングを丁寧にしておけばよかったなと反省しています。
また、トレジャーハント達成時に景品と交換できる仕様なのですが、景品交換画面へ遷移するボタンが交換後も表示されてしまうというバグが起きてしまっていました。原因は、景品交換完了時に達成状況の更新をアプリ内で適切に反映出来ていなかったためのようで、アプリを再起動するとボタンは非活性(あるべき状態)に戻っていました。交換画面に飛ぶだけで、交換ボタンを押しても「すでに景品交換済です。」と表示するエラーハンドリングを行なっていたため重大なバグには繋がりませんでしたが、エラーハンドリングの大切さを痛感したバグになりました。
トレジャーハントをする人々
会場を回っていると、たまに携帯であたりを見回して宝箱を探している人々を見かけました。数ヶ月かけて作ってきたアプリを使って楽しんでくれている人がいることを間近で見て、とても達成感を感じました。難しすぎるという声も内輪であがっていましたが、その難しさも含めて、とてもおもしろいアプリを作れたのではないのかなと思います。
おわりに
エンジニア、デザイナー、マネージャーを問わず、社員の方々には様々なサポートをしていただきました。短い期間の中で、複数プロジェクトを掛け持ちで対応されている方もいらっしゃいました。そのような状況でも、私たちインターンメンバーと密にコミュニケーションを取ってくださり、非常に感謝しています。
また、来年度の入社を前にして、24卒の先輩エンジニアの方々や25卒の同期と交流を深められたことは、非常に貴重な経験になりました。
このインターンで得た刺激や経験をもとに、入社までの期間でさらに成長したいというモチベーションが高まりました!短い期間でしたが、本当にありがとうございました!
サーバー編のブログ記事も公開されているので、ぜひご覧ください!
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。