ReactNativeでゴルフスコアカウンタアプリ作ってみた(前編)


# はじめに

今回は、iOSアプリ開発初心者が手探りでReactNativeを使い実際にiOSアプリを開発していく模様をご紹介します。

:::note warn
暖かい目で見守っていただければ幸いです。
:::

# 対象読者

- ReactNative初心者
- 何となく簡単なアプリを作ってみたいと思っている方
- JavaScript、Reactがある程度扱える方(投稿者は扱えるとは言えない)

# この記事でわかること

- ReactNativeの環境構築 (React Native CLI Quickstart)
- ReactNativeを使った簡単なゴルフスコアカウントアプリの作り方
- ある程度動くものが出来上がっていく過程

# 背景

去年10月、友人の勧めで始めたゴルフ。始めてから1週間でコースに連れて行かれて散々な目にあいましたが、今では予定が合えば休日に友人とショートコース回り放題のゴルフ場へ行き日々修行しています。
(おじさま達が、暇さえあればよく素振りする気持ちがわかるようになりました。)
ゴルフでは、1ホールごとに何打でホールアウトしたのかを記録します。その時に必要なのが、スコアカードです。私は、毎回iPhoneのゴルフスコアアプリで毎回のスコアを記録していました。

その時にふと、思ったのです。

「このアプリってどうやって作られているのか？」
私は普段、業務でPHPを扱っているのでiOSアプリとはどんな感じで開発されているのか
マジでわからなかったので、この良い機会に作ってみようかな…と思ったのが始まりです。

## どうやって作るのか？

まず、iOSアプリをどうやって作るのか。からですよね、作ってみたいものの、実際めちゃくちゃ難しそうってのが本音でした…
とりあえず、「iOS アプリ 作り方」で調べてみることに。
そしたら…
実は、難しいと思われがちだが年々開発の難易度は下がってきているみたい。ホントか？笑。
どうやらiOSアプリの開発に適したプログラミング言語としては下記の言語があるみたいです。

- Swift
- Java
- Python
- Objective-C
- JavaScript
- Ruby
- Dart

そうか、そうか、どれも触れたことがないな…
もう心が折れそうになりました…

ん？JavaScript？一瞬触れたことあるな。一瞬だけ。一瞬だけですよ？

どうやら、JavaScriptを用いたReactNativeというものでiOSアプリを開発できるみたいです。
Reactも一瞬触ったことがある。
iOSアプリ開発に適した言語の中でJavaScriptのみ知ってはいた。が、先ほど話したように、本当に一瞬触っただけだったので開発が不安でしかない…
そこで、身勝手なこととは重々承知ですが、最近一緒にゴルフの練習をした会社の先輩を巻き込み、ペアプログラミングすることにしました。
前編では、ReactNativeとはなんなのかをまとめ、手探りで簡単なゴルフカウンタアプリを作成し、ReactNativeになれるところから入り、後編では先輩とペアでちょっとだけ本格的なゴルフカウンタアプリを作成していきたいと思います。

## React Nativeとは
メタ・プラットフォームズ(旧facebook社　　)が開発したオープンソースのネイティブアプリケーションフレームワークです。

ネイティブアプリケーションというのはApple Store、GooglePlayからインストールして利用するアプリケーションのことで、みなさんが聞いたことがあるアプリで採用されています！

- Instagram
- UberEATS
- Discord
- Progate
- NAVITIME

などなど、人気なアプリに採用されていることもあってなんか安心感がありますね。
他にも、メリットデメリットについてもまとめてみました。

### ReactNativeのメリット

- 開発が効率的
    - クロスプラットフォームのアプリ開発用フレームワークであり、iOSとAndroidのアプリを1つの言語で同時に開発できる
- 瞬時に検証できる
    - ホットリロード機能があるので、瞬時にソースコードを反映することができる
- Reactを使ったことのある人には学習が簡単
    - Reactを使用してWebアプリケーション開発を行っている人には、学習コストが少ない

### ReactNativeのデメリット

- 大規模アップデートが頻繁に行われる
    - ReactNativeは頻繁にアップデートが行われるため、アップデートの度に環境も合わせてアップデートしなければいけない。アプデートが要因で、今まで動いていたアプリが動かなくなってしまったり、予期せぬエラーを招いたりしてしまう。
- エラーの原因が追いにくい
    - エラーが発生した場合、ReactNativeではそのエラーが「JavaScriptレイヤーで起きているエラーなのか」「ネイティブレイヤーで起きているエラーなのか」が判断しづらい傾向にあります。
