Slide 1

Slide 1 text

Vueと共に走った フロントエンドリプレイス 1年間 2019/01/30 【Nuxt.js/Vue.js】スタートアップ企業導入事例 小宮山 智也

Slide 2

Slide 2 text

目次 ● 自己紹介 ● 概要 ● 技術構成 ● コンポーネントとルーティング設計 ● データフロー設計 ● テスト ● 1年かけた学び ● おまけ

Slide 3

Slide 3 text

自己紹介 ● 小宮山智也(Komiyama Tomoya)@株式会社スタディスト ● 最近の趣味 ○ ランニング ○ ウォーキング ○ ボルダリング ● 近況報告 ○ VSCodeからNeovimに乗り換えた 2017年10月 2018年11月

Slide 4

Slide 4 text

概要

Slide 5

Slide 5 text

伝えたいこと概要 ● Vue関連技術をフルに活用した設計と実践の実例 ○ 破綻させずにリリースまで辿り着いた成果の共有 ○ Atomic Design ○ ドメイン駆動Vuex ○ コンポーネントが守るべきこと ● これは良かった悪かったの振り返り ● Storybookの素晴らしさ

Slide 6

Slide 6 text

今回は伝えないこと概要 ● 技術選定のこと ● 開発プロセス的なこと ● UIUXの設計や検討的なこと ● バックエンドのこと

Slide 7

Slide 7 text

