株式会社スマレジの開発部でスマレジのサーバサイドを作っています

情報処理技術者試験は必要?

こんばんは!株式会社スマレジ 、開発部のmasaです。

オミクロン株の亜種が東京都内で市井感染が確認されたりと、出口が見えない状況が続いていますが、 皆様はいかがお過ごしでしょうか?

レジというサービスを作る身としては、今までのような人と人がカウンターを挟んで取引をする日常が戻ってくることを望む一方で、 多様なチェックアウト方式に対応し、どんな時代でも使ってもらえるサービスとして、スマレジ も進化し続けることが大事だなと 日々のお客様からのご要望を見ていると思います。

さて、今日はいつもの個人開発ではなく、社内でここ最近聞かれる話について、僕なりの意見を書いてみたいと思います。

情報処理技術者試験は必要?

最近、この話題を社内でちょいちょい聞かれます。 というのも、masaは実は学生時代に応用情報処理者試験までは合格していて、 他の資格を取っていないエンジニアさんから、この話を伺われることがあるからです。

masaの個人的な意見では、必要な人とそうでない人がいて、多分必要な人の方が多いというのが感想です。 ちなみに、私は前者です。

そもそも情報処理技術者試験って?

このブログ読んでいる人で、知らない人は多分いないと思いますが、論点を整理する意味でも、 一応試験の概要について触れます。

情報処理技術者試験は、独立行政法人IPA情報処理推進機構)が実施している国家試験の一つで、試験の主体は経済産業省になります。

www.jitec.ipa.go.jp

IPA情報処理の促進に関する法律の第四章に規定されている、

プログラムの開発及び利用の促進、情報処理に関する安全性及び信頼性の確保、情報処理システムの高度利用の促進、情報処理サービス業等を営む者に対する助成並びに情報処理に関して必要な知識及び技能の向上に関する業務を行うことにより、情報処理の高度化を推進すること

を目的とする団体になります。

ちなみに、この情報処理の促進に関する法律は昭和四十五年に施行され、今も頻繁に改正が行われています。 (意外に昔からあるんですね・・・)

elaws.e-gov.go.jp

masaIPAのサイトに載っている情報では、試験以外だと契約書の雛形とか、非機能要件のチェックリスト、設計書のテンプレートなんかをお仕事で見たことがあります。(前の会社にいた頃の話ですが)

網羅的に書かれている資料が多く、意外と有用なものも多いので、車内の資料やネットに転がっている他の資料と比較しながら、資料作成をしていた記憶があります。

情報処理技術者試験が有用な人

この試験も上述の「情報処理の高度化を推進」するための目的の試験であり、IPAのサイトには以下の3つの目的が掲げられていて、 それぞれが有用な人の人物像に当てはまります。

1.情報処理技術者に目標を示し、刺激を与えることによって、その技術の向上に資すること。

2.情報処理技術者として備えるべき能力についての水準を示すことにより、学校教育、職業教育、企業内教育等における教育の水準の確保に資すること。

3.情報技術を利用する企業、官庁などが情報処理技術者の採用を行う際に役立つよう客観的な評価の尺度を提供し、これを通じて情報処理技術者の社会的地位の確立を図ること。

1はいわゆる「勉強のマイルストーンにしてね」というところかなと。 ただここでいう「勉強」はプログラミングではないです!

情報処理技術者試験で、プログラミングについて「直接的に」聞かれるのはほとんどないです。 基本情報処理者試験の言語別問題とアルゴリズムくらいかな。

反面、アーキテクチャやネットワーク、データベースなどITを構成する技術については、その粒度を変えて実にいろいろな形で試験します。 また、マネジメントやITサービス業における経営戦略など、組織人向けのものや経営層向けの試験も存在します。

このプログラミング以外の知識って、特に大学や専門学校などでITを学ばないでエンジニアになった人にとってこの試験が唯一基礎を身につける機会になっているんじゃないかなって、周りを見ていると思います。体系的に学ばずにこの業界に来た人にとっては、業務で得た偏った知識を平にする意味では、ありなんじゃないかなと思います。

また、エンジニアでなくても、SaaSビジネスの営業は上流SE的な能力を求められる場合もありますので、そういった方にも提案の精度を上げる意味ではおすすめかもしれません。(実際、ASPのエンジニアが顧客システムと自社システムのアーキテクチャを提案することって、工数や人員の問題で難しい場合が多いです。その商談の規模が大きければ別ですが)

2は、逆に専門的な学習期間でプロパーとして学ばれてきた方や企業内のシニアエンジニアの方が、その能力を学校・職場で評価してもらう意味での利用かなと思います。

3は、いわゆる就職活動用ですね。これは言うまでもなく、持っていないより持っている方が有利になる場合が多いです。 特に公共系の案件を扱うSIer(特にプライムベンダー)などでは、入札段階でその会社のエンジニアの情報処理技術者試験合格者の保有率などが関わるケースもあると、(これも前の会社で)聞いたことがあります。そういった会社に転職を希望される方は、最低でも「応用情報処理技術者試験」までは持っておいた方がいいかなと思います。