- ネイティブエンジニアには学習コストがかかる
    - ReactNativeは普段からネイティブ開発を行っているエンジニアは、
    - ReactNativeはJavaScriptで記述されるので、普段からSwift、Javaなど、ネイティブ開発を行っているエンジニアの場合、1からJavaScriptを習得する必要があります。

メリットデメリットをまとめると、普段からJavaScript、Reactを扱っているフロントエンジニアの方にとっては開発に取っ掛かりやすく、開発が効率的であることがわかりました。

それに対して、ネイティブエンジニアにとっては学習コストが高めで、頻繁に大規模アップデートが行われることから、アプリの運用がめんどいことがわかりました。



https://udemy.benesse.co.jp/development/app/react-native.html

https://dev.classmethod.jp/articles/react-native-for-web-with-typescript-while-referring-to-the-official-introduction/

https://macro-send.com/blog/reactnative

## 環境構築
全体の流れとしては、以下の通りです。
- Homebrewのインストール
    - Macのパッケージ管理ツール
- Node.jsのインストールが必須
    - JavaScript実行環境
- Watchmanのインストール
    - React Nativeではファイルの変更時に自働で再ビルドを行うが、そのファイル検知のためにWatchmanを利用する

- rbenvのインストール

- Xcodeをインストール
- CocoaPodsをインストール

ReactNativeの公式ドキュメントを参考に上から順に進めていきます。

参考 :　

https://reactnative.dev/docs/environment-setup

まずは、Expo Go Quickstartか、ReactNative CLI Quickstartかを選びます。
- モバイル開発が初めての場合はExpoGo
- モバイル開発に精通している場合はReactNativeCLI

モバイル開発に精通していない初心者ですが、思い切ってReactNativeCLIを選びました。理由は特にないです。
![スクリーンショット 2022-11-27 21.39.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/16dac195-0d4f-7b5d-7161-125055ac6965.png)
次にOSを選びます。
今回はmacでiOSアプリを作りたいので、macOSとiOSを選びました。
![スクリーンショット 2022-11-27 21.46.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/6673df77-22e9-6bcb-c9ed-19030ee21fb0.png)
次に依存関係のインストールです。
- Homebrewのインストール
    - Macのパッケージ管理ツール


Homebrewをインストールしていない方はインストールしておきましょう。

- Node.jsのインストール
    - JavaScript実行環境
- Watchmanのインストール
    - React Nativeではファイルの変更時に自働で再ビルドを行うが、そのファイル検知のためにWatchmanを利用する

JavaScriptの実行環境が必要なので、Node.jsをインストールします。また、ReactNativeでは、ファイルの変更時に自動で再ビルトが行われ、そのファイルの変更の検知をするためにWatchmanをインストールします。
![スクリーンショット 2022-11-27 21.58.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/15d89b93-6e94-7119-5aac-8231f94c5eef.png)
- rbenvのインストール

ReactNativeは、.ruby-versionファイルを使用して、Rubyのバージョンが必要なものと一致していることを確認するそうです。
また、この後インストールするCocoapodsにRubyが必要だそうです。
指示に従ってRubyのバージョンマネージャーをインストールし、適切なRubyのバージョンをインストールしました。

https://github.com/rbenv/rbenv

何をしたか、一部始終を記録しておきます。
公式の手順に乗っ取り初期化
```
rbenv init
```
.zshrcに下記を追記しろと言われるので、追記する。
```
eval "$(rbenv init -)"
```
rbenv-doctorをたたいて、問題ないことを確認する。
```
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor | bash
```
インストール出来るRubyのバージョン一覧を確認。
```
rbenv install -l
```
ここでどん詰まりしました。どうやら2.7.5でいけるっぽいのでインストールする。
```
rbenv install 2.7.5
```
2.7.5を設定する
```
rbenv global 2.7.5
```
Rubygemsをインストールする
```
gem install bundler
```
ターミナルを再起動してRubyのバージョン確認
```
ruby -v
ruby 2.7.5p203
```
- Cocoapodsをインストール
```
brew install cocoapods
```
- Xcodeをインストール
Xcodeをインストールしましょう。(インストールは気持ち長めです)
最も簡単な方法としては、macのAppStore経由でインストールするよいと公式では仰ってます。

ここまできたら、プロジェクトを作成しましょう！
プロジェクトを配置したい場所に下記を実行します。
AwesomeProjectの箇所には、任意のプロジェクト名を入れます。
```
npx react-native init AwesomeProject
```
そうするとこのように表示されます！！
![スクリーンショット 2022-11-27 23.22.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/44cebc5c-c808-5ad8-547e-8a8923baf7df.png)