プロジェクト概要 ● Teachme Biz(https://biz.teachme.jp/ )というプロダクトのWeb版UI全面リニュー アル ● ローンチから積み上げ続けてパンク気味だったフロントエンドを一気にモダン化する ● 継ぎ接ぎになってしまったUIUXを根本から見直す ● Before: RailsのERBを使った古き良きRails主体のフロントエンド ● After: Railsと切り離したフロントエンド主体のフロントエンド

Slide 8

Slide 8 text

新旧比較 https://biz.teachme.jp/function/ui-renewal-2018/

Slide 9

Slide 9 text

技術構成

Slide 10

Slide 10 text

概要 ● 開発環境 ○ Docker、Webpack(3系)、Babel、Eslint、Sass、PostCSS ● テスト ○ Karma、Mocka、power-assert、Storybook ● フレームワーク ○ Vue、Vuex、vue-router ● 使っていない/やっていないもの ○ Nuxt、TypeScript、Webpacker、E2E

Slide 11

Slide 11 text

利用パッケージ① "dependencies": { "@storybook/addons": "", "axios": "", "chart.js": "", "encoding-japanese": "", "es6-promise": "", "jquery": "", "jquery-ui": "", "marked": "", "pdfjs-dist": "", "raphael": "", "ress": "", "storybook-router": "", "vue": "", "vue-analytics": "", "vue-chartjs": "", "vue-i18n": "", "vue-loader": "", "vue-router": "", "vue-slider-component": "", "vue-template-compiler": "", "vue-touch": "", "vuedraggable": "", "vuejs-datepicker": "", "vuex": "", "webpack-merge": "", "zxcvbn": "" }, ● deb or 非debの区分けは適当 ○ ライブラリとして配布するわけではな いので気にしていない ○ webアプリとしていい感じの運用方法 があるなら知りたい ● jqueryはリプレイスしきれなかった 過去実装の残骸

Slide 12

Slide 12 text

"eslint-plugin-html": "", "eslint-plugin-import": "", "eslint-plugin-node": "", "eslint-plugin-promise": "", "eslint-plugin-standard": "", "eslint-plugin-vue": "", "extract-text-webpack-plugin": "", "file-loader": "", "jsdom": "", "json-loader": "", "karma": "", "karma-chrome-launcher": "", "karma-firefox-launcher": "", "karma-ie-launcher": "", "karma-jsdom-launcher": "", "karma-mocha": "", "karma-mocha-reporter": "", "karma-opera-launcher": "", "karma-safari-launcher": "", "karma-webpack": "", "devDependencies": { "@storybook/addon-actions": "", "@storybook/addon-knobs": "", "@storybook/addon-links": "", "@storybook/vue": "", "aglio": "", "axios-mock-adapter": "", "babel-core": "", "babel-eslint": "", "babel-loader": "", "babel-plugin-espower": "", "babel-plugin-transform-runtime": "", "babel-preset-env": "", "css-loader": "", "drakov": "", "eslint": "", "eslint-config-standard": "", "eslint-config-vue": "", "eslint-friendly-formatter": "", "eslint-loader": "", 利用パッケージ② "mocha": "", "node-sass": "", "postcss-cssnext": "", "postcss-smart-import": "", "power-assert": "", "sass-loader": "", "style-loader": "", "url-loader": "", "vue-style-loader": "", "webpack": "", "webpack-dev-server": "", "webpack-manifest-plugin": "" }

Slide 13

Slide 13 text

コンポーネントとルーティング設計

Slide 14

Slide 14 text

Atomic Design - src - components - atoms - molecules - organisms - pages - routerLayouts ● ディレクトリ構成は右図 ● routerLayoutsは独自のもの(後述) ● templatesは設けていない ○ SPAだとページ内容は動的に流し込むので、 pagesと実質 同じという記事をどこかで読んだ(見失った) ● コンポーネントの粒度とディレクトリの表示順が一 致するのが地味に有効

Slide 15

Slide 15 text

atoms ● コンポーネントの最小単位 ○ ボタン、テキストインプット、ラジオボタン ● Vuexへのアクセス不可

Slide 16

Slide 16 text

molecules ● atomsよりも大きめの単位 ○ ドロップダウン付きのボタン、パンくずナビゲーション、スナックバー ● organismsほど大きくないけれどatomsというほど小さくな いことが判断基準になることが多い ● Vuexへのアクセス不可

Slide 17

Slide 17 text

organisms ● moleculesよりも大きく、画面レイアウトのパーツとなる ような単位 ○ ダイアログ、ヘッダー、テーブル ● Vuexへのアクセス解禁

Slide 18

Slide 18 text

pages ● 1画面となるような単位 ○ ログイン画面、ユーザープロフィール画面 ● Vuexへのアクセス可 ● その画面のmain処理 ○ データfetchの起点

Slide 19

Slide 19 text

routerLayouts ● 画面レイアウトの枠組み ● 中身はvue-routerから挿入する ○ をtemplateに設置する ● Atomic Designの要素ではない ○ 画面の共通レイアウト置き場が欲しかった

Slide 20

Slide 20 text

レイアウト構成 ● App.vueが全ての画面ルート ○ 共通部分だけを設置 ○ 個別画面の処理には干渉しない ● 画面に応じたレイアウトをrouterが App.vueに挿入 ● 画面に応じたパーツをrouterがレイア ウトに挿入

Slide 21

Slide 21 text

App.vue レイアウト構成 organisms routerLayouts routerが組み立てる pages

Slide 22

Slide 22 text

良かったこと ● 必ずApp.vueがルートコンポーネントになる ○ 全画面で共通する処理を組み込みやすい ■ フラッシュメッセージ、アラートダイアログ、ローディング ● レイアウトを統一できる ○ レイアウトに関わるCSSが散在しない ○ レイアウト種類は想像よりも少ない ● ファイル数が減る(?) ○ 共通処理化、レイアウト統一の恩恵 ○ (調べたら.vueファイルが102あった、画面数は77、全然減ってない)

Slide 23

Slide 23 text

悪かったこと ● ルーティング設定が肥大化する ○ 「設定ファイル」を逸脱しないよう頑張っているが、やはり行数が多いのは辛い ● ルーティング設定の書き方が、コンポーネントの階層構造に引きずられる ● 画面の基点となるコンポーネントを意識していなかった ● レイアウト用コンポーネントが利用前提になってしまった

Slide 24

Slide 24 text

ルーティング設定の肥大化 ● 77画面で1473行のルーティング設定 ● 右図のように、ルーティングのネストはURL ではなく、コンポーネントの階層で区切らな ければならない ○ レイアウト→パーツという階層なので、必ず 2回層 書く必要がある ○ URLの階層と比べて、コンポーネントの階層はあ まり直感的ではない

Slide 25

Slide 25 text

画面の基点 ● contentが実質的に基点となっている ○ 初期データfetchもここで行っている ○ 元々の設計意図ではなく、なし崩し的にそうなった ○ ルーティングに依存させたくない意識があった ● 各パーツを跨ぐ処理が難しくなる ○ headerなどはrouterにより分離されているので、 contentから 制御できない ○ pluginやstoreなどグローバルな存在に頼らざるを得ない ○ 例) ■ パワポのようなサイドメニューでパネルを選んだら、メイ ン部分をそこまでスクロールさせる App.vue