ただ、試験合格を要求する会社や自治体に関して言えば、上記の事情で「持っているのは大前提」という感じが多い気がします。なので他の志願者も資格を持っていることが多いです。なので持っているからと言って有利に事を運べるわけではなく、むしろ「受験できる企業が増える」くらいの認識の方がいいと思います。

情報処理技術者試験が無用な人

上記の1~3に該当しない人は不要だと思います。ただ、特に1については「未経験からエンジニア!」みたいな人はほぼ全員当てはまると思うので、人口的には多いんじゃないかなと思います。

masa的に取得して良かったこと

一番感じるのはアーキテクチャや技術的な課題について説明するときに、同じ資格を持っている人だと同じ用語を使って説明ができるので、私が楽ですし、先輩エンジニアの説明を受ける時も、持っている人の説明の方が吸収しやすいです。

あとは、変な話ですが不具合や問い合わせの対応で真価を発揮することが多いです。 開発部で対応する問い合わせの多くは、技術的に込み入った内容になります。 その内容を適切に表現するには、標準的な表現を用いることが大事になってきますし、お客様側も、

  1. 障害の概略
  2. 障害の原因
  3. 短期的および恒久的な対応の概略

この3点について説明を求められる場合がほとんどです。 でもこれって午後試験の与件文を解くのとほとんど同じなんです。

なので、プロダクトエンジニアを目指す方は特に有効な資格なんじゃないかなと思っています。

チケットの見える化にチャレンジしてみる(実践編: pythonの辞書をIPA辞書に変更する)

こんにちは!株式会社スマレジ 、開発部のmasaです。

3連休は皆様いかがお過ごしでしょうか?
masaはこのご時世で出歩けないのもあって、個人開発などに勤しんでおりました笑

さて、今回から実践編で実際に今までのソースを組み合わせて、Redmineチケットの簡単な軽量テキスト解析をやっていきます。
そのうち、今回は推奨辞書の切り替えをやっていきます。

## MeCabIPA辞書

準備編で使用した、janomeを使った形態素解析なのですが実はそのまま利用するには問題があるのです。

masa2019.hatenablog.com

というのも、例えば「車」や「飛行機」といった汎用的な名刺はjanomeで正確に分類できるのですが、
「スマレジ 」といった固有名詞については正しく認識してくれないのです。

これはjanome本体ではなく、形態素解析に使用している辞書に固有名詞が載っていないことに原因があります。

そのためこのままjanomeを利用するなら辞書に用語を追加する作業などが発生します。
長く使うのであれば、janomeの辞書をチューニングした方が精度は上がるのですが、登録する用語も多いので、
今回はIPAが利用推奨している辞書に切り替えて、形態素解析をすることを考えます。


### MeCab

janomeで利用する辞書を切り替えることも追々考えるのですが、このIPA辞書との連携については、同じく以前チラッと触れた
MeCabという形態素解析ツールの方が導入が楽ちんです。

ただ、MeCabのライブラリはjanomeのようなフィルタリングの定義をカスタマイズする機能などはないので、
その辺りは一長一短です。

まずは、MeCabの導入から。下記のサイト様の方法そのままです。

qiita.com

まずはbrewで必要なライブラリを追加。

```sh
$ brew install mecab mecab-ipadic git curl xz
```

で、インストールが終わりましたら、gitからmecab用のipa辞書定義をcloneしてビルドします。

```sh
git clone --depth 1 git@github.com:neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd
./bin/install-mecab-ipadic-neologd -n
```

これで準備はOK。

### 動かしてみる

ざっくりサンプルソース。(名詞を抜いてくる)
「名詞:でひっかけるのちょっと面倒くさいなと思ったら便利なパースの例がありました。

note.com


```python
import MeCab

sample_txt = 'スマレジ は高機能レジだ。'
m = MeCab.Tagger('-Ochasen -d ' + '/usr/local/lib/mecab/dic/mecab-ipadic-neologd')

nouns = [line for line in m.parse(sample_txt).splitlines()
if "名詞" in line.split()[-1]]

for str in nouns:
print(str.split())
```
`/usr/local/lib/mecab/dic/mecab-ipadic-neologd'`はmecabの辞書が置かれているパスです。
また、`-Ochasen`とすることで表示形式を、下記のような形にすることができます。(この時点でこれはstring型)

```
スマレジ スマレジ スマレジ 名詞-固有名詞-組織
は ハ は 助詞-係助詞
高 コウ 高 接頭詞-名詞接続
機能 キノウ 機能 名詞-サ変接続
レジ レジ レジ 名詞-一般
だ ダ だ 助動詞 特殊・ダ 基本形
。 。 。 記号-句点
```

これを`.splitlines()`で行ごとにループを回して、名詞が含まれる行の先頭三つを撮ってくるという感じです。

ちなみに辞書を切り替えないと、

