狛ログ

2019年5月1日水曜日

AngularのEventEmitter(Output、emit) で複数の値を送りたい。

5月 01, 2019

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

令和最初のブログはAngularです。

今回は、普段は意識しないけど、「そう言えばどうやるんだ?」的な小ネタです。

Angularでは、親コンポーネントから子コンポートに値を渡す時は「@Input()」を使用します。
イメージ的には変数経由で値を渡す、と言う感じですかね。
逆に子コンポーネントから親コンポートに値を渡す時は「@Output()」を使用します。
こちらは、メソッドに経由で値を渡す、と言う感じです。

この「@Output()」ですが、簡単な使い方として、
親コンポーネント側(hoge-parent.component.ts)で、以下のように記載します。
分かりやすくする為にView(html)はファイルを分けています。(hoge-parent.component.html)

[hoge-parent.component.ts]
@Component({
  selector: 'koma-hoge-parent',
  templateUrl: './hoge-parent.component.html'
})
export class HogeParentComponent {

// (中略)

  onSubmit(childValue: string) {
    console.log(childValue);
  }
}

[hoge-parent.component.html]
<koma-hoge-child
  (formSubmit)="onSubmit($event)">
</koma-hoge-child>

「onSubmit($event)」が子コンポーネントからの指示(formSubmit)を待ち構えている、と言う表現がしっくりきますね。

子コンポーネント側(hoge-child.component.ts)は、以下のように記載します。
[hoge-child.component.ts]
@Component({
  selector: 'koma-hoge-child',
  templateUrl: './hoge-child.component.html'
})
export class HogeChildComponent {
  @Output() formSubmit = new EventEmitter<string>();

// (中略)

  onClickButton(textValue: string) {
    this.formSubmit.emit(textValue);
  }
}

onClickButton のメソッドの中で、textValue を引数に、「emit」を使って、親コンポーネント側の処理を実行します。

と、前置きが長くなったのですが、上記までが「@Output()」の使い方の基本です。

ある時、ふと思ったんですよね。
あれ?これ、複数の引数を送りたい場合どうするんだ?
と。

モデル(クラス)を利用して複数の値を送る場合

モデル(クラス)を利用すれば、引数としては1つですが、複数の値を送る事が実現できます。
[hoge.ts]
export class HogeHogeModel {
  hogeId: string;
  hoge1: string;
  hoge2: number;
}

[hoge-child.component.ts]
@Component({
  selector: 'koma-hoge-child',
  templateUrl: './hoge-child.component.html'
})
export class HogeChildComponent {
  @Output() formSubmit = new EventEmitter<HogeHogeModel>();

// (中略)

  onClickButton(textValue: string) {
   const hogeObj = new HogeHogeModel();
    hogeObj.hogeId = textValue;
    hogeObj.hoge1 = '名前';
    hogeObj.hoge2 = 1234;
    this.formSubmit.emit(hogeObj);
  }
}

親コンポーネント側(hoge-parent.component.ts)は下記のようになります。
※View(hoge-parent.component.html)は変更不要です。
[hoge-parent.component.ts]
@Component({
  selector: 'koma-hoge-parent',
  templateUrl: './hoge-parent.component.html'
})
export class HogeParentComponent {

// (中略)

  onSubmit(childObj: HogeHogeModel) {
    console.log(childObj.hogeId);
    console.log(childObj.hoge1);
    console.log(childObj.hoge2);
  }
}

記載は省略していますが、モデル(クラス)のimportは必須です。

「モデル(クラス)を送れば解決」でも良いんですが、
わざわざモデル(クラス)を作るのもなぁ・・・・と思う事があるかもしれません。
(個人的にはそれでもモデル(クラス)作るべきだと思いますが)

連想配列をその場で定義し、複数の値を送る場合

