echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【TypeScript】TypeScriptのインストールと各種環境設定

現在仕事でnode.jsを使用しており、「勉強も兼ねてTypeScriptを導入しよう!」と思い導入したので、その際のメモ。1

前提

参考ページ

packege.jsonの作成

  1. ターミナルソフト(「コマンドプロンプト」とか)を起動。
  2. package.jsonを作成したいフォルダ(アプリケーションのルートフォルダなど)にカレントフォルダを移動。
  3. 下記コマンドを実行。
> npm init
  • ここでは「npm init」の詳細は割愛。
  • 質問事項については、名前以外は「yes」にしておいてOK。(後で変更可能)

TypeScriptのインストール

  • ターミナルソフトで、インストール先フォルダをカレントフォルダにする。(package.jsonと同じで良いなら不要)
  • 下記コマンドを実行。
> npm install --g typescript

テストソースの作成

カレントフォルダに「test.ts」というファイルを作成し、下記ソースを書く。

class Student {
    fullName: string;
    constructor(public firstName: string, public middleInitial: string, public lastName: string) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
        console.log(this.fullName);
    }
}
  
let student = new Student('Leon', 'Scott', 'Kenedy');

その後、ターミナルソフトで下記コマンドを入力。

> tsc test.ts

問題なければ、下記の状態になるはず。

  • ターミナルにカレントフォルダのみが表示される。
  • カレントフォルダに、test.jsファイルが作成される

あとは、test.jsをnode.jsで実行し、下記結果が表示されればOK.

> node test  
Leon Scott Kenedy

その他

ブラウザ(クライアント)側javascript作成時の注意

もちろんTypeScriptでブラウザ側のjavascriptを作成することも可能。
ただしブラウザ側は、「node.jsの「require」(=TypeScriptの「import」)が使用できないため、そのままでは外部ファイルやライブラリをできない、という問題がある。

そこで、この問題を解決する必要があるが、その1つが「browserify」というモジュール。

これは、

  • require先のjsファイルの内容も、まとめて1ファイルにまとめてしまう、というモジュール。
  • require先jsファイルがさらにrequireしている..などの場合でも、依存関係のあるjsファイルをまとめて1つにしてくれる。
  • 結果、requireが不要になるので、ブラウザ側でもrequire先のjsdファイルを使用できる。

手順としては、

  • npmコマンドでインストールして、
> npm install -g browserify
  • 統合先のjsファイルを「-o」オプションで指定して、
> browserify test.js -o bundle.js
  • 最後に、作成された統合先のjsファイルをHTMLなどに埋め込めばOK。
> <script src="bundle.js"></script>
  • 大元のjsファイル(色々requireしているファイル)を「test.js」とします。
  • 統合先ファイルに命名ルールはないですが、通例として「bundle.js」とするようです。

※もちろん、「browserify」以外にも対応策はいろいろありますので、興味があれば探してみてください。

型定義ファイルの作成

「型定義ファイル」とは、

  • 「各種モジュールをTypeScriptで使用可能にする」ためのファイル。
  • Javascriptで記載されてるモジュールを、TypeScriptに変換するためのもの。
  • 型定義ファイルを用意すると、VSCodeでインテリセンスなどが使用可能になる。
  • 型定義ファイルは、下記のコマンドでインストール可能。
> npm install -save @types/(モジュール名) 

※各モジュールの型定義ファイルのインストールコマンドは、下記サイトで検索可能です。

tsconfig.jsonの設定(2019/5/5追記)

tsconfig.jsonとは、

  • TypeScriptや、そのコンパイル時の各種設定を定義するためのファイル。
  • プロジェクトのルートフォルダ(=package.jsonと同じ場所)に保存することで、設定が有効になる。
  • tsconfig.jsonを作成するには、プロジェクトのルートフォルダで下記コマンドを実行すればOK。