次にプロジェクトフォルダー内で次のコマンドを実行します。
```
npx react-native run-ios
```
そうすると、シュミレータが起動し準備が整います。
![スクリーンショット 2022-11-27 23.33.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/f7ce7044-b1e0-5d90-2110-510a2e159c71.png)

## カウントアプリを作ってみる

実際にゴルフカウンターアプリというものを作っていきましょう！
手探りでなんとなく作っていきます。

### 仕様
まず、ゴルフカウンターとはゴルフをやる上で必要不可欠なものです。
自分が何回打ったのかを忘れてしまわないように記録しておくための便利なアイテムです。
参考 :

https://my-best.com/6289

## 機能
今回は、簡単に下記の機能を実装していきたいと思います。

- 現在のホールの記録
- 現在のホールのパット数
- 現在の打数

では、実装していきましょう！

## ソースコード

```react
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */

import React, {useState} from 'react';

import type {Node} from 'react';

import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  FlatList,
  Button,
} from 'react-native';

const App: () => Node = () => {
  const [number, setNumber] = useState(0)
  const [score, setScore] = useState([])
  const [holeCount, setHoleCount] = useState(1)
  const [parCount, setParCount] = useState(3)

  const increase = () => {
    if (number < 20) {
      setNumber(number + 1)
    } 
  }

  const decrease = () => {
    if (number > 0) {
      setNumber(number - 1)
    }
  }

  const clear = () => {
    setNumber(0)
  }

  const holeIncrease = () => {
    if (holeCount < 18) {
      setHoleCount(holeCount + 1)
    }
  }
  const holeDecrease = () => {
    if (holeCount > 1) {
      setHoleCount(holeCount - 1)
    }
  }
  const parIncrease = () => {
    if (parCount < 5) {
      setParCount(parCount + 1)
    }
  }
  const parDecrease = () => {
    if (parCount > 3) {
      setParCount(parCount - 1)
    }
  }

  const onRegistration = () => {
    const newScore = [...score, {
      hole: holeCount,
      par: parCount,
      score: number,
      key: Math.random().toString(),
    }]
    setNumber(0)
    setScore(newScore)
  }

  // Score情報を返す
  const renderItem = ({item}) => {
    return (
      <View style={styles.scoreItem}>
        <View>
          <Text style={styles.scoreItemText}>H{item.hole}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Par{item.par}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Score {item.score}</Text>
        </View>
      </View>
      )
  };

  return (
    <View style={styles.container}>
        {/* ホール情報選択 -Start- */}
        <View style={styles.basicPreference}>
          <View>
            <TouchableOpacity onPress={holeDecrease}>
              <View style={styles.holeSettingButton}>
                <Text>＜</Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              H{holeCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={holeIncrease}>
              <View style={styles.holeSettingButton}>
                <Text>＞</Text>
              </View>
            </TouchableOpacity>
          </View>
          <View>
            <TouchableOpacity onPress={parDecrease}>
              <View style={styles.holeSettingButton}>
                <Text>＜</Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              Par{parCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={parIncrease}>
                <View style={styles.holeSettingButton}>
                  <Text>＞</Text>
                </View>
            </TouchableOpacity>
          </View>
        </View>
        {/* ホール情報選択 -End- */}

        {/* Scoreの結果表示 -Start- */}
        <View style={styles.scoreContainar}>
          <View style={styles.viewScore}>
            <FlatList
              data={score}
              renderItem={renderItem}
            />
          </View>
        </View>
        {/* Scoreの結果表示 -End- */}

        {/* カウンタ表示 -Start- */}
        <View style={styles.viewCounterContainer}>
          <View style={styles.viewCounter}>
            <Text style={styles.viewCounterText}>{number}</Text>
          </View>
        </View>
        {/* カウンタ表示 -End- */}

        {/* カウンタボタン -Start- */}
        <View style={styles.countButtonContainer}>
          <TouchableOpacity onPress={decrease}>
            <View style={styles.decreaseButton}>
              <Text style={styles.countButtonText}>-</Text>
            </View>
          </TouchableOpacity>
          <TouchableOpacity onPress={increase} >
            <View style={styles.IncreaseButton}>
              <Text style={styles.countButtonText}>+</Text>
            </View>
          </TouchableOpacity>
        </View>
        {/* カウンタボタン -End- */}

        {/* Crearボタン、決定ボタン -Start- */}
        <View style={styles.buttonItemContainer}>
          <TouchableOpacity style={styles.clearButton}>
          <Button
            title='Clear'
            onPress={clear}
            color='black'
            />
          </TouchableOpacity>
          <TouchableOpacity style={styles.registrationButton}>
            <View>
              <Button
              title='OK!'
              onPress={onRegistration}
              color='black'
              />
            </View>
          </TouchableOpacity>
        </View>
        {/* Crearボタン、決定ボタン -End- */}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: 'black',
  },

  basicPreference: {
    flexDirection: 'row',
    width: '100%',
    justifyContent: 'center',
    marginTop: 90,
  },

  holeSettingButton: {
    height: 50,
    width: 50,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderWidth: 2, 
    backgroundColor: 'white',
  },
  ViewBasicPreference: {
    height: 50,
    width: 80,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderColor: 'white',
    borderWidth: 1,
  },
  ViewBasicPreferenceText: {
    color: 'white',
    fontWeight: 'bold',
    fontSize:30,
  },

  scoreContainar: {
    height: '30%',
    width: '100%',
    alignItems: 'center',
    marginTop: 10,
  },
  viewScore: {
    height: '100%',
    // borderWidth: 1,
    // borderColor: 'white',
    alignItems: 'center',
  },

  scoreItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    height: 50,
    width: 300,
    backgroundColor: 'gainsboro',
    marginBottom: 3,
    padding: 10,
  },
  scoreItemText: {
    fontSize:20,
    fontWeight: 'bold',
  },

  viewCounterContainer: {
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    marginTop: 70,
  },
  viewCounter: {
    width: '20%',
    alignItems: 'center',
    backgroundColor: 'black',
    borderRadius: 10,
    borderWidth: 1,
    borderColor: 'white',
  },
  viewCounterText: {
    fontSize: 50,
    fontWeight: 'bold',
    color: 'white',
  },

  countButtonContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-evenly',
    width: '100%',
    backgroundColor: 'black',
    marginTop: 20,
    marginBottom: 20,
  },
  IncreaseButton: {
    width: 100,
    height: 100,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'red',
    borderRadius: 50,
  },
  decreaseButton: {
    width: 100,
    height: 100,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'blue',
    borderRadius: 50, 
  },
  countButtonText: {
    color: 'white',
    fontSize: 60,
    fontWeight: 'bold',
  },

  buttonItemContainer: {
    width: '100%',
    flexDirection: 'row',
    justifyContent: 'space-evenly',
    marginTop: 10,
  },

  clearButton: {
    backgroundColor: 'white',
    height: 50,
    width: 100,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderWidth: 2,
    borderColor: 'gray',
  },
  registrationButton: {
    backgroundColor: 'white',
    height: 50,
    width: 100,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 10,
    borderWidth: 2,
  },
});

export default App;


```