```
スマ 名詞,普通名詞,一般,,,,スマ,すま,スマ,スマ,スマ,スマ,和,"","","","","","",体,スマ,スマ,スマ,スマ,"4,0","C4","",71373981340541440,259657
レジ 名詞,普通名詞,一般,,,,レジ,レジ-register,レジ,レジ,レジ,レジ,外,"","","","","","",体,レジ,レジ,レジ,レジ,"1","C3","",33253088538272256,120974
は 助詞,係助詞,,,,,ハ,は,は,ワ,は,ワ,和,"","","","","","",係助,ハ,ハ,ハ,ハ,"","動詞%F2@0,名詞%F1,形容詞%F2@-1","",8059703733133824,29321
高 接頭辞,,,,,,コウ,高,高,コー,高,コー,漢,"","","","","","",接頭,コウ,コウ,コウ,コウ,"","P2","",3261434989519360,11865
機能 名詞,普通名詞,サ変可能,,,,キノウ,機能,機能,キノー,機能,キノー,漢,"","","","","","",体,キノウ,キノウ,キノウ,キノウ,"1","C1","",2407664210551296,8759
POS 名詞,普通名詞,一般,,,,ポス,POS,POS,ポス,POS,ポス,記号,"","","","","","",体,ポス,ポス,ポス,ポス,"1","C3","",25583994934534656,93074
レジ 名詞,普通名詞,一般,,,,レジ,レジ-register,レジ,レジ,レジ,レジ,外,"","","","","","",体,レジ,レジ,レジ,レジ,"1","C3","",33253088538272256,120974
だ 助動詞,,,,助動詞-ダ,終止形-一般,ダ,だ,だ,ダ,だ,ダ,和,"","","","","","",助動,ダ,ダ,ダ,ダ,"","名詞%F1","",6299110739157675,22916
。 補助記号,句点,,,,,,。,。,,。,,記号,"","","","","","",補助,,,,,"","","",6880571302400,25
```
というようにスマとレジで別れてしまいます。
(なおこれは-OChasenを指定しない場合です。MeCabコマンドラインで動かすとこんな感じの出力になります)

これである程度の固有名詞にも対応できるようになりました。


[5月追記]
以後は、2,3節で説明したコサイン類似度やBM25などのスコア値を、D3.jsに渡して見える化をしていく作業になります。
ただ、量のあるredmineのデータを用意するのがちょっと難しく、このシリーズは一旦ここまでとさせてください。

チケットの見える化にチャレンジしてみる(3-3: 準備編 D3.jsの導入(3:SVGの導入))

こんにちは!株式会社スマレジ 、開発部のmasaです。

2022年になって、弊社でも新入社員さんが続々と入ってきています! スマレジ はまだまだ作りたいもの・やりたいことがたくさんあるので、もちろんそれに応じて人も積極採用中です! ちょっとでも興味を持たれた方は、下記採用ページを是非見てみてください。

corp.smaregi.jp

さて、今回はD3.jsの真骨頂、SVGを扱ったグラフの描画を行います。

グラフにするならやっぱりベクター画像(SVG)

htmlの要素でもグラフ描画が可能なのは前回・前々回を通じて見てきました。 しかし、html要素では拡大縮小などの処理が煩雑だったり、処理に失敗すればぼやけたりします。

そういったダサい描画を避ける手段として、SVG要素を使用する方法があります。 前回から扱っている棒グラフに下から上にニョキっと生えるアニメーションをつけた例を作成したので、 それをベースに説明していきます。

実装例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>D3.jsの練習</title>
</head>
<body>
<div id="chart-area"></div>

<script type="text/javascript" src="js/d3.min.js"></script>
<script type="text/javascript">

    /**
     * divで棒グラフを作る。
     * @param data bindする配列データ
     */
    function drawBarChart(data) {
        const WIDTH_MAX = 300;
        const HEIGHT_MAX = 300;
        const svg = d3.create("svg")
            .style("width", WIDTH_MAX)
            .style("height", HEIGHT_MAX)
            .attr("id", "draw-area");
        const rects = svg.selectAll("rect")
            .data(data)
            .enter()
            .append("rect");
        rects
            .attr("width", 10)
            .attr("height", (d) => d * 10)
            .attr("x", (d, i) => i * 10)
            .attr("y", (d) => 300 - d * 10)
            .attr("fill", "steelblue")
            .attr("class", "bar")
        svg.append("rect")
            .attr("width", WIDTH_MAX)
            .attr("height", HEIGHT_MAX)
            .attr("fill", "white")
            .attr("id", "mask")

        document.getElementById("chart-area").appendChild(svg.node());
    }

    /**
     * 棒グラフをアニメーションさせる。
     */
    function animateBarChart() {
        const transitionStyle = d3.transition()
            .duration(1000)
            .ease(d3.easePoly)
        d3.selectAll("#mask")
            .transition(transitionStyle)
            .attr("y", -300)
    }

    let bindArray = [10,3,6,8,22,15,1];
    drawBarChart(bindArray);
    animateBarChart();
</script>
</body>
</html>

どんな動きになるのかは、コピペしてみてみてください。(d3のライブラリがセットで必要なので注意)

説明

前回と大きく変わったのはdrawBarChartなので、そこを中心に説明していきます。

SVG領域の追加

        const WIDTH_MAX = 300;
        const HEIGHT_MAX = 300;
        const svg = d3.create("svg")
            .style("width", WIDTH_MAX)
            .style("height", HEIGHT_MAX)
            .attr("id", "draw-area");

SVGは画像扱いなので、幅と高さを指定して、html内に埋め込んであげる必要があります。 この辺りは、jQueryなどとあまり感覚に差はないと思います。(使う命令が違っているくらいのもの)