> tsc --init 
定義 説明
module モジュール関連のコードをどの方式として扱うか(Common.js, umdなど)
target JavaScriptのバージョン(ES5, ES2015など)
noImplicitAny 型未定義の変数などをエラーとするか(trueならエラー、falseならany型にする。)
strict 型厳格な方チェックを行うか(trueなら実施する。)
outDir コンパイル後のjsファイルの保存先フォルダ(未指定ならtsconfig.jsonがあるフォルダ)
rootDir tsファイルの保存先のルートフォルダ(未指定ならtsconfig.jsonがあるフォルダ)
esModuleInterop common.jsモジュールとESモジュール互換的に扱えるようにするかどうか
strictNullChecks nullやundefinedのチェックを厳格にする(number型の初期値にnullを代入不可、など)
sourceMap souuceMapを作成するかどうか(TypeScriptコード上でブレークポイント設定など、デバッグ可能になる)
exclude tsファイルの保存先として除外するフォルダ(ここで指定したrootDir以下の全フォルダ内の*.tsファイルが、コンパイル対象になる)

とりあえず、今回はこの辺で。


  1. 仕事ではjavascriptを直で書いてます。

【Visual Studio Code】Settings Syncで環境を同期する

前回のブログで、最後に「環境の再現なら、Settings Syncを使えば...」という記載をしたのですが、VS Codeでの環境を同期する拡張機能「Settings Sync」が使いやすかったので、それを使用した環境の同期方法を書きました。

前提

前提として、下記が必要になります。

Githubの「個人アクセストークン」の取得方法

  1. Githubにログインして、右上のメニューから「Settings」を選択
  2. 左のメニューから「Developer Settings」を選択
  3. 左のメニューから「Personal Access Tokens」を選択する。
  4. 「Token description」には任意の文字列(「Settings Sync Token」とか)を入力し、チェックボックスは「gist」のみチェックを付ける。
  5. 1~4を終えると、トークンが表示されるので、メモなりコピーなりしておく。
    • このトークンは一度画面を閉じると、再度表示できないので注意。
    • トークンを忘れたら、「Generate New Token」で別のトークンを生成する。

f:id:Makky12:20190503195003p:plain f:id:Makky12:20190503195017p:plain f:id:Makky12:20190503195028p:plain

インストール&VSCode設定のアップロード方法

  • インストールは、VS Codeの「拡張機能」から「Settings Sync」で検索すれば見つかるので、インストールすればOK。
  • インストール完了後、コマンドパレットの「Sync:設定をアップロード」(またはshift+alt+「u」)を実行。
  • Github の個人アクセストークンを入力してください」と表示されるので、先程取得したトークンを入力する。

f:id:Makky12:20190503195509p:plain f:id:Makky12:20190503195047p:plain

「Gist ID」の確認方法

  1. アップロードを完了後、Github右上のメニューから「Your Gists」を選択
  2. 「cloudSettings」という項目があるので、選択。
  3. 「cloudSettings」のURLを見ると、「https:// gist.github.com/(ユーザー名)/(文字列)」となっており、最後の(文字列)の部分がGist IDになる。

設定のダウンロード方法

  1. コマンドパレットの「Sync:設定をダウンロード」(またはshift+alt+「d」)を実行。
  2. Github の個人アクセストークンを入力してください」と表示されるので、最初に取得したトークンを入力する。
  3. 「Gist Id を入力してください」と表示されるので、先程確認した「Gist ID」を入力する。
  4. すると、設定のダウンロードが始まる。(「出力」にダウンロード状況が表示される。)

トークンとGist IDの順番を間違えないように注意!!
※設定をダウンロードすると、元々ローカルPCにインストールした拡張機能はなくなってしまう(「マージ」ではなく「上書き」)。
なのでダウンロード前に、あらかじめアップロードしておくこと!!

f:id:Makky12:20190503195055p:plain

トークンやGist IDを間違えてしまったら?

  • コマンドパレットの「Sync:拡張機能の設定をリセット」を実行すれば、トークンやGist IDの設定が消去される。
  • その後、アップロードやダウンロードを実行すると、再度聞かれるので、正しい値を入力すればOK。

未確認事項(確認したいけどできない)

  • 複数台のPCで、それぞれ設定をアップロードすると、複数の「cloudSettings」ができる。
  • Gist側で、各*.jsonファイルの内容をマージしたら、一つに統合できるのか?
  • 下記「対処法が不明な現象」のせいで、確認ができない...