Slide 26

Slide 26 text

● contentを画面の基点と認める ○ pagesは画面の基点置き場とする ○ ディレクトリ構造をルーティングに合わせる ○ 実質そうなりつつあるが、設計の前提にしていたらもっと良く なったと悔やまれる ● 共通レイアウトを見極める ○ パーツまではめ込んだものをレイアウトにする ○ content部分だけをslot(router-view)ではめ込む ○ contentと連動していると感じたら共通レイアウト化はさっさ と諦める ○ 複雑でダイナミックな画面も共通レイアウト化はさっさと諦め る こうすればもっと良くなったかも(未検証) slot - pages - folders - FolderList.vue - FolderProfile.vue - users - UserList.vue - UserProfile.vue

Slide 27

Slide 27 text

App.vue レイアウト構成改善案(未検証) routerLayouts slot slot slot pages 各種パーツはorganismsに設置 枠組み自体もコンポーネントにす るのが良さそう 画面の基点はここ その他は干渉できない共通パーツ

Slide 28

Slide 28 text

これってもしかして...

Slide 29

Slide 29 text

Nuxt式ルーティング ● 画面ごとに基点コンポーネント ● ディレクトリ構造によってURLを表現 ○ 非Nuxtではルーティング自動生成はできないが、設計の参 考にしない手はない ● レイアウト構成はまさしく改善案 ルーティング設計はNuxtから学ぶべし (たとえNuxtを導入しないとしても)

Slide 30

Slide 30 text

レイアウトのロックイン ● 共通レイアウトで作ったが、共通にできない独自な画面であることが後から判明す ることもある ○ 例)contentとsidebarを跨ぐ制御が必要になったことが実際にあった ○ 共通の親がいればpropsと$emitで制御できるのに、その親がいない ■ で分断されてしまっている ● 共通レイアウトから分離するコストが高い ○ 独自制御はあるが、見た目のレイアウトは共通と同じ ○ がにさえなっていれば使い回せる(はず) ■ → レイアウトを常にで作り、それをでラップする存在がいたら解決した のではないか?

Slide 31

Slide 31 text

レイアウトコンポーネント改善案(未検証) レイアウトは常にslotで作成 router経由でしか使えない レイアウトになってしまう 共通パーツやrouter-viewをslotに 埋め込むだけのコンポーネントを用 意する

Slide 32

Slide 32 text

ディレクトリ構造改善案(未検証) ● 利用の純粋なスタイル付きレイアウトを organismsに置く ● pagesには各画面のメインとなるコンポーネントを ルーティングのネストに沿って置く ● レイアウトを利用し、や共通パーツを 配置した存在をpageLayoutsに置く ○ は不要 ○ <script>は最低限 - organisms - layouts - HSCLayout.vue - HCLayout.vue - pages - folders - FolderList.vue - FolderProfile.vue - users - UserList.vue - UserProfile.vue - routingLayouts(pageLayouts) - MainLayout.vue - settings - FolderLayout.vue - UserLayout.vue

Slide 33

Slide 33 text

データフロー設計

Slide 34

Slide 34 text

ドメイン駆動Vuex ● Vuexをドメインの処理に集中させる ○ https://medium.com/studist-dev/ddd-vuex-c47055f6c1ba ○ ドメインの複雑さはVuexに閉じ込める ● コンポーネントは理想的なモデルをVuexから受け取り、豊かなユーザー体験の表 現という本来の仕事に集中する

Slide 35

Slide 35 text

Vuexフロー要約 ● コンポーネントからaction実行 ● actionはajaxでAPIにリクエスト ● レスポンスをmutationに受け渡す ● stateにAPIデータを保管 ● getterでモデルに整形してコンポーネン トへ受け渡す

Slide 36

Slide 36 text

非ドメインなデータ ● 全てのデータがドメインとは限らない ○ APIからもらえるものはほぼドメインとみなして問題ない ● 画面都合で一時的に保持したい状態がある ○ 通信中のローディング表示をおこなうための状態 ○ エラーメッセージを表示するための状態 ● Vuexはドメインデータに完全特化させるため、非ドメインなデータはstoreに保管し ない ○ 単一storeというFluxの理想を追いすぎない

