狛ログ

2023年7月25日火曜日

iPhone(iOS16以降の端末)のブラウザでのinput[type="time"]のレイアウトをCSSで修正する。

7月 25, 2023

 


こんにちは、オフィス狛 デザイン部のSatoです。



先日、モバイルで利用するWebシステムのHTMLを作成した際に、時間を入力する必要があるフォームが必要になりました。
そこで、input[type="time"]を使いました。

HTMLの作成後に、「iPhone端末で表示すると、時間入力欄(input[type="time"])の入力後の文字が上揃えになっているので、他のに入力欄と同じ縦中央揃えに修正してほしい」という報告をいただきました。
指摘いただいた箇所を私の方でもiPhone端末で確認すると、時間入力欄(input[type="time"])だけで入力後の文字の行揃えも中央揃えになっていて、他の入力欄に合わせる為に文字揃えも修正が必要になってきました。


そこで時間を入力するinputにclassをつけ、そのclassで余白をつけ行揃えを左に修正しました。
実機で修正ができていることを確認!これで解決!のはずが、
「iOS16以降の端末のブラウザでは不具合が修正されていません!」という報告をいただきとても困りました😢

私が修正確認に利用した端末のOSバージョンはiOS15でしたので、どうやらiOS16以降iPhone端末のブラウザではinputにclassをつけるだけでは修正が良い感じに反映されていないようです😢💦
非常にジミ〜な不具合ですが、解決に時間が掛かった為解決法を共有いたします。



修正には、「::-webkit-date-and-time-value」という疑似要素を使います。
これをinput[type="time"]につけてあげると、iOSで修正適用されなかった修正がうまく適用されます。
今回の場合は、下記のような記載になります👇

input[type="time"]::-webkit-date-and-time-value {
    padding-top: 10px;
    text-align: left !important;
}

「::-webkit-date-and-time-value」の擬似要素を利用する方法は、input[type="date"]にCSSが上手く反映されない場合でも使えるようです。
参考:https://developer.apple.com/forums/thread/691305
iOSのSafariだけ日付の入力欄のデザインが他のinputや他ブラウザと違って困った際は、是非お試しください!



デモを用意しましたので、良ければこちらもiOS16以降のiPhone端末で表示して確認してみてください👇

See the Pen iOS端末でのtype="time"の文字のレイアウトを他のinputと同じにする by sato (@officekoma_sato) on CodePen.

※端末固有の不具合のため、デベロッパーツールなどの開発ツールのモバイルモードで表示した場合は差異がわからないかと思います。
iOS16以降のiPhone端末のブラウザでお試しください!

【Laravel】GA4を使用して、Google Analyticsの値を取得する方法。

7月 25, 2023

オフィス狛 技術部の mmm(むー)です。

突然ですが、Google Analytics を使用していますか?
弊社では、Laravel のプロジェクトにて、Google Analytics で取得した値を集計する処理があります。先日、Google Analytics の古いバージョンであるUA(Universal Analytics)から、GA4(Google Analytics 4)へ移行しましたので、その時の備忘録を残しておこうと思います。