矩形の定義とデータバインド

        const rects = svg.selectAll("rect")
            .data(data)
            .enter()
            .append("rect");
        rects
            .attr("width", 10)
            .attr("height", (d) => d * 10)
            .attr("x", (d, i) => i * 10)
            .attr("y", (d) => 300 - d * 10)
            .attr("fill", "steelblue")
            .attr("class", "bar")

次に棒グラフの「棒」の部分を定義してあげます。 棒の数と高さは、与えられるデータによって変更したいものです。 そういった変更したいデータと矩形を紐づけるときに使うのが、data()メソッドです。 では、データを紐付けたら実際に棒グラフをsvg領域に「追加」しなければいけません。 この「追加」に相当するのがenter()enter()以降はどんな要素(今回ではrect要素)にどんなスタイルで(今回は幅10固定で高さは入力データの各要素を10倍したもので、色はsteelblueでsvg領域に下揃え。)追加するのかを記載しています。

作成した棒グラフを隠すためにマスク用矩形を用意する。

        svg.append("rect")
            .attr("width", WIDTH_MAX)
            .attr("height", HEIGHT_MAX)
            .attr("fill", "white")
            .attr("id", "mask")

svg領域全体を覆うように白の矩形を描画します。後に定義したものはレイヤが上がるので、 初期状態では棒グラフが見えないようになります。

マスク用矩形を上に動かして、アニメーションで出現させる

    /**
     * 棒グラフをアニメーションさせる。
     */
    function animateBarChart() {
        const transitionStyle = d3.transition()
            .duration(1000)
            .ease(d3.easePoly)
        d3.selectAll("#mask")
            .transition(transitionStyle)
            .attr("y", -300)
    }

SVG領域は一番左上の角が(0,0)になるように作成されます。 なので、yに負の値を与えてあげて、transitionさせることで矩形が上に上がってきます。

マスク用の矩形を棒グラフの棒の数だけ作成し、時間差をつけて同じように上方向にtranditionすれば左から順番に 棒がニョキニョキ生えてくるようにも見えますね。

次回からは、いよいよredmineとの連携を考えていきます。

チケットの見える化にチャレンジしてみる(3-2: 準備編 D3.jsの導入(2:アニメーション))

あけましておめでとうございます!(もう1月も半ばですが汗) 株式会社スマレジ 、開発部のmasaです。

皆様は年末年始はいかがでしたか? masaは毎年の通り、実家に帰り親に顔を見せたくらいで、特にこれといったイベントはありませんでした汗。

このブログ関連で言えば、おやすみの間に外部会員連携とプラットフォームアプリを組み合わせて、会員販売だけでなくポイント付与も スプレッドシート でできるようにする拡張を試したので、まとまったら記事としてご紹介します笑。

では、今回はD3.jsのアニメーションのお話です。

前回作った棒グラフをアニメーションさせてみる。

前回のコードをベースに、棒グラフがニョキッと生えるアニメーションを試してみます。

サンプル

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>D3.jsの練習</title>
</head>
<body>
<div id="text-area"></div>
<div id="chart-area"></div>

<script type="text/javascript" src="js/d3.min.js"></script>
<script type="text/javascript">


    /**
     * divで棒グラフを作る。(bindデータのvalidationは単純化のため割愛しています)
     * @param data bindする配列データ
     */
    function drawBarChart(data) {
        const div = d3.create("div")
            .style("font", "10px sans-serif")
            .style("text-align", "right")
            .style("color", "white");
        div.selectAll("div")
            .data(data)
            .join("div")
            .attr("class", "bar")
            .style("background", "steelblue")
            .style("padding", "0px")
            .style("margin", "1px")
            .style("width", "0px");
        document.getElementById("chart-area").appendChild(div.node());
    }

    /**
     * 棒グラフをアニメーションさせる。
     */
    function animateBarChart() {
        const transitionStyle = d3.transition()
            .duration(500)
            .ease(d3.easePoly);
        d3.selectAll(".bar")
            .transition(transitionStyle)
            .style("width", d => `${d * 10}px`)
            .text(d => d)
    }

    let bindArray = [10,3,6,8,22,15,1];
    drawBarChart(bindArray);
    animateBarChart();
</script>
</body>
</html>

drawBarChartの変更点

  • text表示を無くしました。(アニメーション時に表示したいので)
  • widthにbindしたデータを参照せず、0で固定にしました。
  • class指定を追加しました。(.bar)

上記の変更を加えることで、ブラウザ上は棒グラフは表示されませんが、棒グラフ用のdiv DOMには引数で与えられたdataがbindされている状態になります。

animateBarChartについて

前回のコードから追加されたのが、このanimateBarChartです。 要は上述のdrawBarChartで設定していないwidthを設定して、配列の長さを表示してあげればOKなわけです。 ただ、それだけだと「どのようにアニメーションさせるか」の情報がないですよね。 その部分に当たるのが↓の部分。

        const transitionStyle = d3.transition()
            .duration(500)
            .ease(d3.easePoly);