Slide 37

Slide 37 text

非ドメインなデータ置き場 ● 各コンポーネントのdata ○ コンポーネントに閉じるようなデータならばここに置くのが原則 ○ 一覧データのページネーション、ドロップダウン開閉状態、保存前のフォーム入力など ● Vueのplugin機能 ○ コンポーネントを跨ぐようなデータはここに置く ○ モーダルなローディング状態、アラートメッセージなど ● コンポーネントのレイアウト構成に失敗すると、その画面でしか使わないのに pluginに状態を置かざるを得ない状況が生まれてしまう ○ レイアウト構成を見直すべし

Slide 38

Slide 38 text

例)アラートダイアログ① ● アラートダイアログを表示するため の状態をpluginに置く ● pluginとして読み込んでいるので、 コンポーネントから下記のように呼 び出せる

Slide 39

Slide 39 text

例)アラートダイアログ② ● コンポーネントの実体はApp.vueに設置しておく ○ App.vueは常にルートなので、どのコンポーネントからでもアラートを呼び出せる App.vueのcomputed App.vueのtemplate

Slide 40

Slide 40 text

アラートダイアログ余談 ● アラートダイアログのコンポーネントをApp.vueではなく、呼び出す側のコンポーネ ントに設置すればpluginではなくdataに状態を持たせるだけで済む ○ Vuetify式 ○ 初期はこの方針で実装していたが、あまりにも同じコードを量産することになったので plugin利用に 方針変更した ○ モーダルなローディング、 Confirm(Yes/No)ダイアログも同様の実装方針 ● Vue Materialもプラグインで手軽に呼び出せる実装になっている

Slide 41

Slide 41 text

- store/ - folders/ - users/ - actions.js - actionTypes.js - getters.js - getterTypes.js - index.js - mutations.js - mutationTypes.js - users/ - folders/ - index.js store構成 ● ディレクトリ構成例は右を参照 ○ ドメインごとにディレクトリを作る ○ 従属リソースはディレクトリをネストする ○ モジュール毎のファイル構成は同一 ● モジュール数は24だった ○ 画面ごとにモジュールを作るよりは少なく済んだはず ○ 画面で忠実に分けたなら 77必要だったかもしれない

Slide 42

Slide 42 text

従属リソース ● フォルダに所属しているユーザーを folders/users というモジュールに切り分けて いる ○ ユーザーというドメインに従うなら、 usersに全てまとめるべきかもしれない ● 分けているのは実践的な理由 ○ フォルダ所属ユーザー一覧画面では、所属ユーザーの一覧と、所属させたい候補ユーザーの一覧 を同時に表示する必要がある →「ユーザー一覧」が2つ必要 ○ 他の従属リソースも大抵同様の画面がある ○ usersにまとめると、「ユーザー一覧」が usersのstateに大量発生してしまう ○ 付随してactionやmutationも増えていく ○ 切り出してしまったほうが見通しが良い

Slide 43

Slide 43 text

store定数 ● actionTypes、getterTypes、mutationTypesは関数名の定数定義 ○ actionは永続化データの変更を伴うので、 CREATE/DELETE/UPDATEなどの命名 ○ mutationはstore内データの変更なので、 ADD/REMOVE/UPDATEなどの命名 ○ getterはドメインモデル名そのまま(右参照) ● 良かったこと ○ 利用箇所をgrepで探しやすい ● 悪かったこと ○ ファイル数とimportが増える ■ 1ファイルにまとめてみる ■ 右のようなtypes.jsを設置してみる

Slide 44

Slide 44 text

一覧データと詳細データ ● データ置き場は分けている(右図) ○ 一覧 = users#index ○ 詳細 = users#show ● モデルも異なるものとして扱う ○ 一覧をベースに、プロパティを追加したものが詳細 ■ 一覧:id、name、icon、createdAt… ■ 詳細:一覧+α(自己紹介とか) 一覧 詳細 一覧 詳細 V.S.

Slide 45

Slide 45 text

一覧データと詳細データのgetter例 ● フォルダ一覧と詳細のモデルを返すgetter実装 ○ getterはstateの内容をモデルに変換して返すだけ → ドメイン駆動Vuex ○ モデル変換は従属リソースなど他の場所でも頻出なので util化しておく