対処法が不明な現象

  • VS Codeの最新版(1.33.1)をインストール後、Settings Syncでアップロードやダウンロードを行おうとすると、下記エラーが発生する。
    • DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
  • 「Buffer()」が脆弱性の関係で非推奨なので別の記載をしろ、というエラーだが、いろいろネットで調べても、有効な回避法が見つかっていない。
  • 対処方法をご存知の方がいれば、Twitterなどでご連絡をお願いします。

最後、尻切れトンボみたいな感じになってしまいましたが、だいたいこんな感じで、環境の同期ができます。

【Visual Studio Code】「ERRO Bad arguments:」で更新ができない場合の対策

現象

Visual Studio Codeで「更新の確認」などを行い、アップデートを行った際、

  • 下の画像のようなエラーが表示され、更新に失敗する。
  • エラーメッセージに記載されているログを確認すると、「ERRO Bad arguments: Code path doesn't seem to exist: (Code.exeの絶対パス)」というログが記載されている。

f:id:Makky12:20190426201106p:plain

【注】

  • 下の画像では消しましたが、実際には一番下に、詳細情報が記載されたログファイルの保存先が表示されています。
  • 「ERRO」は記載ミスではなく、本当にそう書いてあります。(おそらく4文字にしたので、最後の「R」が削られたのかと。)

原因

(Code.exeの絶対パス)の場所に、「Code.exe」がない。

  • インストールした際のユーザーアカウントと、実際に使用しているユーザーアカウントが違う...などの際に起こる場合がある。
  • 使用アカウントを1つに固定していれば起こらないが、1人で複数アカウントを使用していると、まれに起こる。(僕みたいに...)

対策

VS Codeのアンインストール→再インストールする。

【調べたこと】

  • 「設定」にはCode.exeの保存先パスを定義しているような場所はなかった。
  • Code.exeのショートカットを「Code.exe」という名前で(Code.exeの絶対パス)に保存しても、現象変わらず。
  • レジストリエディタ」でそれらしいパスを定義してる場所があったが、変更→再起動しても、症状は改善しなかった。
    • これについては、もう少し突っ込んで調べてもよかったかも...

※個人的には、下記の理由から、リスクを負ってまでレジストリを触るくらいなら、正直再インストールしたほうが良いと思う。

  • Visual Stuidioと違って、言っても「エディタ」なので、ダウンロード&インストール自体はすぐ終わる。
  • 環境(設定や拡張機能)自体は、Settings Syncの様な便利な拡張機能を使えば、すぐ元にに戻せる。

【Docker】ベースイメージ取得時のトラブル対応

今回もDockerネタ。
ベースイメージ取得時(docker buildコマンド実行時など)に発生するトラブルと、その原因など。
ただし今回は本当に備忘録程度に。

ベースイメージ取得時のエラー(その1)

現象

ベースイメージ取得時に下記メッセージが表示され、取得に失敗する。
request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers).

原因

DNSサーバーの自動解決がうまくいっていない。

対策

DNSサーバーを自動解決させるのではなく、自分で指定する。
Docker for Windowsの場合、[Settings] - [Network] - [DNS Server]の設定を「Fixed/8.8.8.8」にするとうまくいった。(僕の場合)

ベースイメージ取得時のエラー(その2)

現象

ベースイメージ取得時に下記メッセージが表示され、取得に失敗する。
unauthorized: authentication required

原因

  • 起動時にログインができていない。
  • メールアドレスでログインしている。

対策

  • 前者は事前に正しくログインが出来ていることを確認する。
  • 後者はメールアドレスではなく、ユーザ名でログインする。

※Docker起動時のログインはメールアドレスでもOKだが、(ベースイメージ取得時など)一部データ取得時の認証が、メールアドレスだと失敗する模様。

以上、今回は本当に備忘録程度に。

【Docker】起動やドライブのマウントができない場合の対処法

※2019/3/12追記:タイトル少し変えました。

最近Laravelの動作環境としてDocker + LaraDockを導入したのですが、Dockerの[Settings] - [Shared Drives]でドライブのマウントに失敗する事例があったので、その対処法のメモ。

Windows版での内容になります。
※「個人のPCにインストールする」前提で書いてます。