durationがアニメーションさせる長さ(ms指定)でeaseがアニメーションの方式です。 アニメーション方式については、d3クラス内のメンバ定数でその方式を指定できます。(d3.easePolyはニョキッと二次関数的な緩急をつけて生えてくる)

で、このtransitionStyleと合わせて、下記のように遷移後のデータをbindします。

        d3.selectAll(".bar") // drawBarChartで付与したクラス
            .transition(transitionStyle)
            .style("width", d => `${d * 10}px`)
            .text(d => d)

最後の二行については、前回のブログのdrawBarChartで記載されていたもの、そのままですね。(前回のブログは↓)

masa2019.hatenablog.com

次回は今はdiv領域を使用した表示をしていましたが、今度はSVGを利用して描画してみます。 (申し訳ないです。今回盛り込む予定だったんですが、アニメーションの説明を先にした方が分かりやすくなりそうだったので、構成を変更しました汗)

チケットの見える化にチャレンジしてみる(3-1: 準備編 D3.jsの導入(1))

こんばんは!株式会社スマレジ 、開発部のmasaです。

晦日ですねー。masaも先ほど年越し蕎麦を食べたところです。 年の瀬になるとやっぱり一年の反省が頭の中を巡りますね・・・(一年の振り返りは前回の記事で書きましたけど汗)

さて、今回からはのびのびになっていた、D3.jsについて取り扱っていきます。

D3.jsって?

d3js.org

D3.jsは入力されたデータ(Data)を元にDOMを操作(Driven)しブラウザ上に様々な情報をグラフィカルに 表示(Document)するJSのライブラリです。

DOM操作系のライブラリというと、jQueryなんかもそうですし、実際D3.jsはjQueryの代わりに使用することもできます。 ただ、jQueryと違う点として、SVGのDOM要素も手軽に操作することができるところがあります。

注意点として、jQueryと同じDOM操作系のライブラリになるので、仮想DOMを扱うVueやReactと合わせて使用する場合は、 DOMの状態に注意しながら利用する必要があります。

まずは文字を表示させてみる。

というわけで実際に触っていきましょう。まずは背景を黒、文字色を白にしたspan 属性を作成して、 表示させてみます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>D3.jsの練習</title>
</head>
<body>
<div id="text-area"></div>

<script type="text/javascript" src="js/d3.min.js"></script>
<script type="text/javascript">
    /**
     * div領域(#text-area)にspanで引数の文字列を表示する。
     * 背景は黒、文字色は白。
     */
    function drawSpan(message) {
        const span = d3.create("span")
            .style("color", "white")
            .style("background-color", "black")
            .html(message)
            .node();
        document.getElementById("text-area").appendChild(span);
    }

    drawSpan("Hello D3.js!");
</script>
</body>
</html>

表示してみると、こんな感じです。

f:id:masa2019:20211231222021p:plain
spanの表示

drawSpan()関数が実処理です。ざっくり言うと、d3.~~~の部分で作成する属性、そしてそのプロパティを指定して最後の.node()でDOM化して変数に格納、作成したDOMをappendChild()で既存のdiv要素に埋め込んでいるんですね。

棒グラフを表示させてみる。

次は棒グラフの表示です。ここでは、整数値の入った配列を引数に、その整数をもとに棒グラフを書く処理を作成します。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>D3.jsの練習</title>
</head>
<body>
<div id="chart-area"></div>

<script type="text/javascript" src="js/d3.min.js"></script>
<script type="text/javascript">

    /**
     * div領域(#text-area)にspanで引数の文字列を表示する。
     * 背景は黒、文字色は白。
     */
    function drawBarChart(data) {
        const div = d3.create("div")
            .style("font", "10px sans-serif")
            .style("text-align", "right")
            .style("color", "white");

        div.selectAll("div")
            .data(data)
            .join("div")
            .style("background", "steelblue")
            .style("padding", "3px")
            .style("margin", "1px")
            .style("width", d => `${d * 10}px`)
            .text(d => d);

        document.getElementById("chart-area").appendChild(div.node());
    }

    drawBarChart([10,3,6,8,22,15,1]);
</script>
</body>
</html>

drawBarChart([10,3,6,8,22,15,1]);で指定している引数をベースに、下記のようなグラフが表示されます。

f:id:masa2019:20211231222519p:plain
グラフの例

注目するのは、下記の部分です。

        div.selectAll("div")
            .data(data)  // <-- ここで配列のデータを関連づけて
            .join("div") // <-- 関連づけたデータをどの属性に紐づけるのかを指定
            .style("background", "steelblue") 
            .style("padding", "3px")
            .style("margin", "1px")
            .style("width", d => `${d * 10}px`) // divの横幅を取り込んだdataの値に応じて変更
            .text(d => d); 

data()メソッドで数値情報を読み込み、スタイル指定の中で指定した数値を読み込むことで、div要素の幅を変更しています。

次回はSVG要素を使うケースと、transitionについてご紹介できればと思います。

今年一年を振り返って

こんばんは!株式会社スマレジ 、開発部のmasaです。 投稿が空いてしまい申し訳ないです。。。

実は空いてしまった言い訳ではないのですが、このブログの他に、↓のようなものを弊社で始めまして、そちらの準備にかまけておりました笑。

note.com