ちょっと特殊ですが、もう一つの書き方を紹介します。
子コンポーネント側(hoge-child.component.ts)を以下のように記載します。
[hoge-child.component.ts]
@Component({
  selector: 'koma-hoge-child',
  templateUrl: './hoge-child.component.html'
})
export class HogeChildComponent {
  @Output() formSubmit = new EventEmitter<{ hogeId: string; hoge1: string; hoge2: number }>();

// (中略)

  onClickButton(textValue: string) {
    this.formSubmit.emit({
      hogeId: textValue,
      hoge1: '名前';,
      hoge2: 1234
    });
  }
}

まあ、分かってしまえば、「そりゃそうだよな」と言う書き方なのですが。

続いて、親コンポーネント側(hoge-parent.component.ts)は下記のようになります。
※View(hoge-parent.component.html)は変更不要です。
[hoge-parent.component.ts]
@Component({
  selector: 'koma-hoge-parent',
  templateUrl: './hoge-parent.component.html'
})
export class HogeParentComponent {

// (中略)

  onSubmit(childObj: any) {
    console.log(childObj.hogeId);
    console.log(childObj.hoge1);
    console.log(childObj.hoge2);
  }
}

気を付ける事は引数の型を「any」にする事ぐらいですかね。
型を特定出来ない(any)と言う事は、実行時にエラーになる可能性が高いので、やはりオススメしません。

以上です。今回も内容の割には長くなってしまいました・・・・

では、令和も良いAngularライフを!


2019年4月30日火曜日

Node.js + Express + log4jsで、アクセスログとリクエストログを取る。

4月 30, 2019

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

平成最後の投稿です。

弊社では「Node.js + Express」でAPIを作成する事が多々あるのですが、
その際、アクセスログは別レイヤーで出力して、リクエストログは各業務処理内で出力しています。

今回、それを共通化してまとめたいと思います。
各バージョンは以下の通りです。

node 10.13.0
log4js 3.0.6

※それぞれ、ちょっと前のバージョンになるので、参考にされる方はご注意下さい。

log4js のインストール

まずは log4js のインストールです
npm install log4js

Configファイルの作成

ログの出力レベルなどの設定関連は、別ファイルにしたいので、
「[プロジェクトルート]/src/config」に「log4js.config.json」ファイルを作成します。
ファイルの中身は今回は以下のようにしました。
{
  "appenders": {
    "ConsoleLogAppender": {
      "type": "console"
    },
    "SystemLogAppender": {
      "type": "file",
      "filename": "./log/system.log",
      "maxLogSize": 5000000,
      "backups": 3
    },
    "HttpLogAppender": {
      "type": "dateFile",
      "filename": "./log/http.log",
      "pattern": ".yyyy-MM-dd",
      "daysToKeep": 7
    },
    "AccessLogAppender": {
      "type": "dateFile",
      "filename": "./log/access.log",
      "pattern": ".yyyy-MM-dd",
      "daysToKeep": 7
    }
  },
  "categories": {
    "default": {
      "appenders": ["ConsoleLogAppender"],
      "level": "all"
    },
    "system": {
      "appenders": ["SystemLogAppender"],
      "level": "info"
    },
    "http": {
      "appenders": ["HttpLogAppender"],
      "level": "info"
    },
    "access": {
      "appenders": ["AccessLogAppender"],
      "level": "info"
    }
  }
}

各設定とも「filename」でログの保存場所&ファイル名を指定しています。
「type」によってローテーションが違いますが、
「"type": "file"」の場合は、「maxLogSize」で最大ファイルサイズ(byte)を指定し、
それを超えた場合、「backups」に記載している世代分は保存されます。

「"type": "dateFile"」の場合は、「pattern」に記載した形式(今回は「yyyy-MM-dd」)、つまり日毎にローテーションされ、「daysToKeep」に記載している日付分は保存されます。
詳しくは本家のドキュメントを参照ください。

出力ロジックの記載

続いて、app.jsにログ出力について記載していきます。
const log4js = require('log4js');
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
require('dotenv').config();