Slide 46

Slide 46 text

一覧画面から詳細画面への遷移 ● 一覧画面 ○ usersをfetch ■ ページネーションやソート情報はコンポーネントが管理 ● URLクエリとの連携もコンポーネント側で管理 ■ ページネーションなしの総数( users_size)はstateに保管 ○ 詳細表示したいユーザーを選択したら、その idと共に$router.push ● 詳細画面 ○ routerからprops経由でidを受け取る ○ userをfetch ○ stateには一覧が残ったままだが気にしない ○ userに更新を加えても一覧側には反映しない ■ 一覧画面に遷移したら usersのfetchが常に走らせる 初期データfetchは遷移先画面の役割 => 画面リロードしても同じ処理で済む

Slide 47

Slide 47 text

詳細データfetch工夫の歴史(初期) ● actionはAPIにリクエストし、レスポンスをmutationに受け渡す ○ ドメイン駆動Vuex ● 問題 ○ stateには前回取得した詳細データが残っていることがある ○ 新規の詳細データfetchが完了するまでの間、前回の詳細情報が画面に表示されてしまう (gif)

Slide 48

Slide 48 text

詳細データfetch工夫の歴史(中期) ● 新規の詳細データfetchを行うときに、前回データのクリアを行った ○ 前回データが表示される問題は解決 ● 問題 ○ 詳細データが必要な画面は複数存在したりする ○ それらの画面間で遷移した場合、毎回詳細データクリアが挟まり UXが悪い ■ コンポーネントはv-ifで描画制御を行っていたりするため stateクリアmutation (gif)

Slide 49

Slide 49 text

詳細データfetch工夫の歴史(現在) ● 詳細データが既に取得済の場合、データクリアをしない ○ 詳細データfetchは続行し、完了したら無言で差し替える ○ 更新されていない情報が画面に表示される可能性はある ■ fetch完了までの一瞬&完了したら差し替わるので気にしない ● 一瞬で済まないのなら APIを高速化! stateクリアをするかの分岐 (gif)

Slide 50

Slide 50 text

詳細データfetchのコンポーネント側 ● getterからモデルを取得 ● モデルロードをactionに要求 ○ mountedなどで実行する ● ロードやアラートなど、画面都合の処理はコンポーネントが担当 ● アンダーバー付き命名は独自ルール ○ 見分けやすい ○ 基本的に直実行はしない ○ 前後処理をラップした methodを作る ○ (Vue的にはアンダーバー非推奨かも)

Slide 51

Slide 51 text

テスト

Slide 52

Slide 52 text

ユニットテスト ● Karma、Mocka、power-assertで構成 ● Vuexのactions、getters、mutationsは網羅 ● utilとして切り出した処理は網羅 ● .vueファイルはやっていない ○ 開発初期にわりきってしまった ○ storybookで頑張る方針 ○ 難しい処理は書かない &utilに切り出してテスト ■ 権限判定、バリデーションなど

Slide 53

Slide 53 text

コンポーネントテスト ● Storybookで.vueファイルを網羅 ○ 入力(props)による分岐はなるべく網羅 ● 全画面をモックで表示 ○ バックエンドなしの完全モック ■ 独立したフロントエンドの実現 ○ knobを使って権限やプランを切り替え可能にした ■ 全パターン網羅は辛すぎた ■ せめて切り替えて任意の条件を再現できるようにした

Slide 54

Slide 54 text

Storybookスクショ

Slide 55

Slide 55 text

Storybookモック動作 (gif)

Slide 56

Slide 56 text

How: Vuexモック ● APIとの対話はVuexに集約されている ○ ドメイン駆動Vuex ○ VuexをモックできればAPIを丸ごとモックしたことになる ● コンポーネントとVuexのインタフェースはactionsとgettersのみ ○ actionsは return Promise.resolve() すればいいだけ ■ モックなので全部正常系にしてしまう ■ 非同期間が欲しければ setTimeout を挟む ■ 一部 resolve でデータを返している actionがあったので、それは都度対応が必要 ○ gettersはモックのモデルを返せばいいだけ ● 最初は手間だが、storeの丸ごとモックを一度作ってしまえば全画面で使うことがで きる

Slide 57

Slide 57 text