そう、スマレジ でもついにアドベントカレンダーをやることになりましたー! masaも執筆者の一人として参加させていただいていますので、こちらのブログをご覧の方はぜひぜひアドベントカレンダーもチェックお願いいたします!

本日は振り返り回です。

今年一年を振り返って。

全体を通して、今まで以上に「学び」が多い一年でした。

基本設計

去年までは、プログラミングやスマレジ の仕様把握を振ってくる案件をこなしながら身につけていくことが主だったのですが、 在籍期間が長くなってきたのもあって、仕様を作る側(つまり基本設計レイヤー)になる機会も増えてきました。 これまでも、そこそこの規模の設計〜実装を一人でやる機会を与えていただいてもらっていたのですが、今年は自分の設計ベースで、 別の人が実装をするというのも非常に多かったです。(もちろんその逆も)

実装を他人に委ねることで、自分の設計面で抜けやすい部分や得意な部分が明確になるので、 設計上の弱点を補うことも、公立的にできた年だったのかな、といまになって思います。 この辺りは、うちのプロマネさんに感謝ですね。

AWS

正直身に付けた、というよりは体当たりで学んだ感が強いのがAWSでした。これまで、LambdaとAPI gatewayくらいだったのですが、 ハイパフォーマンスなAPIや処理性能の向上を目指す中で、AWS のテクニカルアーキテクトの方から、いろいろなサービスを教えていただく機会に恵まれました。結果的に、

この辺りのサービスを使ってサービスを作ることができました。 SNSやSQSは機能的にシンプルなので、習得も早かったんですが、Beanstalkはインフラチームと頭を捻り倒しながらどうにか運用まで漕ぎ着けたって感じでした汗。インフラチームのリーダーさんには感謝しかないです・・・!

お問い合わせ対応

お客様の要望の精査、そしてそれを機能に落とし込むところもスマレジのエンジニアは関わるのですが、 去年までは、自分が主体となって機能までの落とし込みを行っていたのですが、今年からは新しくJOINしてくれた方に、 お伝えしていくことがメインでした。

高機能クラウドレジという性質上、仕様がかなり大きいサービスなので、周りのサポートなしに問い合わせを対応していくのはなかなか辛いですし、僕自身、苦労したことはできる限り苦労させずに、だけど身につけてもらう方向で、動くことがある程度はできたかなと思いました。 ただ、それはJOINしてくれたエンジニアさんたちが優秀だったことも大きいので、ただただ感謝しかないですね。

最後に

こうして、書いていて感じたのが、去年よりも感謝する機会が増えたなと、改めて感じました。 やっぱり年々お仕事の中で関わる人の幅が増えていって、その中で、助けることもあったし、助けられることも多かったなと。 来年は来年で、新たな出会いも受け入れつつ、今までのつながりを大事にしながらお仕事をしていければなと思いました。

スマレジの外部会員連携をAWS Lambda + Google スプレッドシート でサーバレスに試してみる。

こんにちは!株式会社スマレジ 、開発部のmasaです。

今回は、D3.jsの触りの部分をやる予定だったんですが、ちょっと予定を変更して、 スマレジ の外部会員連携について、ご紹介したいと思います。 D3.jsの記事は次回 or 2回後くらいから始める予定です。

変更の理由

この機能、最近よくお問い合わせをいただくので、「やってみた記事」の一つでもあれば、 もう少しとっかかりやすくなるかなー、というだけです。深い意味はないです笑

外部会員連携とは

ざっくり言うと、スマレジ の会員マスタを使わない(スマレジ に会員情報を持たない)で、 外部の会員データを会員マスタとして扱う機能になります。

f:id:masa2019:20211123164204p:plain
外部会員連携のイメージ

外部の会員マスタを参照する方式として、APIを利用する方式が取られています。 このAPIの仕様(スマレジ 側から、外部サービスにどんなリクエストが飛んできて、どう返せばいいのか)については、 スマレジ 管理画面にログイン後、下記のリンクを辿っていくか、[設定]->[システム連携]->[外部会員連携]をクリックすると、画像のようなページが開かれるので、そこから仕様書をダウンロードしてみてください。(サンドボックスをご利用の開発者様は下記リンクではなく、[設定]->[システム連携]->[外部会員連携]をクリックして、ダウンロードをお願いします。)

www1.smaregi.jp

f:id:masa2019:20211123164545p:plain
仕様書のリンク

簡単に外部会員連携を試してみる。

連携イメージは分かったので、実際に試してみたい!だけど、今のCRMにつなげるのはちょっと不安 or 個人情報保護ポリシー的にNGで試せない。。。なんてことはあるんじゃないかなーと思います。

というわけで、ダミーデータをGoogle スプレッドシート で作成して、それを会員マスタに見立てて連携できるように作ってみます。

連携イメージは下記のような感じです。

f:id:masa2019:20211123165316p:plain
連携イメージ

今回はAWS Lambdaを利用しますが、GCPのCloud Functionsでも良いと思います。

「え、GASのエンドポイントを直接連携先に指定するんじゃダメなの?」と思われたかもしれません。 GASのdoGet()などで取得するデータは、一度302でリダイレクト情報が返され、リダイレクト先で結果を取得する仕組みになっており、 現状、スマレジ の外部会員連携は302リダイレクト方式には対応していないため、間に1レイヤー挟んでリダイレクト先からデータを取ってくることをしないといけないんです。そのためのLambdaというわけです。