## 結果

![golfcounter_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/4c74734b-9b7e-3cf4-9e0c-dd3fb87f09cf.gif)

## 解説

アプリの画面については下記の構成です。
① ホール情報の選択
② スコアの表示
③ カウンタ
④ ClearボタンとOKボタン

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2079147/b6137353-59e3-2445-6886-e0a69a0e2d0c.png)

### ① ホール情報の選択
ホール情報の状態を管理するために、useStateを宣言します。
(React若干触っててよかった...)

```react
  const [number, setNumber] = useState(0)
  const [score, setScore] = useState([])
  const [holeCount, setHoleCount] = useState(1)
  const [parCount, setParCount] = useState(3)
```

```react
        {/* ホール情報選択 -Start- */}
        <View style={styles.basicPreference}>
          <View>
            <TouchableOpacity onPress={holeDecrease}>
              <View style={styles.holeSettingButton}>
                <Text>＜</Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              H{holeCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={holeIncrease}>
              <View style={styles.holeSettingButton}>
                <Text>＞</Text>
              </View>
            </TouchableOpacity>
          </View>
          <View>
            <TouchableOpacity onPress={parDecrease}>
              <View style={styles.holeSettingButton}>
                <Text>＜</Text>
              </View>
            </TouchableOpacity>
          </View>
          <View style={styles.ViewBasicPreference}>
            <Text style={styles.ViewBasicPreferenceText}>
              Par{parCount}
            </Text>
          </View>
          <View>
            <TouchableOpacity onPress={parIncrease}>
                <View style={styles.holeSettingButton}>
                  <Text>＞</Text>
                </View>
            </TouchableOpacity>
          </View>
        </View>
        {/* ホール情報選択 -End- */}
```