// (中略)

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(cors(corsOptions));

// health check
app.use('/health', check);

// ログ設定
log4js.configure('./src/config/log4js.config.json');
const systemLogger = log4js.getLogger('system'); 
const httpLogger = log4js.getLogger('http'); 
const accessLogger = log4js.getLogger('access');
app.use(log4js.connectLogger(accessLogger));
app.use((req, res, next) => {
  if (typeof req === 'undefined' || req === null ||
        typeof req.method === 'undefined' || req.method === null ||
        typeof req.header === 'undefined' || req.header === null) {
    next();
    return;
  }
  if (req.method === 'GET' || req.method === 'DELETE') {
    httpLogger.info(req.query);
  } else {
    httpLogger.info(req.body);
  }
  next();
});
systemLogger.info("App start");

// API Version
const apiVersion = '/api/v2.1.0';

// 各業務API
app.use(`${apiVersion}/hoge/auth`, hogeAuth);
app.use(`${apiVersion}/hoge/master/user`, userMaster);

// (後略)

ポイントは、ログ出力設定をhealth checkの後、そして、業務APIの前に記載している事です。
(今回は、health checkからの接続をログに出力したくなかったので)

ただこれは、health checkのURLへの接続を前もって制限出来ている事が前提となります。
health checkのURLには、特定の信頼している接続元からのみ接続出来る事が分かっているので、ログは不要です、と。

では、ログ設定の部分を見ていきます。

まずは、先程作成した外部の設定ファイルを読み込みます。
log4js.configure('./src/config/log4js.config.json');

続いて、各設定を読み込みます。(configファイルの「categories」に記載した名前を使用します。)
const systemLogger = log4js.getLogger('system'); 
const httpLogger = log4js.getLogger('http'); 
const accessLogger = log4js.getLogger('access');

続いて、下記の記載で、Expressへのアクセスをログに出力することが可能になります。
app.use(log4js.connectLogger(accessLogger));

続いて、下記の記載で、接続時のリクエスト値をログに出力することが可能になります。
app.use((req, res, next) => {
  if (typeof req === 'undefined' || req === null ||
        typeof req.method === 'undefined' || req.method === null ||
        typeof req.header === 'undefined' || req.header === null) {
    next();
    return;
  }
  if (req.method === 'GET' || req.method === 'DELETE') {
    httpLogger.info(req.query);
  } else {
    httpLogger.info(req.body);
  }
  next();
});

当APIプロジェクトはRESTになっているので、
GETとDELETEはqueryで、それ以外(POST、PUT)は、bodyで送られてくる、という想定になっています。

※「next()」を忘れないようにして下さい。
これが無いと、そこで処理が終了してしまい、以降の業務API側に値が行き渡りません。


そして最後に
systemLogger.info("App start");
これは、単純にExpressを起動した時に一度だけ出力されます。

これで、Expressを起動するとログが出力されます。

今回は、設定ファイル(log4js.config.json)で「"filename": "./log/xxxx.log",」と記載しているので、 app.jsが格納しているディレクトリにlogディレクトリが作成され、その中に、ログが格納されます。
こんなイメージです。

ちなみに、今回は説明を省きましたが、
業務側で500エラーになった場合も、app.js内で最終的にcatchし、systemLoggerでエラー内容(スタックトレース)をシステムログに出力しています。

以上です。

平成最後の投稿、内容の割に長くなってしまいました。

それでは、また令和でお会いしましょう!


2019年4月26日金曜日

Amazon ConnectとLambdaで架電してみる ①お問い合わせフローの作成

4月 26, 2019

こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。
今回の投稿は、 AmazonConnectとLambdaで任意の番号に架電する方法をご紹介します。









AmazonConnectはコールセンターをメインとして、簡易に作成できるというものなので、
受電する記事をよく見かけますが、架電方法の記事は少なかったので記事にしました。




難しそうに見えますが、簡単で短時間で作成できますので試してみてください。
こちらの4ステップで紹介していきます。