Lambdaのソース

上述の、リダイレクト先から取ってくる、という部分は下記の記事を参考にさせていただきました。

blog.ikappio.com

const https = require('https');
const querystring = require('querystring');

const retrieveResourse = async (postData) => {
    
    let retryCount = 1;
    const MAX_RETRY = 5;
    let response = null;
    let queryParams = "";
    if (postData.length > 0) {
        queryParams = "?req=" + postData;
    }
    let url = "https://script.google.com/macros/s/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/exec" + queryParams; // GCP側のエンドポイントを設定してください。
    while(true) {
        if(retryCount > MAX_RETRY) {
            // 5回やってダメなら諦める
            response = { success: false, reason: "Retry Count Exceeded"};
            break;
        }

        // カウントの更新
        retryCount++;

        const promise = new Promise((resolve, reject) => {
            https.get(url, (res) => {
                const { statusCode } = res;
                console.log(statusCode);
                if(statusCode === 302) {
                    // リダイレクトするのでもう一度ループする。
                    resolve({
                        statusCode,
                        url: res.headers.location,  // リダイレクト先のURL
                    });
                    return;
                }
                if(statusCode === 200) {
                    // データを受け取り終了
                    let data = "";
                    res.setEncoding("utf8");
                    res.on("data", (chunk) => {
                        console.log(chunk);
                        data += chunk;
                    });
                    res.on("end", () => {
                        resolve({
                            statusCode,
                            data,
                        });
                        return;
                    });
                    return;
                }
                // 他のステータスコードは想定外
                resolve({
                    statusCode,
                });
            });
        });

        // 上のpromiseの終了を待つ。
        const result = await promise.catch((reason) => {
            // レスポンス以外のエラー(get自体に失敗など)
            return { statusCode: 0, reason }
        });

        if(result.statusCode === 302) {
            // もう1回ループする必要あり
            url = result.url;
            continue;
        }
        if(result.statusCode === 200) {
            // 取得できた
            response = {
                success: true,
                data: result.data,
            };
            break;
        }
        // エラーハンドリングする
        console.log("不明なエラー");
        // 一応リトライ
        continue;
    }
    return response;
}

exports.handler =  async function(event, context){
    console.log(event["body-json"])
    const requestBodyString = event["body-json"]

    const res =  await retrieveResourse(requestBodyString)
    console.log(res.data)
    if (res !== null && res.success) {
        console.log(res.data)
        return JSON.parse(res.data)
    } else {
        console.log("取得失敗:", res.reason)
    }
};

GASのソース

今回は、手軽に試せるContainer Bound Scriptで作成しています。

/**
 * メイン処理(GET)
 */
function doGet(e) {
  console.log(e);
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("会員マスタ");
  const params = e.parameter.req;
  let resData = "";
if (params.match("searchString")) {
  let tmpArray = params.split('=');
  resData = doSearch(tmpArray[1]);
} else if (params.match("customerCode")) {
  let tmpQueryArray = params.split('&');
  for (let i = 0; i < tmpQueryArray.length; i++) {
    if (!tmpQueryArray[i].match("customerCode")) {
      continue;
    }
    let tmpArray = tmpQueryArray[i].split('=');
    resData = doSearchDetail(tmpArray[1]);
    break;
  }
} else {
  sheet.getRange("B3").setValue("不正");  
}

  let responce = ContentService.createTextOutput();
  responce.setMimeType(ContentService.MimeType.JSON);
  responce.setContent(JSON.stringify(resData));
  return responce;
}

/**
 * 会員検索処理
 * @var string searchString 検索文字列
 * @return object[] 会員一覧
 */
function doSearch(searchString, division) {
  const ss = SpreadsheetApp.getActiveSpreadsheet()
  const sheet = ss.getSheetByName("会員マスタ");
  const customerList = sheet.getDataRange().getValues();  
  const customers = [];
  let currentCustomer = [];
  let currentCustomerName = "";
  for (let i = 0; i < customerList.length; i++) {
    if (i < 1) {
      continue;
    }
    currentCustomer = customerList[i];
    currentCustomerName = currentCustomer[4] + currentCustomer[5];
    if (!currentCustomerName.match(searchString)) {
      continue;
    }
    customers.push(
      {
        customerId: currentCustomer[0],
        customerCode: currentCustomer[1],
        lastName: currentCustomer[4],
        firstName: currentCustomer[5],
        status: currentCustomer[40]
      }
    );
  }
  let res = [];
  if (customers.length > 0) {
    res = {
      "result": {
          "count": customers.length,
          "customers": customers
      }
    };
  } else {
    res = {
      "result": {},
      "error": {
        "message": "検索条件に合致する会員は存在しませんでした。"
      }
    }
  }
  return res;
}

/**
 * 会員取得処理
 * @var string customerCode 会員コード
 * @return object 会員取得結果
 */
