");background-repeat:no-repeat;-webkit-background-size:30px 24px;-moz-background-size:30px 24px;background-size:30px 24px;background-position:center center;border:none;margin:0;padding:0;width:60px;height:60px;cursor:pointer;-ms-flex-item-align:center;-ms-grid-row-align:center;-webkit-align-self:center;align-self:center;-ms-flex-item-align:center}.tab{position:relative;color:#424242;padding:6px;margin:0 4px;text-decoration:none;text-transform:uppercase;font-weight:400;-webkit-transition:.1s ease color;-webkit-transition:.1s ease color;-moz-transition:.1s ease color;-o-transition:.1s ease color;-ms-transition:.1s ease color;transition:.1s ease color}amp-sidebar{width:200px}amp-sidebar ul{padding:10px 0}amp-sidebar li a{display:block;padding:10px 20px}.title-cover{background-color:#4f7086;color:#fff;padding:30px 15px 20px 15px;text-align:center}.title-cover a{color:#fff}.title-cover a:hover{color:#fff}.meta-content{background-color:#e9e9e9;padding:20px 0}.meta-wrapper{margin:20px 20px;padding:20px 20px;background-color:#fff}#footer .copyright-tool,#footer .homeLink{margin:20px}.copyright-tool{color:#fff;font-size:.5em}.social-share{margin:10px 0 0 0;display:-webkit-box;display:-moz-box;display:-webkit-flex;display:-ms-flexbox;display:box;display:flex;-webkit-box-pack:center;-moz-box-pack:center;-o-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center}.social-share amp-social-share{margin:5px}#totop{position:fixed;bottom:20px;right:20px;font-size:1.2em;z-index:995;line-height:1.8em}#totop a{background:rgba(152,209,245,.6);text-decoration:none;color:#fff;padding:0;text-align:center;display:block;width:1.2em;height:2.2em;line-height:2em;letter-spacing:0;position:relative;display:inline-block;padding:0 0 0 16px;vertical-align:middle;text-decoration:none;font-size:15px}#totop a:after,#totop a:before{position:absolute;top:0;bottom:0;left:0;margin:auto;content:"";vertical-align:middle}#totop a:before{left:12px;width:8px;height:8px;border-top:2px solid #fff;border-right:2px solid #fff;-webkit-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg)}#totop a:hover{text-decoration:none;background:#4f7086;color:#fff;line-height:2.3em}.site-author-desc{font-size:1.2em;line-height:2em;margin-top:1em}.site-author-info{background-color:#fff;margin:20px}.gravatar-area{-webkit-border-radius:50%;border-radius:50%}.article-categories,.article-categories-list,.article-tag,.article-tag-list{padding:0;margin:0}.article-categories{margin-bottom:1.6em}.article-categories-list li,.article-tag-list li{list-style:none;display:inline-block;padding:0;margin:.2em 0}.article-categories-list li a,.article-tag-list li a{padding:.5em .6em;margin:.4em 0;background-color:#f5f2f0;color:#3a484b}.article-categories-list li a:before,.article-tag-list li a:before{content:"#"}.article-categories-list li a:hover,.article-tag-list li a:hover{color:#fff;background-color:#648aa4}.homeLink,.htmlPageLink{font-weight:700;-moz-transition:.7s;-ms-transition:.7s;-o-transition:.7s;-webkit-transition:.7s;-webkit-transition:.7s;-moz-transition:.7s;-o-transition:.7s;-ms-transition:.7s;transition:.7s;padding:.4em 0}.homeLink a,.htmlPageLink a{color:#fff;display:block;padding:1em;text-align:center}.homeLink:hover,.htmlPageLink:hover{-moz-transition:.3s;-ms-transition:.3s;-o-transition:.3s;-webkit-transition:.3s;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s}.homeLink{font-size:120%;background:#7496ad}.homeLink:hover{background:#94afc0}.htmlPageLink{font-size:110%;background:#7496ad}.htmlPageLink:hover{background:#94afc0}.ad-footer,.googleAdPost,amp-instagram,amp-twitter,amp-video,amp-vimeo,amp-youtube{margin:2.8em 0}.sns-link{margin:1em 0}.sns-link a{font-size:2.6em;padding:.4em .1em;margin:0 .1em;color:#d0d0d0}.sns-link a:hover{color:#4f7086}.applink-wrapper .applink-col{width:100%}.applink-wrapper .applink-col a{display:block;width:60%;margin:0 auto}.applink-wrapper,.babylink-box{border:1px solid #f8981d;position:relative}.applink-wrapper .applink,.applink-wrapper .babylink-wrapper,.babylink-box .applink,.babylink-box .babylink-wrapper{margin:1em 1em 1em 1em}.applink-wrapper .app-image,.applink-wrapper .babylink-image,.babylink-box .app-image,.babylink-box .babylink-image{margin:0 5% 0 0;display:inline-block;vertical-align:middle;width:40%}.applink-wrapper .app-title,.applink-wrapper .babylink-info,.babylink-box .app-title,.babylink-box .babylink-info{margin:0;display:inline-block;vertical-align:middle;width:55%}.applink-wrapper .app-info,.applink-wrapper .babylink-description,.babylink-box .app-info,.babylink-box .babylink-description{margin:1em 1.2em .5em 1.2em}.applink-wrapper .babylink-description,.babylink-box .babylink-description{line-height:1.8em}.applink-wrapper .babylink-manufacturer,.babylink-box .babylink-manufacturer{margin:.8em 0;line-height:1.8em}.applink-wrapper .babylink-title,.babylink-box .babylink-title{font-size:1em;margin:.8em 0;line-height:1.8em}.applink-wrapper .babylink-title a,.babylink-box .babylink-title a{display:block;padding:1em 0}.applink-wrapper .aicon,.babylink-box .aicon{position:absolute;display:none}.applink-wrapper .babylink-amazonBtn,.babylink-box .babylink-amazonBtn{margin:1.4em 0 1em 0;height:4em}.applink-wrapper .babylink-amazonBtn a.amb,.babylink-box .babylink-amazonBtn a.amb{-webkit-transition:.1s;-moz-transition:.1s;-o-transition:.1s;-webkit-transition:.1s;-moz-transition:.1s;-o-transition:.1s;-ms-transition:.1s;transition:.1s;font-size:1em;height:3.5em;line-height:3.5em;text-align:center;color:#fff;width:85%;margin:0 auto;padding:0;display:block;background-color:#f8981d;border-top:1px solid #fff;border-bottom:4px solid #c27006;border-radius:4px}.applink-wrapper .babylink-amazonBtn a.amb:hover,.babylink-box .babylink-amazonBtn a.amb:hover{color:#fff;border-top:4px solid #fff;border-bottom:1px solid #c27006}.applink-wrapper .amazon-icon:before,.babylink-box .amazon-icon:before{content:"\f270";font-family:fontawesome;color:#fff}.entry-content p.openMap{text-align:center}.entry-content .attention{border:1px solid #ce8a8a}.entry-content .attention h5.label,.entry-content .attention h6.label{color:#ce8a8a}.entry-content .r-download{border:1px solid #88a3c1}.entry-content .r-download h5.label,.entry-content .r-download h6.label{color:#88a3c1}.entry-content .r-memo{border:1px solid #88b9c1}.entry-content .r-memo h5.label,.entry-content .r-memo h6.label{color:#88b9c1}.entry-content .attention,.entry-content .babylink-box,.entry-content .r-download,.entry-content .r-memo{margin:2.2em 0;padding:0;border-radius:2px}.entry-content .attention h5.label,.entry-content .attention h6.label,.entry-content .babylink-box h5.label,.entry-content .babylink-box h6.label,.entry-content .r-download h5.label,.entry-content .r-download h6.label,.entry-content .r-memo h5.label,.entry-content .r-memo h6.label{background:0 0;background-color:none;border:none;border-bottom:none;border-left:none;border-right:none;border-top:none;margin:1.2em 1.5em;padding:0}.entry-content .attention h6.label,.entry-content .r-download h6.label,.entry-content .r-memo h6.label{font-size:1em}.entry-content .attention p.label,.entry-content .r-download p.label,.entry-content .r-memo p.label{font-size:.85em;margin:1.5em;padding:0}.entry-content .attention .amp-img-wrapper,.entry-content .attention table,.entry-content .r-download .amp-img-wrapper,.entry-content .r-download table,.entry-content .r-memo .amp-img-wrapper,.entry-content .r-memo table{margin:1.8em 1.8em 0 1.8em}.entry-content .attention dl,.entry-content .attention ol,.entry-content .attention ul{margin:1em 1em 1em 2em}.entry-content .r-download dl,.entry-content .r-download ol,.entry-content .r-download ul{margin:1em 1em 1em 2em}.entry-content .r-memo dl,.entry-content .r-memo ol,.entry-content .r-memo ul{margin:1em 1em 1em 2em}.entry-content .attention h5.label:before,.entry-content .attention h6.label:before{content:"\f071"}.entry-content .r-download h5.label:before,.entry-content .r-download h6.label:before{content:"\f019"}.entry-content .r-memo h5.label:before,.entry-content .r-memo h6.label:before{content:"\f0f6"}.entry-content .attention h5.label,.entry-content .attention h6.label,.entry-content .r-download h5.label,.entry-content .r-download h6.label,.entry-content .r-memo h5.label,.entry-content .r-memo h6.label{margin:1.8em 1.8em 0}.entry-content .attention h5.label:before,.entry-content .attention h6.label:before,.entry-content .r-download h5.label:before,.entry-content .r-download h6.label:before,.entry-content .r-memo h5.label:before,.entry-content .r-memo h6.label:before{margin:0 .5em 0 0;font-family:fontawesome}.entry-content .attention ol li,.entry-content .attention ul li{font-size:.85em}.entry-content .r-download ol li,.entry-content .r-download ul li{font-size:.85em}.entry-content .r-memo ol li,.entry-content .r-memo ul li{font-size:.85em}.entry-content .attention ul li:after,.entry-content .r-download ul li:after,.entry-content .r-memo ul li:after{top:.85em}.entry-content .attention p.label,.entry-content .r-download p.label,.entry-content .r-memo p.label{font-size:.85em;margin:1.8em}.amSimple{margin:1.8em 0;padding:1em 1.8em;position:relative;background-color:#f6f6f4}.amSimple p{margin:1em 0}amp-img,img{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-drag:none;-khtml-user-drag:none}.imgCaption{text-align:center}.imgCaption span{font-size:.5em;color:#b6b6b6}.bcg{width:90%;margin:2.8em auto}.bcg-x-and-cell{width:100%;margin:0;position:relative;height:100%}.bcg-x-label{width:94%;margin:5px 0 0 6%;text-align:center}.bcg-y-label{-webkit-writing-mode:vertical-rl;-ms-writing-mode:tb-rl;writing-mode:vertical-rl;text-orientation:sideways-right;width:6%;display:inline-block}.bcg-y-label-inner{position:absolute;top:50%;left:1%;display:inline-block;-webkit-transform:translate(-50%,-50%) rotate(180deg);-moz-transform:translate(-50%,-50%) rotate(180deg);-o-transform:translate(-50%,-50%) rotate(180deg);-ms-transform:translate(-50%,-50%) rotate(180deg);transform:translate(-50%,-50%) rotate(180deg);text-align:center;height:100%}.label-circle{display:inline-block;padding:.2em;border-radius:50%;width:1.6em;height:1.6em;line-height:1.6em;font-size:.8em}.label-week{background-color:#758f94;color:#fff}.label-strong{background-color:#3c4b4e;color:#fff}.label-desc{margin:.8em;display:inline-block}.bcg-table-wrapper{margin:0;padding:0;width:94%;display:inline-block}.bcg-table{border-collapse:collapse}.bcg-table .bcg-td{width:50%;text-align:center;height:100px;border:1px solid #fff;margin:0;padding:0;font-size:1.2em}.bcg-table .def{background-color:#758f94;color:#fff}.bcg-table .em{background-color:#55696e;color:#fff}.bcg-table .strong{background-color:#3c4b4e;color:#fff}@media (min-width:800px){.article-nav,.entry-content,.entry-header,.entry-title{width:780px;margin:0 auto}.nav-container{width:800px;margin:0 auto}.meta-wrapper{width:760px;margin:20px auto}.site-author-info{width:760px;margin:20px auto}#footer .copyright-tool,#footer .homeLink{width:760px;margin:20px auto}}#hexo-amp-id-1{line-height:1.8em;padding:0}#hexo-amp-id-2{line-height:1.8em;padding:0}#hexo-amp-id-3{line-height:1.8em;padding:0}
2、3年前の話ですが、クリエーターズマーケット名古屋(通称:クリマ)向けにPOSレジ・アプリを作ってみました。そこで今回はアプリのご紹介と伴に、技術寄りでアプリ制作の裏側を振り返ってみたいと思います。
javascriptやHTMLを沢山使っていますので、web界隈の方に参考にして頂ければ幸いです^^ 早速ですが、構成図はこんな感じです。
POSレジの主な構成図 ©
目次
レジ・アプリでできること
POSレジでは、離れた場所から商品登録できたり、登録した商品数からバーコードを自動作成する事ができます。詳しくは以下のような機能があります。
商品登録
商品画像や商品名、販売数、価格をアプリで登録する事ができます。ネットを介して登録できますので、出品者が複数いても各人の好きな場所で作業ができます。
バーコード・ラベル作成
スーパーのレジのように販売商品をバーコード管理できます。アプリでは登録された商品の販売数をもとに、バーコードに変換してPDFファイルを作成します。バーコードのレイアウトは一般販売されているシール台紙レイアウトに対応しています。PDFを台紙に印刷する事で、商品に貼ることのできるバーコード・ラベルとして使えます。
売上登録
バーコードの読み取りと売上登録をオフラインで行います。売上情報は本体に記録したあと、最後にサーバーへアップロードする事ができます。
集計結果
売上の集計結果をWebページから閲覧できます。グループ内で場所を選ばず売上状況を確認する事ができます。
報告書作成
売上報告書をPDFファイル形式で自動作成します。PDFはLINEで共有するなどして、情報を共有することができます。
バーコードの印刷用紙はAmazonで売ってるA-oneさんのラベル印刷用紙に対応しました。接着面はポストイット糊になっており、全部で486枚のシールを切り貼りすることが出来ますので、コストパフォーマンスはかなり優れていると思います。
用途
私は趣味で写真を撮っています。その関係で、友人たちとポストカードの販売に参加させて頂くようになりました。
tea dugong | Sony A7R2 + Sonnar T* FE 55mm F1.8 ZA photo by tea © レジアプリは、名古屋で毎年2回開催されているなごやクリエーターズマーケット(通称クリマ)の写真販売ブースに向けに、制作しました。実際にポートメッセなごやでレジ・アプリを使っていますので、機会がございましたら遊びにきてくださいね^^
「写真好きのつどい」名古屋クリエイターズマーケット
http://photo-love-gathering.tumblr.com/
解決したかったこと
私の参加しているクリマのブースでは、複数の出展者で写真にまつわる商品を販売しています。出展者が複数いるため、販売価格や商品が多様で、商品の管理や売上の管理、また会計方法が年々複雑になりました。
そこで、商品管理や売上集計の複雑さや手間を取り除くために、できるかぎりの事は自動化してしまおうと考えました。これがレジアプリを作ろうと思った切欠です。
現在、巷ではレジアプリが沢山存在しますが、解消したかったニーズと一致しなかったので、頑張って自作してみようと思いました。
どのような方針で作ったか?
前述のような経緯でクリマ向けにレジ・アプリを作ってみた訳ですが、以下の様な方針で制作してみました。
場所を選ばす商品管理等の作業ができる
商品の出品者が全員集まって作業することが難しいので、ネットを活用して商品を登録できるようにしたいと思いました。
システムの維持費を一切掛けない
趣味のブースという事と、年に2回開催されるイベントですので、維持コストを掛けない仕組みにしようと思いました。制作した結果、ラベルを印刷するための用紙費用だけで済ませる事ができました。
現場の手間は極力軽減
商品の把握、会計や報告書の作成に関する手間を軽減できるよう、アプリ側で自動化できるものは、とことん実装したいと思いました。
実装
冒頭でご紹介しましたが、システムは以下の様な構成で制作しました。使った技術はjavascript(node.js)とHTML5です。商用システムではまず使わないのですが、フロントエンドからバックエンドまでjavascriptで実装できるフルスタックMEANを採用してみました。
POSレジ・システムの主な構成図 ©
システム開発に用いたサービスやフレームワークは以下の様な内容です。
商品登録アプリはnw.js
nw.js(旧称:node-webkit)はHTMLを使ってクロスプラットフォームにデスクトップ・アプリを展開できるフレームワークです。現在ではElectron(旧称:Atom-shell)が有名です。特徴は、Web制作のノウハウをそのまま活用しつつ、node.jsの充実したライブラリを使用する事ができる点です。
商品登録アプリのUIはAngularJSとBootstrap
UIはBootstrapとAngularJSを使ってみました。Bootstrapは短いHTMLの記述で、ボタンをはじめとした各種UIを配置できるものです。AngularJSは、UIと値のやりとりを円滑に行うことができるフレームワークです。
売上のデータベースはmongoDB
売上結果を蓄積させるデータベースとして、mongoDBをHerokuのFree DynoでExpressサーバーから利用しています。mongoDBはJavascriptで操作できるKVSです。今回、興味があってなかなか使う機会がなかったので導入してみました。使った感想は、クエリと返ってくる結果がJSONなので感覚的で分かりやすく、スムーズに構築することができました。
商品情報や商品画像はFlickr
画像サーバーと商品情報のDBサーバーを考えるのが面倒だったので、Flickrを利用させてもらいました。アプリではFlickrの充実したAPIを、nw.jsの中に組み込んでいるnode-flickrから利用させてもらいました。アップロードやタイトルの変更(=タイトルに商品情報を埋め込む)もAPI越しに行えます。商用のシステムならアウトだと思いますが、個人用途ですので、どうか広い心で見てやってください(笑)
商品バーコードや売上報告書をヘッドレスブラウザから生成
こちらは今回のシステムの重要な機能を担っている部分です。nw.jsのアプリからボタン操作一つで、商品バーコードや売上報告書をPDF形式で出力できる訳ですが、これはヘッドレスブラウザのPhantomJSを使い、HTMLをPDFに変換し、出力できる機能を活用させてもらいました。
レジアプリはOnsenUIでAngularを使って商品の売上登録
生成したバーコードを読み取るアプリは、iPadで動作するMonacaアプリにて、OnsenUIとAngularJSを使っています。OnsenUIはBootstrapと似ているフレームワークですが、モバイル向けに軽量動作となっていたり、AngularJSと親密になっているのが特徴です。現在は大人の事情により、nw.jsアプリにリプレイスしています(^^;)
構成図には載っていませんが、ビルド&デプロイまではgulpタスクで自動化しています。ビルドされたアプリはDropboxフォルダーに生成されており、出展メンバーと最新バージョンを常に共有できる形にしてみました。
苦労したところ
制作には2ヶ月ほど要しましたが、主に時間が必要だったのは以下でした。
パッケージがバグまみれだった
Flickrにアクセスするために、node.jsのAPIラッパーを利用していたのですが、そのパッケージがかなり曲者でした。
開発初期は、パッケージの修正依頼をいくつかコミッターにお願いしながら制作を進めていきました。幸い、コミッターは修正について前向きでしたので、お陰さまでアプリを無事デプロイする所まで漕ぎ着ける事ができました。
AngluarJSを120%活用
今回はフロントエンドでAngularJSをフル活用しました。基本的な利用に加えて、カスタム・ディレクティブを作ったり、カスタム・フィルターを定義したりするなど、普段使わないような使い方を沢山しました。例えば商品IDを示す数値から、1次元バーコードのSVGに変換する処理などはカスタム・フィルターを利用しています。
UIに関しては、Angular Bootstrap (UI Bootstrap)をモジュールとして組み込んで使っています。SPAとして成り立たせるための必須スキルである、ルーティングのテクニックやモーダルウインドウの制御、更にはプログレスバーの実装が不慣れだったので、覚えるまで苦労しました。
また、AngularJSとAngular Bootstrapのバージョン依存により、モーダルウインドウが正常に動作しない等の問題が発生したので、依存関係にはかなり苦しめられました(^^;)
ヘッドレスブラウザの可能性
最も苦戦したのはヘッドレスブラウザのPhantomJSからPDFを生成する処理でした。
用紙やプリンターの送り出し誤差の関係上、1ミリ程の許容誤差でバーコードを正確にレイアウトし印刷する必要性があったのですが、なかなか骨の折れる作業でした。
PDF生成するHTMLにはcssやjsを含める事ができるのですが、OSによってスタイルが崩れてしまったり、画像が全て取得される前にタイムアウト(=画像が抜け落ちる問題が発生)してしまったりと、初期の段階では上手く生成できませんでした。まずはPhatomJSの使い方や癖を知る事から始めて、1つ1つ試行錯誤していきました。
この過程で、nw.jsから狙ったとおりのPDFレイアウトを自在に作成できるようになり、結果的には勉強になりました。
ヘッドレスブラウザは多機能なのですが、その反面で使い方を熟知するのは大変な事です。巷では、PhantomJSが難解すぎて、機能を簡素にしたAPIラッパーが存在するほどです。その名前は、Phantom JS→Casper JS→Nightmare JS。…いやいや、怖いんですが(笑)なぜこんな命名にしたんだろうw 幽霊ポケモンが進化したみたいで面白いですよねw
ヘッドレスブラウザは、一旦使いこなせるようになると恐ろしく便利な代物で、様々な応用ができる可能性を秘めたパッケージだと思いました。
Heorkuのプラン改定には動揺した
予想外の出来事だったのは、Herokuの無料プランが改定されてしまった事でした。Herokuはもともと、750時間/月の無料利用枠が利用することができ、試験サービスやアクセスの少ないサービスでは、サーバーを実質無料で運用できたのです。
ところが、サービスプランの改定により、18時間/日の起動に制限されてしまいました。つまり24時間でサーバーが動きません。このサービス改定には、かなりショックでしたが、今回はアクセス時間帯が数時間に限られている案件でしたので、現在もそのまま使ってもらっています(^^;)いざとなれば、有料プランを利用すればしっかりと動作します。
最後に
ということで、今回はクリエーターズマーケット名古屋(クリマ)で使えるレジアプリを自作した経緯や実装に関する裏側をご紹介させて頂きました。制作に関する何かの参考になれば幸いです(^ ^)また、何か改善案がありましたらTwitterなどで教えていただけましたら、大変勉強になります。それでは!