Firewall Detected」メッセージが表示される

現象

ドライブのマウント時に、下記メッセージが表示され、マウントができない。
A firewall is blocking file sharing between Windows and containers. See documantation for more info.

原因

セキュリティソフトのファイアウォールが原因で、ブロックされている。

対策

セキュリティソフトのファイアウォールの設定を変更し、Dockerを許可する
※設定方法はセキュリティソフトにより異なる。

【参考】カスペルスキー インターネットセキュリティの場合

  • [設定] - [プロテクション] - [ファイアウォール]を選択
  • [ネットワーク]をクリック
  • 「(DockerNat)」と記載のあるネットワークを[編集]
  • 「ネットワークの種別」を「許可するネットワーク」にする。

Microsoft 365 Businessでログインしている場合に、マウントのチェックが消える

現象

Microsoft 365 Businessアカウント(xxx@onmicorsoft.com)でログインしているユーザーでドライブのマウントを実行すると、

  • 正しいユーザー名・パスワードを入力しても、該当ドライブにチェックが付かず、マウントできない。
  • エラーメッセージは表示されない

原因

アカウント名とセキュリティ ID の間のマッピングに失敗している。
※詳細は下記サイトの「AzureAD の場合」を参照

対策

下記手順を実行する。

  • アカウント名(AzureAD\(ユーザー名))の「ユーザー名」の部分のみのローカルアカウントを[コントロールパネル] - [ユーザーアカウント]から作成する。
  • 再起動後、Microsoft 365 Businessアカウントでログインする(ローカルアカウントではない)。
  • 作成したローカルアカウントの権限を「管理者」にする。(2019/3/12追記)
  • ドライブマウント時に聞かれるユーザー名では、「ユーザー名」の部分のみ入力する。(「AzureAD\」は不要。なおパスワードは変更なし)

起動時に「Access Denied」メッセージが表示される(2019/3/12追記)

現象

Docker起動時に下記メッセージが表示され、Dockerを起動できない。
You are not allowed to Use Docker.
You must be in the "docker-users" group

原因

該当ユーザーがPC内での「docker-users」グループに所属していない。
(インストールユーザーと別ユーザーでログインした、など)

対策

下記手順で、該当ユーザーを「docker-users」に追加する。(管理者権限ユーザーで実行する)

  • Windwosの[コンピュータの管理]を実行。([プログラム] - [管理ツール])
  • 左ツリーから[ローカルユーザーとグループ] - [グループ]をクリック
  • 右側の[docker-users]を右クリック - [プロパティ]を選択
  • [追加]をクリックして、[ユーザーの追加]画面で該当ユーザーを選択して[OK]
  • PCを再起動

ただ、dockerグループのユーザー追加は、セキュリティ上のリスクがある。
(下記サイト参照)
Windows版は上記対策しないとそもそも起動自体が出来ないのであれだけど、
今後は権限(実行可能なコマンド)設定なども可能になる?

僕の場合はこれで解決できたが、もちろん上記では解決できない現象はあるかも。

というか、Docker(やLaraDock)は便利な半面、いろいろあるみたいですね。次回は環境構築や上記以外のトラブルについてのブログを書く予定です。

【Excella】Excella Repotsで、結合セルの値の取得・設定を行う方法

前回、「単一セルのExcella Reports上で書式・スタイルの設定を行う」方法を書きました。

しかし、結合セルの場合、前回記載した

Row row = sheet.getRow(0);
Cell cell = row.getCell(0);
cell.setCellStyle(style);

のように、単純に(結合セルの一番左上の)行・列のインデックスを指定する方法では、対象の結合セルを取得できません。

そこで、結合セルの場合どうするか、というのが今回の内容です。

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.CellRangeAddress;
import org.apache.poi.ss.usermodel.Workbook;
import org.bbreak.excella.core.SheetData;
import org.bbreak.excella.core.SheetParser;
import org.bbreak.excella.core.exception.ParseException;
import org.bbreak.excella.reports.listener.ReportProcessListener;
import org.bbreak.excella.reports.model.ReportBook;
import org.bbreak.excella.reports.model.ReportSheet;

public class SheetFormatSetter implements ReportProcessListener {
    