function doSearchDetail(customerCode) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const customerMaster = ss.getSheetByName("会員マスタ");
  const customerList = customerMaster.getDataRange().getValues();  
  const customerAlignmentMaster = ss.getSheetByName("連携情報マスタ");
  const customerAlignmentList = customerAlignmentMaster.getDataRange().getValues();  
  let customer = {};
  let currentCustomer = [];
  let currentCustomerCode = "";
  for (let i = 0; i < customerList.length; i++) {
    if (i < 1) {
      continue;
    }
    currentCustomer = customerList[i];
    currentCustomerCode = currentCustomer[1];
    if (!currentCustomerCode.match(customerCode)) {
      continue;
    }   
    for (let j = 1; j < customerAlignmentList.length; j++) {
      if (customerAlignmentList[j][2] !== 1) {
        continue;
      }
      if (currentCustomer[j - 1].length < 1) {
        continue;
      }
      customer[customerAlignmentList[j][1]] = currentCustomer[j - 1];
    }
  }
  let res = [];
  if (customer !== "") {
    res = {
      "result": customer
    };
  } else {
    res = {
      "result": [],
      "error": {
        "message": "指定の会員コードを持つ会員は存在しませんでした。"
      }
    }
  }
  return res;
}

また、取り込むスプレッドシートの構造は下記のようなものを想定しています。 customerの配列の添字は、下記の順番をベースにしています。

会員マスタ

会員ID 会員コード 会員番号 所属店舗 フリガナ(姓) フリガナ(名) アルファベット氏名 国籍 旅券番号 郵便番号 住所 電話番号 FAX番号 携帯電話番号 メールアドレス メールアドレス2 メールアドレス3 性別(0:不明, 1:男, 2:女) 生年月日 会員ランクコード 会員ランク名 会社名 部署名 役職 社員ランクコード 社員ランク名 ポイント付与単位(金額) ポイント付与単位(ポイント) ポイント ポイント期限 マイル 最終来店日時 入会日 退会日 案内メール受取許可フラグ (0:拒否, 1:許可) 備考 備考2 PINコード 会員状態区分 (0:利用可, 1:利用停止, 2:紛失, 3:退会, 4:名寄せ)
1 test001 1 テスト ユーザ テスト ユーザ USER TEST JPN 4000014 山梨県甲府市府中町 412345678 412349876 9012345678 test@test.co.jp 0 1980/01/01 1 ブロンズランク 株式会社須磨商事 レジ事業部 一般 100 2 0 2021/11/21 1 テストユーザです。 テストユーザです。(備考2) 20211121001 0
2 test002 2 hoge fuga ホゲ フガ HOGE HUGA JPN 4000014 山梨県甲府市府中町 412345678 412349876 9012345678 test@test.co.jp 0 1980/01/01 1 ブロンズランク 株式会社須磨商事 レジ事業部 一般 100 2 0 2021/11/21 1 テストユーザです。 テストユーザです。(備考2) 20211121001 0

連携情報マスタ(連携する項目を選ぶ)

項目名 物理名 連携区分(0:連携しない,1:連携する)
会員ID customerId 1
会員コード customerCode 1
会員番号 customerNo 1
所属店舗 storeId 1
lastName 1
firstName 1
フリガナ(姓) lastKana 1
フリガナ(名) firstKana 1
アルファベット氏名 非対応 0
国籍 非対応 0
旅券番号 非対応 0
郵便番号 postalCode 1
住所 address 1
電話番号 phoneNumber 1
FAX番号 faxNumber 1
携帯電話番号 mobileNumber 1
メールアドレス mailAddress 1
メールアドレス2 非対応 0
メールアドレス3 非対応 0
性別(0:不明, 1:男, 2:女) sex 1
生年月日 birthDate 1
会員ランクコード rank 1
会員ランク名 非対応 0
会社名 非対応 0
部署名 非対応 0
役職 非対応 0
社員ランクコード staffRank 1
社員ランク名 非対応 0
ポイント付与単位(金額) pointGivingUnitPrice 1
ポイント付与単位(ポイント) pointGivingUnit 1
ポイント point 1
ポイント期限 pointExpireDate 1
マイル mile 1
最終来店日時 lastComeDateTime 1
入会日 entryDate 1
退会日 leaveDate 1
案内メール受取許可フラグ (0:拒否, 1:許可) 非対応 0
備考 note 1
備考2 note2 1
PINコード pinCode 1
会員状態区分 (0:利用可, 1:利用停止, 2:紛失, 3:退会, 4:名寄せ) status 1

連携区分については、非対応な項目以外は連携する設定にしています。 ご自身の環境に合わせて、設定値の変更をお願いします。

API Gatewayについては、CDK or Terraterm化できてないので割愛させてください汗。ダイジェストで載せますと、

  • 統合タイプはLambda関数
  • マッピングテンプレートにapplication/x-www-form-urlencodedを指定して、「メソッドリクエストのパススルー」を設定
  • エラーレスポンスについては、仕様書に従って、400エラーの設定をお願いします。

という感じです。(コード化できたら、追記します)

また、今回は軽くお試ししてみるのが主題なので、シートが壊れてた時や、細かいエラーチェックは入れていませんのでご注意ください。 (入れるとコード量増えてわかりづらくなるので)

12/17追記

GASのコードで、一部不備があったので修正しています。