How: vue-routerモック ● storybook-routerを使う ○ https://github.com/gvaldambrini/storybook-router ● ルーティング設定をゴニョゴニョしていい感じに使う

Slide 58

Slide 58 text

1年かけた学び

Slide 59

Slide 59 text

闇のextends V.S. 薄闇のmixins ● コンポーネントは親に対してpropsと$emitというインタフェースを公開している ● その他のdata、computed、methodsは全てprivateな存在 ● extendsはこの原則に横穴を開けてしまう ○ 継承されていると知らずに修正したことで不具合にしばしば遭遇した ○ コンポーネントは閉じた存在という意識が強いため、横穴に気付くのが非常に難しい ● mixinsであれば、横穴が存在することが前提なので危険が減る ● どちらも使わないに越したことはない ○ slotで表現できないか十分に検討するべき ○ slotを制する者はVueを制する ○ どうしても無理なら最後の手段で mixins

Slide 60

Slide 60 text

1年かけた学び ● コンポーネントは自己完結しているべき ○ 親とのインタフェースである propsと$emit以外はいつでも変更できるのが理想

Slide 61

Slide 61 text

禁断の /deep/ ● scopedなスタイルを外側からこじ開けて上書きできる禁術 ○ https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html ● 使っていいシーンは非常に少ない ○ 外部ライブラリによるコンポーネントスタイルをどうしても変更したいとき ○ v-htmlなどで動的にdom生成してスタイルを当てたいとき ● 軽い気持ちで絶対に使ってはいけない ○ 「この画面でだけマージンがちょっと違うから /deep/で変えよう」→NG ○ 「フォントサイズだけ変えれば使い回せるから(ry」 →NG ○ 「色が違うだけ(ry」→NG ● どんなに些細なスタイル変更であっても、propsによる明示的なインタフェースを公 開しておくことが世のため人のため

Slide 62

Slide 62 text

1年かけた学び ● コンポーネントは自己完結してこそ美しい ○ 親とのインタフェースである propsと$emit以外はいつでも変更できるのが理想 ● scopedなスタイルは神聖かつ不可侵である ○ ルート要素は例外

Slide 63

Slide 63 text

魔性の $refs ● $refs自体は悪いものではない ○ エレメントからgetBoundingClientRectしたいときだってある ● $refsを通してdataやcomputedに触るべきではない ○ それはプライバシーの侵害 ○ 先頭を大文字にしたからって publicにはならない ○ 多少汚い実装になってでも $emitや.syncを駆使して避ける ● $refsを通してmethodsに触るべきではない ○ それはプライバシーの侵害 ○ 先頭を大文字にしたからって publicにはならない ○ 関数に切り出してutil化できるはず

Slide 64

Slide 64 text

1年かけた学び ● コンポーネントは自己完結してこそ美しい ○ 親とのインタフェースである propsと$emit以外はいつでも変更できるのが理想 ● scopedなスタイルは神聖かつ不可侵である ○ ルート要素は例外 ● 親が子のdata、computed、methodsにアクセスしてよい場面はない ○ アクセスする手段はあるがそれは地獄の入口

Slide 65

Slide 65 text

1年かけた学び ● コンポーネントは自己完結してこそ美しい ○ 親とのインタフェースである propsと$emit以外はいつでも変更できるのが理想 ● scopedなスタイルは神聖かつ不可侵である ○ ルート要素は例外 ● 親が子のdata、computed、methodsにアクセスしてよい場面はない ○ アクセスする手段はあるがそれは地獄の入口 コンポーネントの外部インタフェースは propsと$emitだけであることを徹底する

Slide 66

Slide 66 text

おまけ

Slide 67

Slide 67 text

シャイなダイアログ ● App.vueに共通で設置した以外のダイアログは使いたい場所で設置する ● 最前面に表示できないときがある ○ 無慈悲なfixedやz-indexに阻まれやすい ○ 特にsafari ● domをbody直下に突っ込んでしまう ○ コンポーネントフレームワークも普通にやってる ○ 片付けは完璧に行う必要あり ○ ドロップダウン系もやってしまうと楽 巷のコンポーネントフレームワークも普通にやっている https://github.com/vuematerial/vue-material/blob/4fa84a4005484563632c898c14974f65eaaca916/src/components/ MdPortal/MdPortal.js#L133

Slide 68

Slide 68 text

ご静聴ありがとうございました