ReactNativeではHTMLタグが使えないので、ReactNativeが用意してくださったcomponentを使用します。HTMLに例えるとViewはdivタグのようなものです。
TouchableOpacityはbuttonと似ており、タップすると透明になります。
このTouchableOpacityのonPressメソッドが実行された時にそれぞれの関数が実行され、状態が更新されます。
「>」ボタンが押されたら＋１。「<」ボタンが押されたら-１。という感じです。
parCountの初期値が3というのは、一般的にパット数は3,4,5ですので初期値は3にしています。
```react
  const holeIncrease = () => {
    if (holeCount < 18) {
      setHoleCount(holeCount + 1)
    }
  }
  const holeDecrease = () => {
    if (holeCount > 1) {
      setHoleCount(holeCount - 1)
    }
  }
  const parIncrease = () => {
    if (parCount < 5) {
      setParCount(parCount + 1)
    }
  }
  const parDecrease = () => {
    if (parCount > 3) {
      setParCount(parCount - 1)
    }
  }
```
### ② スコアの表示
```react
        {/* Scoreの結果表示 -Start- */}
        <View style={styles.scoreContainar}>
          <View style={styles.viewScore}>
            <FlatList
              data={score}
              renderItem={renderItem}
            />
          </View>
        </View>
        {/* Scoreの結果表示 -End- */}
```
こちらが、スコアの表示部分です。
FlatListを使うことによってリストを表示させることができます。
順番的には、ホール選択、パット数選択、打数入力終了後にOKボタンが押された時にスコアを表示するため、
```react
const [score, setScore] = useState([])
```
実際には、こちらにスコア情報が表示される頃にはスコア情報のオブジェクトが追加されている状態です。
そこで、dataにオブジェクトが代入された配列を代入、renderItemにスコア情報を返す関数を代入します。
```react
<FlatList
    data={score}
    renderItem={renderItem}
/>
```

```react
  // Score情報を返す
  const renderItem = ({item}) => {
    return (
      <View style={styles.scoreItem}>
        <View>
          <Text style={styles.scoreItemText}>H{item.hole}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Par{item.par}</Text>
        </View>
        <View>
          <Text style={styles.scoreItemText}>Score {item.score}</Text>
        </View>
      </View>
      )
  };
```
### ③ カウンタ
```react
        {/* カウンタ表示 -Start- */}
        <View style={styles.viewCounterContainer}>
          <View style={styles.viewCounter}>
            <Text style={styles.viewCounterText}>{number}</Text>
          </View>
        </View>
        {/* カウンタ表示 -End- */}

        {/* カウンタボタン -Start- */}
        <View style={styles.countButtonContainer}>
          <TouchableOpacity onPress={decrease}>
            <View style={styles.decreaseButton}>
              <Text style={styles.countButtonText}>-</Text>
            </View>
          </TouchableOpacity>
          <TouchableOpacity onPress={increase} >
            <View style={styles.IncreaseButton}>
              <Text style={styles.countButtonText}>+</Text>
            </View>
          </TouchableOpacity>
        </View>
        {/* カウンタボタン -End- */}
```
カウンタ表示部分では、現在の打数が表示されます。
裏側の処理としては、ホール情報選択の時と一緒です。
ボタンが押され次第状態が更新されていきます。
「＋」ボタンが押されれば＋１「-」ボタンが押されれば-１という感じです。
```react
  const increase = () => {
    if (number < 20) {
      setNumber(number + 1)
    } 
  }

  const decrease = () => {
    if (number > 0) {
      setNumber(number - 1)
    }
  }
```

### ④ ClearボタンとOKボタン
```react
{/* Clearボタン、決定ボタン -Start- */}
        <View style={styles.buttonItemContainer}>
          <TouchableOpacity style={styles.clearButton}>
          <Button
            title='Clear'
            onPress={clear}
            color='black'
            />
          </TouchableOpacity>
          <TouchableOpacity style={styles.registrationButton}>
            <View>
              <Button
              title='OK!'
              onPress={onRegistration}
              color='black'
              />
            </View>
          </TouchableOpacity>
        </View>
        {/* Clearボタン、決定ボタン -End- */}
```
Clearボタンが押された時にカウンタの状態を０にしています。
OKボタンが押された時は、scoreの配列にスコア情報のオブジェクトを追加してスコアの結果を表示します。

```react
  const clear = () => {
    setNumber(0)
  }

  const onRegistration = () => {
    const newScore = [...score, {
      hole: holeCount,
      par: parCount,
      score: number,
      key: Math.random().toString(),
    }]
    setNumber(0)
    setScore(newScore)
  }
```

# 感想
JacaScript、React、HTML、CSSなどのWeb技術がある程度わかっている、かつ、公式ドキュメント、ReactNativeについての記事を読み漁ればかろうじて動くアプリが作れることがわかりました。ちょっと、環境構築怪しいけど。笑
気合いでなんとかなりました...

本年は大変お世話になりました。どうぞ良いお年をお迎えください。