※UA(Universal Analytics)は2023年7月1日にサービスが終了しました。
(参考:ユニバーサル アナリティクスについて

前提条件

Google Analytics 4 の設定が完了していること。

■ Laravel バージョン
$ php artisan -V
Laravel Framework 8.27.0

■ Comporser バージョン
$ composer --version 
Composer version 2.4.1 2022-08-20 11:44:50

google/analytics-data のインストール

google/analytics-data をインストールします。
composer require google/analytics-data

Google Analytics API を実行するためのコードを記載します。
use Google\Analytics\Data\V1beta\BetaAnalyticsDataClient;
use Google\Analytics\Data\V1beta\DateRange;
use Google\Analytics\Data\V1beta\Dimension;
use Google\Analytics\Data\V1beta\Metric;
use Google\Analytics\Data\V1beta\OrderBy;
use Google\Analytics\Data\V1beta\OrderBy\MetricOrderBy;


// GAコンソール画面にてサービスアカウントの認証情報を確認し、取得してください。
// ファイル名と格納場所は一例となりますので、ご自身の環境に合わせて値を修正してください。
$jsonPath = storage_path('app/analytics/service-account-credentials.json');

// 認証用のライブラリで getenv が使用されているため、putenv の使用が必須のようです。
putenv("GOOGLE_APPLICATION_CREDENTIALS=".$jsonPath);

// GAのコンソール画面にてプロパティIDの値を確認し、ご自身の環境に合わせて値を修正してください。
$propertyId = 12345;

$client = new BetaAnalyticsDataClient();

// Google Analytics API を実行します。(GA4)
$response = $client->runReport([
    'property' => "properties/{$propertyId}",
    // 取得期間
    'dateRanges' => [
            new DateRange([
                'start_date' => '2023-07-01',
                'end_date' => '2023-07-02',
            ])
    ],
    // ディメンション
    'dimensions' => [new Dimension(['name' => 'pagePath'])],
    // 指標
    'metrics' => [new Metric(['name' => 'screenPageViews'])],
    // ソート
    'orderBys' => [
        new OrderBy([
            'metric' => new MetricOrderBy(
                [
                    'metric_name' => 'screenPageViews'
                ]
            ),
            'desc' => true
        ])
    ],
]);

// 取得した値を処理します。例となりますので、具体的な処理については省略します。
foreach ($response->getRows() as $row) {
    // ディメンションの値
    foreach ($row->getDimensionValues() as $v) {
        $pagePath = $v->getValue();
    }

    // 指標の値
    foreach ($row->getMetricValues() as $v) {
        $screenPageViews = $v->getValue();
    }
}

■補足(コードについて)
service-account-credentials.json には以下のような値の設定が必要となります。ご自身の環境に合わせて値を設定してください。
// app/analytics/service-account-credentials.json
{
  "type": "service_account",
  "project_id": "testapi",
  "private_key_id": "...",
  "private_key": "...",
  "client_email": "...",
  "client_id": "1234",
  "auth_uri": "...",
  "token_uri": "...",
  "auth_provider_x509_cert_url": "...",
  "client_x509_cert_url": "..."
}

ディメンションと指標は、公式ページを参考にご自身が必要なものを設定してください。上記の例では、各ページごとのビューを取得しています。
(参考:API のディメンションと指標

■補足(エラーが発生した場合)
この修正をおこなっている時に遭遇したエラーについて、記載します。

1. Google Analytics Data API が無効になっているエラー
下記エラーが表示されている場合、Google Analytics Data API を使用したことがなく、無効になっていることが原因ですので、メッセージに記載されている URL にアクセスして API を有効にしてください。 (「..12345..」の部分は本来プロジェクトのIDとなります)
Google Analytics Data API has not been used in project ..12345.. before or it is disabled.
Enable it by visiting https://console.developers.google.com/apis/api/analyticsdata.googleapis.com/overview?project=..12345.. then retry.
If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

2. bcmath がインストールされていないエラー
下記エラーが表示されている場合、bcmath が入っていないことが原因ですので、サーバーにてインストールを行なってください。(Apache を使用している場合、リロードも行なってください。)
Call to undefined function Google\Protobuf\Internal\bccomp()

■参考サイト
Google Analytics Data for PHP
google-cloud-php
Analytics Data API の概要

以上となります。
参考にして頂ければ幸いです。

2023年2月27日月曜日

2023年オフィス狛の年賀状の制作秘話。

2月 27, 2023


出遅れたオフィス狛 デザイン部のSatoです💦
毎年恒例(?)の年賀状の記事を出さないまま2月になってしまいました!
言い訳になってしまいますが、オフィス狛社員全員私含め2023年1月から2月にかけ色々とてんてこまいでした😭💦
遅れてしまいましたが、2023年もどうぞよろしくお願いいたします!

2月も終わりそうですが、デザイン部の年明け初めての記事ですので去年同様、今年の年賀状のおはなしをしようと思います。
↓去年の年賀状の記事はこちら


2023年の年賀状はこちら!

例年通り弊社サイトやSNSなどのアイコンなどで登場しているこまちゃんとここまちゃんのイラストです。
2023年の干支は卯(うさぎ)!ウサギといえば月でお餅つきをしているウサギ!
……ということでこまちゃんにはウサギさんとお餅つきにチャレンジしてもらいました!
小柄なここまちゃんに餅つきはちょっと危険かな?杵で潰れちゃいそう?と思ったので、添えるだけになりました。



そして、今年の年賀状のラフです。

個人的には②の鏡餅とウサギ耳こまちゃん&ここまちゃんが結構気に入っています。
ここまちゃんの( ´ ω ` )←顔が良いと思ったのですが、ウサギ分がたりなかったのかもですね。
③は宇宙猫のパロディです。背景はフリー素材の宇宙の写真です


そして、①を清書したものが今年の年賀状はになります

ラフの時点で丸い窓状にイラストを切り抜きすることは決めていたのですが、実際に切り抜きしてみると寂しく感じたので、どうにか装飾がつけられないかな?と考えてみて、お正月飾りとしてポピュラーな「しめ飾り」を入れてみました。
実は私、このお正月飾りが「しめ飾り」という名前だということを、今回年賀状イラストを描く際に初めて知りました!


もっと「少しでも重心がずれたらこまちゃんが転びそう!」と見る人もハラハラするような躍動感のあるイラストにしたかったのですが、イメージよりも50%躍動感オフされたようなイラストになってしまいました💦
今後は動きのある絵を練習せねばと思いました!

そのためには遠近含めたパース、構図練り、デッサン…etc色々練習が必要そうです!

2022年11月1日火曜日

「iOSのSafariでheight:100vhが上手くいかない!」の問題をCSSで解決する。

11月 01, 2022

こんにちは、オフィス狛 デザイン部のSatoです。


私がモバイル用サイトをコーディングする際、諸事情によりbodyタグにclassをつけられないことが多いです。

ですので、divタグで全体的な内容を囲んで背景色を設定していたりします。


そうなると、背景色をページごとに設定が必要サイトでSafariなどのiOSのブラウザで要素が少ないページを表示した場合に、下に余白ができたり余白をなくすためにheight:100vhを設定すると不要なスクロールができてしまうことが多々ありました。

不要なスクロールをなくすにはJSを書く必要がありましたが、CSSの新要素で解決できるようになったので、ご紹介します。




CSSの新要素、それは新しく追加された単位であるsvh・lvh・dvhです。

これら3つとも、ビューポートを基準にした単位ですがモバイルサイトをコーディングする際に困っていた部分を解決できる仕様になっています。


★svhはスモールビューポートです。

表示領域の高さが最小の際のサイズが基準になります。

height:100svhを設定すると、表示領域の高さが最小の時(アドレスバーが表示されている時)の高さになります。


★lvhはラージビューポートです。

表示領域の高さが最大の際のサイズが基準になります。

height:100lvhを設定すると、表示領域の高さが最大の時(アドレスバーの表示が小さい時)の高さになります。


★dvhはダイレクトビューポートです。

ブラウザの表示領域の動きに対し、動的に対応してくれます。この単位は今までの悩みを解決してくれるすごい単位です!

height:100dvhを設定すると、ブラウザの表示項目の表示・非表示に合わせサイズをフレキシブルに変更してくれます。



そして、min-height:100dvh;を設定することで、ブラウザの表示項目の表示・非表示に合わせサイズをフレキシブルに変更してくれるので画面の要素が少ない際は余分なスクロールが発生しない、かつ内容が画面に収まらない場合は下まで背景色がつくようになります!


どの単位も、今後スマートフォンサイトをコーディングする際に絶対に役に経ちますので、覚えておきたいですね。

2022年10月29日土曜日

Mavenビルドの「Error code 501, HTTPS Required」エラーに対応する。

10月 29, 2022

オフィス狛 技術部のJoeです。
先日、担当しているJavaのプロジェクトでMavenのビルドを行ったところ、下記のエラーが発生しました。

[ERROR] Failed to execute goal on project web: Could not resolve dependencies for project [プロジェクト]: Failed to collect dependencies at [アーティファクトID]: Failed to read artifact descriptor for [アーティファクトID]: Could not transfer artifact org.springframework.boot:spring-boot-dependencies:pom:1.5.4.RELEASE from/to central (http://repo1.maven.org/maven2/): Failed to transfer http://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/1.5.4.RELEASE/spring-boot-dependencies-1.5.4.RELEASE.pom. Error code 501, HTTPS Required

Maven Central リポジトリからアーティファクトをダウンロードできずに、「501」でエラーとなっています。

原因は、2020年1月15日から、Maven の Central リポジトリ へのHTTP経由による接続のサポートが終了し、HTTPS経由での接続が必須となりました。(ちょっと古い情報ですが)
そのため、このままですと、Central リポジトリ への接続が拒否され、ビルドが失敗します。

解決方法を調査してみると、いくつか対応策が出てきますが、私が解決できた方法をご紹介します。

【環境】
・Spring Tool Suite(Windows):4.15.3
・Maven:3.8.4

※環境の状態によっては、下記のエラーが発生する場合もありますが、同じ対応で解決できました
[ERROR] Failed to execute goal on project web: Could not resolve dependencies for project [プロジェクト]: Failed to collect dependencies at [アーティファクトID]: Failed to read artifact descriptor for [アーティファクトID]: org.springframework.boot:spring-boot-dependencies:pom:1.5.4.RELEASE was not found in https://oss.sonatype.org/content/repositories/snapshots during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of sonatype-snapshots has elapsed or updates are forced

① エラーとなったアーティファクトのバージョンを最新版に上げる

私の利用していたアーティファクト「doma-spring-boot-starter」は、バージョンを最新版(1.1.1 → 1.6.0)にすることでエラーが解消し、アーティファクトをダウンロードできました。
※アーティファクトによっては、最新バージョンとしても今回の事象が解消されていない可能性もあるかと思います

バージョンの上げ方は、Central リポジトリで、ご利用のアーティファクトを検索、最新バージョンを確認いただき、pom.xmlファイルで、対象のアーティファクトの「<version>タグ」で指定下さい。

② Maven Central リポジトリのURLを指定する

①でエラーが解消されない場合や、事情によりバージョンを変更できない場合は、pom.xmlファイルに、下記のとおり、Maven Central リポジトリのURLを明示的に指定することで、エラーを解消することが出来ました。
    <repositories>
        <repository>
            <id>maven</id>
            <name>Maven Central</name>
            <url>https://repo.maven.apache.org/maven2/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

※調査してみると、「<id>タグ」と「<name>タグ」には、下記を指定することで解消できるとの情報もありましたが、私の環境ではエラーを解消できませんでした。
            <id>central</id>
            <name>Central Repository</name>


①、②でpom.xmlファイルを編集し、ビルドしてもエラーが解消されない場合は、プロジェクトの更新(プロジェクトを右クリック > Maven > プロジェクトの更新)し、再度ビルドすることもお試しください。

以上です。
同じエラーが発生した場合に、お役に立てば幸いです。

2022年10月27日木曜日

【Angular】画面遷移前に確認ダイアログ風のポップアップを表示する。

10月 27, 2022
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

今回は画面遷移前に表示する下図のような確認ダイアログ風のポップアップ(以降、確認ポップアップ)を作成していこうと思います。

1からこのような仕組みを作るのは大変なので、「Simple Modal Module」(ngx-simple-modal)というプラグインを使いたいと思います。

※「Simple Modal Module」のインストールや使い方の細かい説明は、弊社メンバーの記事(【Angular・【Simple Modal Module】モーダルを手軽に実装する。)をご覧ください。

当記事では、以前の記事で使用した「アカウント登録機能」に実装する形で、実践的な説明をしようと思います。
と言うことで、是非、以前の記事も参照ください

参照1:【Angular】独自エラーチェック(カスタムバリデーション)を作成する。
参照2:【Angular】コンポーネントの設計(画面ごとの設計)について。
参照3:【Angular】エラーメッセージの管理について考える。

(1)どのコンポーネントでポップアップを呼び出すべきか

まず、どのコンポーネントでポップアップを呼び出すべきか考えてみます。
親(Container)なのか、子(Presentation)なのか・・・・
以前の記事で、こんな定義をしたかと思います。

「Container」として分類するもの
  • 画画面の状態保持に関すること(NgRxのStore操作など)
  • 画面に入力した値の業務チェック(API呼び出しが必要なもの、など)
  • API(バックエンド処理)の呼び出し
  • 画面遷移

「Presentation」として分類するもの
  • 画面表示、及び表示内容の制御(エラー時など)
  • 画面に入力した値のバリデーションチェック
  • 画面に入力した値の関連チェック、及び業務チェック


という事で、ちょっと悩みどころではあるのですが、呼び出しは「子(Presentation)」の方が良いと思います。
確認ポップアップの「OKボタン=画面遷移する」、「キャンセルボタン=画面遷移しない」と考えると、確認ポップアップとしての動きは、あくまで「画面遷移するかどうかの関連チェック」に過ぎない、というのが理由です。
他にも、「キャンセルを押された時に画面の表示を変えたい」という要件があった場合、確認ポップアップを親(Container)で呼び出すと、かなり複雑になってしまうから、というのも理由の1つです。(今回はそのような要件はないですが)

(2)確認ポップアップの作成

呼び出す場所は決まりましたので、呼び出す「確認ポップアップ」用のコンポーネントを作成しようと思います。
弊社のプロジェクトでは、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置しています。

今回は、以下のように作成します。
src/
└ app/
    └ shared/
        └ modal/
            └ containers/
                └ confirmation-dialog/
                    └ confirmation-dialog.component.html
                    └ confirmation-dialog.component.ts

では、それぞれ実装を見ていきましょう。まずは、テンプレート(View)から。
[confirmation-dialog.component.html]
<div class="message">
  <div class="modal-dialog modal-content">
    <div class="modal-header">
      <div class="modal-title"></div>
      <button type="button" class="close" aria-label="閉じる" (click)="onClickCancel()">
        <span aria-hidden="true">×</span></button>
    </div>
    <div class="modal-body">
      <p class="text-center">{{ message }}</p>
    </div>
    <div class="modal-footer p-0">
      <button class="btn btn2" (click)="onClickCancel()">キャンセル</button>
      <button class="btn btn1 btn-block" type="submit" (click)="onClickOk()">OK</button>
    </div>
  </div>
</div>
特筆すべきところは特に無いですが、このコンポーネントを汎用的に利用する為に、表示するメッセージは「{{ message }}」で変数を展開する形にしています。

続いて、コンポーネントの実装です。
[rconfirmation-dialog.component.ts]
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { SimpleModalComponent } from 'ngx-simple-modal';

export interface ConfirmModel {
  message: string;
}

@Component({
  selector: 'koma-confirmation-dialog',
  templateUrl: './confirmation-dialog.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmationDialogComponent
  extends SimpleModalComponent<ConfirmModel, boolean>
  implements ConfirmModel, OnInit {
 
  message: string;

  constructor() {
    super();
  }

  ngOnInit() {}

  onClickCancel() {
    this.result = false;
    this.close();
  }

  onClickOk() {
    this.result = true;
    this.close();
  }
}

少し詳細を説明しています。
まず、下記の記載でポップアップに表示するメッセージを呼び出し元で設定出来るようにしています。
export interface ConfirmModel {
  message: string;
}

あと重要なのは、「ngx-simple-modal」のクラス(SimpleModalComponent)を継承し、先程作った「ConfirmModel」を実装する、と言う事です。
export class ConfirmationDialogComponent
  extends SimpleModalComponent<ConfirmModel, boolean>
  implements ConfirmModel, OnInit, OnDestroy {

「OKが押されたか、キャンセルが押されたか」は、「result」にTrue、Falseを設定することで、呼び出し側へ知らせます。
  onClickCancel() {
    this.result = false;
    this.close();
  }

  onClickOk() {
    this.result = true;
    this.close();
  }

これで確認ポップアップが作成できました。次はこの確認ポップアップを呼び出してみましょう。

※モジュール(NgModule)ファイルの記載は省略していますので、適宜、追加したコンポーネントをモジュールでdeclarationsしておいてください。
※モジュール(NgModule)ファイルでの「SimpleModalModule(ngx-simple-modal)」のインポートについてですが、共通的に使用されることを考えると、「app.module.ts」などでインポートした方が良いと思います。

(3)確認ポップアップの呼び出し

では早速、先程作成した確認ポップアップの呼び出しを実装して行きます。
まずは、呼び出し側の子のテンプレート(View)を実装します。

[register.component.html](form部分のみ抜粋)
    <form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
      <div class="form-input">
        <div>
          <label>携帯電話番号<span>必須</span></label>
          <input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力"
            [ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus>
          <ng-container *ngIf="v.mobilePhoneNumberInvalid">
            <p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p>
            <p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p>
            <p *ngIf="v.mobilePhoneNumberHasErrorFormat" class="error-message">{{ message('msg_error_cellphone_number') }}</p>
          </ng-container>
        </div>
        <div>
          <label>お名前</label>
          <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(任意)"
            [ngClass]="{'alert-danger' : v.nameInvalid}">
          <ng-container *ngIf="v.nameInvalid">
            <p class="error-message">{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}</p>
          </ng-container>
        </div>
      </div>
      <footer class="footer fixed-bottom d-flex">
        <button type="button" (click)="onClickPrev()">戻る</button>
        <button [disabled]="formRegister.invalid" type="submit">確認</button>
      </footer>
    </form>

特筆すべき部分は特になく、単純に下記の部分を追加しているだけです。
<button type="button" (click)="onClickPrev()">戻る</button>
これは単純ですね、「戻る」ボタンを追加しただけ、になります。

では、次は子コンポーネントに戻るボタンを押した時のアクションを実装して行きます。
[register.component.ts]
import { Component, OnInit, Input, Output, OnDestroy, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { CustomValidators } from '@app/shared/validator/custom-validators';
import { RegisterValidator } from './register.validator';
import { AccountViewSaveModel } from '../../models/account';
import { getMessage } from '@app/shared/message/error-messages';
import { SimpleModalService } from 'ngx-simple-modal';
import { ConfirmationDialogComponent } from '@app/shared/modal/containers/confirmation-dialog/confirmation-dialog.component';

@Component({
  selector: 'koma-register',
  templateUrl: './register.component.html',
})
export class RegisterComponent implements OnInit, OnDestroy {
  // Input・Outputの定義。
  @Input() accountViewSave$: Observable<AccountViewSaveModel>;
  @Output() formBack = new EventEmitter();
  @Output() formSubmit = new EventEmitter<AccountViewSaveModel>();

  mobilePhoneNumberMaxLength = 11;
  nameMaxLength = 20;

  registerSubscription: Subscription = new Subscription();

  formRegister: FormGroup = this.formBuilder.group({
    mobilePhoneNumber: [
      '',
      [
        Validators.required,
        Validators.maxLength(this.mobilePhoneNumberMaxLength),
        CustomValidators.mobilePhoneNumberValidator,
      ],
    ],
    name: ['', [Validators.maxLength(this.nameMaxLength)]],
  });

  constructor(
    private formBuilder: FormBuilder,
    public v: RegisterValidator,
    private simpleModalService: SimpleModalService,
  ) {}

  ngOnInit(): void {
    // バリデーション設定
    this.v.formGroup = this.formRegister;

    this.registerSubscription.add(
      // 画面初期値設定
      this.accountViewSave$.subscribe(value => {
        if (value) {
          this.formRegister.controls.mobilePhoneNumber.setValue(
            value.mobilePhoneNumber,
          );
          this.formRegister.controls.name.setValue(value.name);
        }
      }),
    );
  }

  ngOnDestroy(): void {
    // ダイアログ削除
    this.simpleModalService.removeAll();
    // サブスクリプション解除
    this.registerSubscription.unsubscribe();
  }

  onClickPrev(): void {
    // 確認ダイアログの表示
    this.simpleModalService
      .addModal(ConfirmationDialogComponent, {
        message: getMessage('msg_confirm_screen_transition'),
      })
      .subscribe(result => {
        if (result) {
          this.formBack.emit();
        }
      });
  }

  // 画面でSubmitが発生した時の処理
  onSubmit(): void {
    if (this.formRegister.valid) {
      // バリデーションエラーが発生していない場合
      const formModel = {
        mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value,
        name: this.formRegister.controls.name.value,
      } as AccountViewSaveModel;
      this.formSubmit.emit(formModel);
    }
  }

  message(messageId: string, ...args: any[]): string {
    return getMessage(messageId, ...args);
  }
}

中身を細かく見ていきましょう。
まずは、「ngx-simple-modalのSimpleModalService」と、先程作成した確認ポップアップをインポートしています。
import { SimpleModalService } from 'ngx-simple-modal';
import { ConfirmationDialogComponent } from '@app/shared/modal/containers/confirmation-dialog/confirmation-dialog.component';

戻るボタンを押された場合の画面遷移の制御は、親コンポーネント側で行うので、親コンポーネントへのイベント登録(定義)も行います。
@Output() formBack = new EventEmitter();

そして、インポートした「SimpleModalService」はDIする必要があります。
  constructor(
    private formBuilder: FormBuilder,
    public v: RegisterValidator,
    private simpleModalService: SimpleModalService,
  ) {}

次に「戻るボタン」を押した時の処理です。ここで確認ポップアップを表示します。
  onClickPrev(): void {
    // 確認ダイアログの表示
    this.simpleModalService
      .addModal(ConfirmationDialogComponent, {
        message: getMessage('msg_confirm_screen_transition'),
      })
      .subscribe(result => {
        if (result) {
          this.formBack.emit();
        }
      });
  }

ざっくり説明すると、
  • SimpleModalServiceのaddModalに自作コンポーネントを指定して、さらに自作コンポーネントのmessageに、表示用の文言を設定している
  • ポップアップの戻りは、subscribeで受け取り設定する(resultにTrue、Falseが設定されて戻ってくる)
と言う感じですね。
Trueの場合(確認ポップアップで「OK」が押された場合)、親画面へイベントを委譲しています。

(4)親コンポーネントの実装

確認ポップアップの表示とは直接関係ないですが、親コンポーネントも実装しておきましょう。
まずはテンプレート(View)に、子のOutput(formBack)と親のイベント(onClickBack)の紐付けを追加します。

[account-register.component.html]
<koma-register
  (formBack)="onClickBack()"
  (formSubmit)="onSubmit($event)"
  [accountViewSave$]="accountViewSave$"
></koma-register>

続いて、コンポーネント側の実装です。
[account-register.component.ts]
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Router } from '@angular/router';
import { AccountViewSaveModel } from '../../models/account';
import * as fromAccount from '../../store/reducers';
import * as AccountActions from '../../store/actions/account.actions';

@Component({
  selector: 'koma-account-register',
  templateUrl: './account-register.component.html',
})
export class AccountRegisterComponent implements OnInit {

  // Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
  // 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
  accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));

  constructor(public store: Store<fromAccount.State>, private router: Router) {}

  ngOnInit(): void {}

  onClickBack(): void {
    // ホーム画面に遷移する(戻る)
    this.router.navigateByUrl('/home/top');
  }

  onSubmit(formModel: AccountViewSaveModel): void {
    // 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
    this.store.dispatch(
      AccountActions.setRegisterPostData({ data: formModel }),
    );
    // 確認画面へ遷移する
    this.router.navigateByUrl('/account/register-confirm');
  }
}

「NgRxのStore」などが使われていますが、ここは一旦スルーしてください。(いずれ、NgRxの実装方法も記事にしようと思います。)
ここでは、画面で入力された値はStore(と言う場所)に格納されている、ぐらいの理解でOKです。
(重要なのは、「データ保存方式」ではなく、あくまで「戻るボタンの制御」なので)

と言う事で、中身を見ていくと・・・・先程、子側と紐付けたイベント(onClickBack)が実装されていますね。
画面遷移するだけの処理ですが、URLがそのまま固定文字列で記載されているのが気になります。
こちらも今回の記事の本質では無いですが、URLの固定文字列も定数化しちゃいましょう。

(2)でも説明したように、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置します。
今回は、以下のように作成します。
src/
└ app/
    └ shared/
        └ constant/
            └ page-constants.ts

実装は下記のようになります。
[page-constants.ts]
export namespace url {
  export const HOME_TOP = '/home/top';
  export const ACCOUNT_REGISTER_CONFIRM = '/account/register-confirm';
}

では、先程の固定文字列を定数に変更してみます。
下記が、最終的な親のコンポーネントとなります。
[account-register.component.ts]
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Router } from '@angular/router';
import * as PageConstants from '@app/shared/constant/page-constants';
import { AccountViewSaveModel } from '../../models/account';
import * as fromAccount from '../../store/reducers';
import * as AccountActions from '../../store/actions/account.actions';

@Component({
  selector: 'koma-account-register',
  templateUrl: './account-register.component.html',
})
export class AccountRegisterComponent implements OnInit {

  // Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
  // 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
  accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));

  constructor(public store: Store<fromAccount.State>, private router: Router) {}

  ngOnInit(): void {}

  onClickBack(): void {
    // ホーム画面に遷移する(戻る)
    this.router.navigateByUrl(PageConstants.url.HOME_TOP);
  }

  onSubmit(formModel: AccountViewSaveModel): void {
    // 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
    this.store.dispatch(
      AccountActions.setRegisterPostData({ data: formModel }),
    );
    // 確認画面へ遷移する
    this.router.navigateByUrl(PageConstants.url.ACCOUNT_REGISTER_CONFIRM);
  }
}

(5)メッセージを追加する

最後に確認ポップアップに送る為のメッセージ定義もしておきましょう。

[error-messages.ts]
export const errorMessages: { [key: string]: string } = {
  msg_error_field_required: '{0}は必ず入力してください。',
  msg_error_field_max: '{0}は{1}文字以内で入力してください。',
  msg_confirm_screen_transition:
    '現在の入力中の情報は破棄されます。前の画面に戻りますか?',
};

function formatMessage(msg: string, ...args: any[]): string {
  return msg.replace(/\{(\d+)\}/g, (m, k) => {
    return args[k];
  });
}

export function getMessage(messageId: string, ...args: any[]): string {
  return formatMessage(errorMessages[messageId], ...args);
}

これで、全ての実装が完了しました。
この応用(むしろ、今回の方が応用ですが)で、「アラート表示ポップアップ」や、「完了メッセージ表示ポップアップ」など、色々作れると思います。

ぜひ、色々カスタマイズしてみてください!


【AWS】プライベートサブネット内のLambda関数でKMSを利用する(NAT Gatewayとエンドポイントについて)

10月 27, 2022

オフィス狛 技術部のJoeです。
担当のプロジェクトで、AWSのプライベートサブネット内のLambda関数で、環境変数の複合化を行うため、AWS KMSを利用する必要がありました。
インターネットゲートウェイとルーティングできないプライベートサブネット内のLambda関数が、VPC外のサービスであるAWS KMSを利用するためには、2つの方法があります。

方法と設定

1.NAT Gatewayを作成する


パブリックサブネットにNAT Gatewayを作成し、インターネット経由でKMSに接続します。

【NAT Gatewayの設定手順】
 ※NAT Gateway 以外は、既に構築済みの前提として、手順は省略させていただきます

① VPCのサービスから、NAT Gateway を作成します。
・名前:任意の名前を指定します。
・サブネット:NAT Gatewayを作成するパブリックサブネット を選択します。
・接続タイプ:「パブリック」を選択します。
・Elastic IP 割り当て ID:任意のElastic IP アドレスを選択します。
 ※「Elastic IP を割り当て」ボタンで、新たなElastic IP を割り当てることも可能です

② プライベートサブネットのルートテーブルのルートに、①で作成したNAT Gateway を指定します。
・送信先:「0.0.0.0/0」を指定します。
・ターゲット:①で作成したNAT GatewayのID(「nat-…」)を指定します。

■注意点
・Elastic IP アドレスは、AWS アカウントごとに各リージョンで 5 つのデフォルト制限がありますので、既に5つ割り当てている場合、制限の増加をリクエストする必要があります。


2.エンドポイントを作成する(PrivateLink)

KMS用のエンドポイント(インターフェイスエンドポイント)を作成し、プライベートリンクでKMSに接続します。

【エンドポイントの設定手順】
 ※エンドポイント以外は、既に構築済みの前提として、手順は省略させていただきます

① VPCのサービスからエンドポイントを作成します。
・名前タグ : 任意の名前を指定します。
・サービスカテゴリ : 「AWS のサービス」を選択します。
・サービス : 「com.amazonaws.ap-northeast-1.kms」を選択します。
・VPC : ご自身の環境の、VPC を選択します。
・DNS 名を有効化 : プライベート DNS 名を有効にする場合、チェックしてください。
・DNS レコードの IP タイプ : DNS 名を有効にした場合、IPv4 or IPv6 を選択ください。
・サブネット : ご自身の環境の、プライベートサブネット を選択します。
・IP アドレスタイプ : 選択したサブネットのIP アドレスタイプから、IPv4 or IPv6 を選択ください。
・セキュリティグループ : 任意のセキュリティグループを指定します。
・ポリシー : 今回は「フルアクセス」としますが、制限したい場合「カスタム」を選択し、ポリシーを設定します。

■注意点
・エンドポイントに設定するセキュリティグループは、インバウンドルールに「HTTPS」を許可する必要があります。


NAT Gatewayとエンドポイントのどちらを利用したほうが良いのか


ご利用に環境による場合もあるかもしれませんが、今回のように、新たにプライベートサブネット内のLambdaやEC2から、AWSのサービス(KMS、S3など)を利用する場合は、 下記の理由などから、エンドポイントを作成し、プライベートリンクで接続が良さそうです。

ただし、対象の AWS サービスで VPC エンドポイントが利用できない場合もありますので、こちらで事前にご確認ください。
AWS PrivateLink と統合できる AWS のサービス

1.セキュリティ

NAT Gatewayは、インターネット経由で接続しますが、プライベートリンクを利用すると、トラフィックをインターネットに公開することなく、AWS のサービスに接続できます。
そのため、ブルートフォース攻撃やDDos攻撃、その他の脅威に晒される危険が軽減されます。
また、セキュリティグループを関連付けることで、アクセス制御も可能です。

2.料金

料金については、AWS アーキテクチャ ブログを見ますと、エンドポイント(インターフェイスエンドポイント)は、「インターネット ゲートウェイへのトラフィックを回避し、NAT ゲートウェイ、NAT インスタンス、またはファイアウォールの維持に関連するコストが発生するのを回避することで、ネットワーク パスを最適化できます。」との記載があります。
また、NAT Gatewayと「1つ当たりの料金」と「処理データ 1 GB あたりの料金」を比較すると、料金は約6分の1で、AWSのコストを削減できる可能性があります。
NAT Gatewayの料金
エンドポイントの料金

3.管理

管理については、エンドポイントは設定手順が少なく、構成も分かり易いため、個人的には管理し易いと思いました。


以上です。
今回は、インターフェイスエンドポイント(プライベートリンク)についてご紹介しましたが、エンドポイントは他にも「ゲートウェイロードバランサーのエンドポイント」、「ゲートウェイエンドポイント」がありますので、機会があればご紹介したいと思います。