    public SheetFormatSetter() {
    }
    
    @Override
    public void postParse(Sheet sheet, SheetParser sheetParser, SheetData sheetData) throws ParseException {

        // sheetは、処理対象のシート。
        final int mergedCellCount = sheet.getNumMergedRegions();
        
        for(int i = 0; i < mergedCellCount; i++) {

            // シート内の各結合セルについて、左上のセルの行・列のインデックスを取得する。
            final CellRangeAddress range = sheet.GetMergedRegion(i);
            final int firstRow = range.getFirstRow();
            final int firstCol = range.getFirstColumn();

            // 結合セルの一番左上のセルを取得する。(値は一番左上のセルに設定されてるから)
            Row row = sheet.getRow(firstRow);
            Cell cell = row.getCell(firstCol);

            // セルの値の取得&設定。(ここまでくれば、あとは前回の内容と同じ)
            String value = cell.getStringCellValue();
            cell.setCellValue("new Value");
        }

        return;
    }

説明

ソースを見て、なんとなくわかった人も多いと思いますが、

  • (行・列のインデックス指定による結合セルの直接取得はでき)ないです。
  • (煩雑ですが、シート内の全ての結合セルを取得後、各結合セルの左上のセルを取得するという方法しか)ないです。
  • 結合セルの数は、SheetのgetNumMergedRegions()メソッドで取得できます。

という、ちょっと残念な結果になってしまいました。

注意点

  • 「特定のセル」を取得する際は、getFirstRow()やgetFirstColumn()の値に対して、ifなどでチェックをする必要があります。
  • GetMergedRegion(index)で取得できるセルは、indexを0, 1, 2...のように指定しても、「行の昇順、列の昇順」のように決まった順序で取得できるわけではないので注意です。*1

ということで、今回もExcella Reportsの記事でした。


実は、3月は完全にお休み...なので、1ヶ月まるまる休みです。でもいざ休みとなると、嬉しい反面、やっぱり不安もあったり。

...まあ、3月どころか、4月以降も全く未定なんですけどね...(お仕事のお話がありましたら、TwitterのDMなどで連絡をお願いします。)

*1:実際、ログにgetFirstRow()やgetFirstColumn()の値を記載してみましたが、てんでバラバラでした

【Excella】Excella Reportsで書式・スタイル設定を行う方法

現在、仕事で「Excella Reports」という、JavaからExcel/PDFを出力するライブラリを使用してます。

Excella Reports

便利なのですが、中の人(?)も言っている通り、書式やスタイル設定はちょっと弱いところのようです。

なので、分かる範囲で書式・スタイル設定についてまとめてみました

ソースコード

import org.bbreak.excella.core.SheetData;
import org.bbreak.excella.core.SheetParser;
import org.bbreak.excella.core.exception.ParseException;
import org.bbreak.excella.reports.listener.ReportProcessListener;
import org.bbreak.excella.reports.model.ReportBook;
import org.bbreak.excella.reports.model.ReportSheet;

public class Main {

    public static void main(String[] args) throws Exception {

        // テンプレートファイルの指定
        String templateFilePath = "SampleTemplate.xls";
        String outputFileName = "Sample";
        String outputFileDir = "Sample/Temp";
        String outputFilePath = outputFileDir.concat(outputFileName);

       // ReportBookの作成
        ReportBook outputBook = new ReportBook(templateFilePath,
                outputFilePath, ExcelExporter.FORMAT_TYPE);

        // シートの作成
        ReportSheet outputSheet = new ReportSheet("TemplateSheet", "Sheet1");
        outputBook.addReportSheet(outputSheet);

        // シートのセルに値を設定する。
        int data = 1.23;
        outputSheet.addParam(SingleParamParser.DEFAULT_TAG, "DATA1", data1);

        // ReportProcessListenerの定義&ブックの出力
        ReportProcessor reportProcessor = new ReportProcessor();
        reportProcessor.addReportProcessListener(new SheetFormatSetter());
        reportProcessor.process(outputBook);
    }
}
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.bbreak.excella.core.SheetData;
import org.bbreak.excella.core.SheetParser;
import org.bbreak.excella.core.exception.ParseException;
import org.bbreak.excella.reports.listener.ReportProcessListener;
import org.bbreak.excella.reports.model.ReportBook;
import org.bbreak.excella.reports.model.ReportSheet;

public class SheetFormatSetterimplements ReportProcessListener {