①Amazon Connectでお問い合わせフローの作成 ← 今回はこちら
②IAMでロールを作成
③Lambda関数の作成
④実行!!


①Amazon Connectでお問い合わせフローの作成

AmazonConnect登録後、ダッシュボードまでたどり着くことができたら、下の動画のようにお問い合わせフローを作成してください。



左のバーからもう一度ダッシュボードまで戻り、電話番号の取得、お問い合わせフローの選択をしてください。




意外と簡単に作成できましたね。

次回は②IAMでロールを作成を書いていきます。


2019年4月25日木曜日

ASP.NET MVCでファイルダウンロード後にViewが表示できないときの対処方法。

4月 25, 2019

オフィス狛 技術部のJoeです。

最近、ASP.NET MVCで開発されたWebシステムの改修プロジェクトをお手伝いしたのですが、ASP.NET MVCでの開発が初めてだったので、色々と戸惑いが多かったです。
その中でも、あるエラーについての対処方法をご紹介したいと思います。

今回のプロジェクトでは、ControllerからViewを表示したり、別のアクションを起こす場合は、主に下記のActionResultオブジェクトを使っていました。

① 呼び出し元のViewを表示する

return View();

② 指定したViewを表示する

return View("~/Views/Test/index.cshtml");

③ 指定したControllerのアクションを実行する

return RedirectToAction(MethodName.Index, ControllerName.Test);

ところが、ファイルをダウンロードした後に上記のActionResultオブジェクトを使用すると、Exceptionが発生してしまいました。