    public SheetFormatSetter() {
    }

    @Override
    public void postParse(Sheet sheet, SheetParser sheetParser, SheetData sheetData) throws ParseException {
        
        // WorkbookはsheetのgetWorkBook()メソッドから取得。
        Workbook wb = sheet.getWorkBook();

        CellStyle style = wb.createCellStyle();

        // 表示形式、フォントの設定
        DataFormat format = wb.createDataFormat();
        style.setDataFormat(format.getFormat("#,##0.###");       
       
        Font font = wb.createFont();
        font.setFontName("MS 明朝");
        font.setFontHeightInPoints((short)10);
        style.setFont(font);

        // 縦横位置&上枠線の設定
        style.setAlignment(Cellstyle.ALIGN_RIGHT);
        style.setVerticalAlignment(Cellstyle.VERTICAL_CENTER); 
        style.setBorderTop(CellStyle.BORDER_THICK);
       
        // 対象セル(A1)を取得し、スタイルを設定
        Row row = sheet.getRow(0);
        Cell cell = row.getCell(0);

        cell.setCellStyle(style);
    
        return;
    }
}

基本的にはリファレンスにも書いてある通り、ReportProcessListenerのメソッド(preXX(), postXX())から、Apache POIのAPIを直接呼び出すことになります。

ブック&スタイルの作成

 Workbook wb = sheet.getWorkBook();
 CellStyle style = wb.createCellStyle();

Apache POIだと、CellStyleをHSSFWorkBookクラスのcreateCellStyle()メソッドで作成しますが、Excella ReportsのReportBookクラスには、それに該当するメソッドはありません。

ただし、ReportProcessListenerのメソッドの引数sheetのgetWorkBook()メソッドでWorkBookが取得できますので、そのcreateCellStyle()メソッドからCellStyleを取得できます。

スタイルの各書式の設定

// 上から順に、位置揃え(横)、位置揃え(縦)、セルの上枠線
style.setAlignment(Cellstyle.ALIGN_RIGHT);
style.setVerticalAlignment(Cellstyle.VERTICAL_CENTER); 
style.setBorderTop(CellStyle.BORDER_THICK);

スタイルの各プロパティの設定は、プロパティごとに設定用のメソッドが用意されているので、それを使用します。

上記のように、あらかじめ用意されている定数をそのまま使用する場合は、該当の定数を引数に指定します。

// 表示形式、およびフォント
DataFormat format = wb.createDataFormat();
style.setDataFormat(format.getFormat("#,##0.###");       
       
Font font = wb.createFont();
font.setFontName("MS 明朝");
font.setFontHeightInPoints((short)10);
style.setFont(font);

定数をそのまま使用するのではなく、自分で色々カスタマイズする場合、上記のようにWorkBookのcreateXX()メソッドで該当のインターフェースを取得後、

そのsetXX()メソッドで各種プロパティを設定し、最後にCellStyleの設定用メソッドの引数として該当のインターフェースのインスタンスを渡します。

実際にセルに設定する

Row row = sheet.getRow(0);
Cell cell = row.getCell(0);

cell.setCellStyle(style);

最後に、SheetのgetRow()メソッド、及びRowのgetCell()メソッドで対象のセルを取得し、そのsetCellStyle()メソッドの引数に先程のCellStyleのインスタンスを渡し、実際に書式・スタイルを適用します。

なお、長くなるので省略していますが、実際にはrowやcellのnullチェックを忘れないようにしてください。(値が設定されていない場合、nullが返ります。)

また、今回は値を決め打ちしたので不要でしたが、実際に表示形式など、値の内容に依存する書式を設定する際は、事前にCellのgetCellType()メソッドで、セルの値の型を調べるのを忘れないようにしてください。

参考サイト


前回の投稿から2ヶ月...確かに新年入ってから、すごくメンタルの調子が悪かったのです。

...でも、もう少し更新頻度は上げていきたいです。