[ C#(Response.WriteFileメソッドを使用してサーバ上のExcelファイルをダウンロード) ]
// Excelファイルをダウンロード
var DownloadFile = Server.MapPath("~/Reports/Download.xlsx");
long offset = 0;
long size = new FileInfo(DownloadFile).Length;
Response.Clear();
Response.ContentEncoding = Encoding.GetEncoding("UTF-8");
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";  // これはExcel
Response.AppendHeader("content-disposition", "attachment; filename=" + Server.UrlEncode("ダウンロード.xlsx"));
Response.WriteFile(DownloadFile, offset, size);
Response.End();

// 呼び出し元のViewを表示する(①のパターン)
return View();

[ エラー内容 ]
HTTP ヘッダーの送信後にサーバーでヘッダーを追加できません。

②でも同じエラーになります。

実は今回、CSRF対策としてView側で「Html.AntiForgeryToken」ヘルパーを呼び出していました。
ファイルをダウンロードしているので、「Response.End()」の時点でクライアントにHTTPヘッダーが送信されています。
「return View(…)」でViewを返した際に、View側で「Html.AntiForgeryToken」ヘルパーがCookieを設定しようとして、エラーになってしまいました。

では③「RedirectToAction」を試してみると

[ エラー内容 ]
HTTP ヘッダーを送信後はリダイレクトできません。

こちらはエラーのとおり、リダイレクトができないようです。


【対処方法1】何も返さない
何も返さなければいいということで、以下に修正してExceptionを回避できました。
return new EmptyResult();

ちなみに以下でも同じ動きです。MVCが判断して「return new EmptyResult();」を返してくれます。
return null;

【対処方法2】ダウンロードするファイルを返す
こちらは「return File」でファイルを返します。(「Response.WriteFileメソッド」を使用しません)
なんだかスッキリしてますし、ダウンロード後に何か処理が無ければこちらのほうが良いですね。
[ C# ]
var DownloadFile = Server.MapPath("~/Reports/ManuallyRecord.xlsx");
var contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";  // Excel
return File(DownloadFile, contentType, Server.UrlEncode("ダウンロード.xlsx"));

ActionResultオブジェクトだけでもいろいろなパターンがあって、まだまだ覚えることがたくさんありそうです。


2019年4月24日水曜日

C#(ADO.NET)でIN句にSqlParameterをわたす方法

4月 24, 2019

オフィス狛 技術部のHammarです。

SQLでIN句を使ってデータを取得するというのはよくある取得条件ですが、実際のプログラムで実装する場合には、SQLインジェクション対策といったセキュリティの観点から、IN句の中はプレースホルダを使って動的に値を受け渡したいという場面があると思います。

基本的にはIN句の中を作成し、そのままパラメータとして渡すという方法でいけるのですが、C#(ADO.NET)を使って同様の実装をすると、意図したデータが取得できずにうまくいかずハマってしまいましたので、今回はその対応方法を書きたいと思います。

DB環境としてSQLServerを対象にしています。
まず通常のIN句をそのままパラメータで渡す方法として、下記のように記述します。
// 配列データ
string[] array = new string[3];
array[0] = "name1";
array[1] = "name2";
array[2] = "name3";

// DB接続
string connect = 【DB接続内容】

// 取得SQL
string query =
   "SELECT * FROM TestTable "
       + "WHERE Name IN (@param) ";
// IN句作成
string paramValue = string.Join(",", array);

using (SqlConnection connection = new SqlConnection(connect))
{
   SqlCommand command = new SqlCommand(query, connection);
   // パラメータセット
   command.Parameters.Add("@param", SqlDbType.VarChar).Value = paramValue; 
   connection.Open();
   // SQL実行
   SqlDataReader reader = command.ExecuteReader();
   while (reader.Read())
   {
       Console.WriteLine("\t{0}\t{1}\t{2}", reader[0], reader[1], reader[2]);
   }
   connection.Close();
}

上記を実行すると、結果は何も取得できません。 どうやらSqlParameterでわたす値は1つの値と見なされるなしく、IN句のカンマ区切りで渡した内容も1つの値となるため、検索条件がおかしくなり意図しない結果として返ってくるようです。

これを知らなかったので、いくらIN句の値をいろいろ変えてやってみたんですが、全く値が取れずにかなりハマりました。

結果「SqlParameterでわたす値は1つの値と見なされる」ということなので、IN句の中で渡すパラメータを1つずつ設定してあげないといけないようです。 SQLはこんな感じになります。

string query =
   "SELECT * FROM TestTable "
       + "WHERE Name IN (@param1, @param2, @param3) ";

当然IN句の中は動的に変える必要があるので、@param自体も動的に作成する必要があるということになります。
で、最終的にはこのような感じになりました。

// DB接続
string connect = 【DB接続内容】
// 取得SQL作成
string query =
   "SELECT * FROM TestTable WHERE Name IN ( ";
for (int i = 0; i < array.Count(); i++)
{
   if (i < (array.Count() - 1))
   {
       query += "@param" + i + ", ";
   }
   else
   {
       query += "@param" + i;
   }
}
query += ")";

using (SqlConnection connection = new SqlConnection(connect))
{
   SqlCommand command = new SqlCommand(query, connection);
   string[] paramNames = new string[array.Count()];
   // パラメータ作成
   for (int j = 0; j < array.Count(); j++)
   {
       paramNames[j] = "@param" + j;
       command.Parameters.Add(paramNames[j], SqlDbType.VarChar).Value = array[j];
   }
   connection.Open();
   // SQL実行
   SqlDataReader reader = command.ExecuteReader();
   while (reader.Read())
   {
       Console.WriteLine("\t{0}\t{1}\t{2}", reader[0], reader[1], reader[2]);
   }
   connection.Close();
}

上記のようにSQLとパラメータのIN句部分をそれぞれ動的に生成することで対応します。

結局スマートな書き方というか、そういう書き方ではなく、どちらかというとまあ当然の書き方というか、IN句をループして動的処理を自作しなければいけないので、ちょっときれいな感じではないですが、IN句はよく使われると思うのでご参考になれば。

ちなみにいろいろ調べてみると、LIKE句をうまく使ってやることもできるようで自分も試してみたのですが、なぜか最初の1件だけしか取得できずうまくいかなかったので、今回のようなスタンダードな書き方に落ち着きました。

2019年3月30日土曜日

Storybordで制約を付け直す(Xcode、iOS)

3月 30, 2019

こんにちは、オフィス狛 モバイル開発担当 Aika-yuy です。


viewを動的に消したり、表示したり、大きさを変更したいときなどに制約も修正しなければいけないと思います。

そんな時にコードで修正するのが面倒。。なんてことはないでしょうか。
そこでstorybordで簡単に制約を付け直す方法をご紹介します。
コードでの修正はとても少なく、使いやすいです。

まず、3つのviewを用意し、間を均等に60空ける制約をつけます。

今回は、動的にyellowViewを消す場合をやってみます。

黄色のviewを消してみました。
黄色のviewがなくなっても、赤と青のviewの位置はそのままです。

それでは、変更したい制約をstorybordで作ります。


redViewのbottomとblueViewのtopの間を60空ける制約をつけました。
この時制約同士が、ぶつかってしまうので後からつけた制約を無効にしなければなりません。
写真では、無効にしてあるので、色が薄くなっています。こうなっていればOKです。


上の写真のように、installedのチェックを外すだけです!!

それができたら、viewControllerに接続します。


後は消したいタイミングで .isActiveをtrueにするだけです。

と言いたいところですが、、、、、
制約の変更はスレッドセーフではない為、メインスレッドで実行しないと反映されません。
DispatchQueue.main.async {}で囲みましょう。

























うまくできました!!!

class ViewController: UIViewController {

    @IBOutlet weak var redView: UIView!
    @IBOutlet weak var yellowView: UIView!
    @IBOutlet weak var blueView: UIView!
    @IBOutlet weak var changeConstraint: NSLayoutConstraint!
    @IBOutlet weak var blueViewConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.yellowView.isHidden = true
        
        DispatchQueue.main.async {
            self.blueViewConstraint.isActive = false
            self.changeConstraint.isActive = true
        }
    }
}



2019年3月29日金曜日

Illustratorのワークスペースでの色と書き出した時の色が違う時はカラー設定のせいかもしれません。

3月 29, 2019

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

Illustratorで画像を書き出した時に「あれ?きちんとドキュメントのカラーモードをRGBに設定していたのにIllustrator上での色と書き出した時の色が違う……?」と思った経験がありませんか?
そんな時に見直すべきだなと思うのがカラー設定です。

Illustratorを使っている方ならだいたい知っている基礎的な話なのですが、新規ファイルを作成するとカラー設定が戻ってしまうバグで私自身焦ったので今後似たようなバグがあった時用に記載しておこうと思います。
https://forums.adobe.com/docs/DOC-9568←私の環境で起こった現象はこのバグに近いですがちょっと違うようでした)


Illustratorカラー設定を確認するにはまず、メニューバーの「編集」のカラー設定をクリックして出てくるカラー設定の設定セレクトボックスを「Web ・インターネット用-日本」に変更すればweb用に書き出した際の色で作業できるようになります!簡単!

初期設定である「Adobe Illustrator5.5をエミュート」に設定されてる場合のワークスペースと「Web ・インターネット用-日本」に設定されてる際のワークスペースの色の違いはこんな感じです。
「Adobe Illustrator5.5をエミュート」での見え方はなんだか薄いです。

ちなみにですが、私がカラー設定が「Adobe Illustrator5.5をエミュート」になっていると気づいたのは作業中のワークスペースの色がなんだかおかしいので色の校正を間違って設定したのかと思いメニューを確認したところ、「色の校正」が使えない状態になっており、調べたところカラー設定が「Adobe Illustrator5.5をエミュート」になっていると使えないという情報が出てきたからです。

調べたところ、数ヶ月前から起こっているバグなのに現在もまだ修正されていないようですね……。
もし色がおかしいな?と思ったら「カラー設定」を見直してみてください。