社内UI/UXデザイナーチームの立ち上げとこれから

 

はじめまして。

株式会社アップガレージグループ ITソリューション事業部 UI/UXデザイナーの佐藤です。

 

4月になって新年度が始まりましたね。

無事24卒メンバーの入社式も終わり、私もあっという間に社会人2年目に突入します。

 

実は、弊社、株式会社アップガレージグループで社内デザイナーチームが立ち上がったのは昨年の2023年。なんと1年しか経っていないのです。

 

そんな私も入社したのは1年前。

 

「新卒で入社してまさかの先輩がいない?!」

「デザイナーの知識は向上できるのだろうか。」

「マニュアルがなくて間違いを犯してしまったらどうしよう。」

 

少し不安になりながらも、新しいことに挑戦できることのワクワク感とドキドキ感を噛み締めながら入社したのを今でも覚えています。

結論、この1年間は濃くて深くて、大きな学びと挑戦、さらには成長を感じられた一年でした。たくさんの挑戦できる環境を与えてくれた会社に非常に感謝しています。

 

では具体的に、この1年間デザイナーチームではどんなことを行ってきたのか。

社内デザイナーチームの立ち上げから今後のデザイナー育成にあたり、取り組んできた内容を今回お話ししたいと思います。

 

デザイナーチームのこれまでの取り組み

1. 入社して初!外部の方との研修期間

先輩がいなくて「これからどうなるのだろう?」と感じながら始まった、研修初日。

毎日緊張と期待を胸に、全力だったのを覚えています。

 

会社の全体研修と部署の研修が並行して行われ、デザイナーの基本的な技術サポートは

長年専属で弊社をサポートしてくださっている外部の方にお願いしていました。

 

外部の方とは、リモートで直接やり取りをし、毎日たくさんのフォローをしていただきました。

結果、当初の不安は取り除かれ、研修内容に不安になることはありませんでした。

デザインの基礎からコーディング実務まで学ぶことのできる、とても満足できる研修で、今の実務にも非常に役立っています。

 

研修内容

研修期間ではデザイン基礎やコーディング基礎だけでなく、バナーのトレースや考察なども行いました。実務の時間には、なかなかできない勉強の時間にもなりました。

バナートレース

バナーからの考察

 

WEBデザイナーを目指す人であれば、こういった内容を学校で学ぶかもしれません。

こういったところを再度確かめながら、勉強できた研修期間は非常にありがたかったですね。

 

そんな中、7月中旬研修が終了し、いよいよ本格的にデザイナーとして配属となりました。

 

研修に関してですが、新入社員の研修期間はとても貴重であっという間です。

今から研修が始まる方は毎日を大切にしてほしいです。(一人の社会人2年目の先輩の意見ですが絶対全力で戦って損はないです。笑)

 

同期の存在

もちろん、デザイナーチーム立ち上げにあたって(これはどんなことにも言えますが…)研修期間の同期との親交は非常に大切でした。

基本お仕事はチームで行うもので、企業で研修を行った場合、その期間に出会った人と

お仕事をしていく可能性は高いです。

 

私もデザイナーチームの一員として同期には非常に感謝しています。

特に今回のデザイナーチームという枠組みを立ち上げるにあたっては

まず、仲間の存在無くしては、チームにもなりませんでした。

チームにしてくれた、同期と先輩方に感謝ですね。

同期のハラちゃんとさと(私)

これは社内の25thプロジェクトで、一緒に取り組んだ時の写真です。

(仲良さそう、本当に仲良いです)

 

2. デザイナーチームの定例MTG

正式にチームとなり、デザイナーチームでは研修を終えてから、毎日行っていることがあります。

それは「デザインの共有」です。

 

お仕事の流れで毎日MTGで自分たちのタスクのチェックをしています。

ただ、せっかくMTGの時間をとっているのだから、

このチェックだけの時間にするのは勿体無い!

余った時間をデザイン共有の時間として、使っています。

 

どんな些細なことでも構わないので、

チームのメンバーが気になった記事や技術、WEBサイトデザイン、バナーデザイン

などを共有する時間です。

こんな感じで、事前にリンクなどを載せて、定例MTGでチームメンバーで意見を共有します。

 

Slackチャンネルを利用して共有しています

ここでの内容が新たなアイデアの引き出しになっていることは多いです。

実務でも、「そういえば昨日使っていたあの技術やデザイン良かったな」と思い出して、お仕事に生かすことは多くあります。

 

3. UXチーム立ち上げ

「デザイナー✖️マーケティング」でUXチームも下半期から立ち上がりました。

デザイナーの領域ってただ、色や形を綺麗にまとめて成果物を出せばいい。

というわけではないです。

 

この辺りは、お仕事をしていくにあたってすごく感じるようになりました。

デザインは必ず伝わらなくては意味がない。

「伝わるデザイン」って?

「伝える相手」は?

 

下記体制を図式化してみました。

 

今までの体制(かなりシンプルにいうと)

UXチーム体制


デザイナーも依頼者が求めるものや本来の目的を根本から理解しようというイメージです。

お仕事において、デザインの成果物を届けるエンドユーザが必ずいます。

そして、その方に何かアクションをしてもらうことが基本です。

(それが売上向上やブランディングにつながります)

 

そのためには、デザイナーが元から理解していなければ、お客様に届くはずがない。

「UXを考える」。とは?

まさに私が直面していた壁でした。

 

こうやって図式化してみるとその概念が、形になっていく感覚がありますね。

ユーザ体験をよくすると一口に言っても、そのために細分化して、デザイナーの私が何をできるのか。こういった考えはチームで共有しておくべき内容です。

 

このように進めていくと、UXデザイナーは、会社について一番詳しく知っているくらいの勢いで、さらには経営者目線の意見を知っておくことが非常に重要だと感じます。

 

ビジネス側の意見は関係ないでしょ?効果があるならやってみることが重要!

というふうに思うかもしれません。

でもでも、ビジネス側の意見って非常に重要です。

だってお仕事には必ずお金がかかるから。

費用対効果も考えておかないと、ビジネスにはなりません。

 

下期からのUXチーム発足は依頼者から直接聞ける効率の良さであったり、よりプロダクトを良いものにしていくために欠かせないものだと感じます。

まだまだ発足したばかりですが、ここからよりスピード感を持って、良いプロダクトや会社としての成長に貢献できるように頑張りたいです。

 

そして直近ではこちら!

25周年プロジェクトです。(先程の写真にもありましたね。)

UXチームとして、会社内で参加させていただきました。

メインでサイト作成をハラちゃん(原)、ロゴ作成をさと(佐藤)で携わらせていただきました。

 

ぜひサイトも見ていただけると嬉しいです!デザイナーチームが加わったことで

より今までになかった新しいイメージを実現することができました。

www.upgarage.com

 

デザイナーチームのこれから

1. ナレッジ・素材の共有

今年発足したチーム。まだまだ私たちチームには欠ける部分が多くあります。

各サイトのナレッジやデザイン素材の共有はまとめておく必要があります。

これは、一組織として、チームに新たにジョインしてくれる将来のメンバーのためにも。

 

内製化できたからこそ、社内でそういった居場所を作れる機会になっていると思います。

私としては、デザイナー立ち上げメンバーの一人としてしっかり土台を作り上げたいと思っています。

下記のように事業が大きくなれば、しっかりプロダクトごとに分業し、より高い製品や成長を目指せる領域までいきたいですね。

 

組織の拡大イメージ

 

そのためにも、まずは今土台をしっかり丁寧に構築することが大切。

そしてその第一人者が他の人に知識を共有できるくらいまでに、土台を理解していることが重要だと感じます。

 

まず今行っていることとして、会社のために作成されてきた素材などの整理です。

その制作物、特に企業ロゴや画像、イラストなどは繰り返し使うものが多いです。

まとめて共有できる居場所を作らなければ、

新しい人が入ってきた時に困ってしまいます。

さらに、会社全体の理解も進めており、自分たちの基礎固めをしています。

 

土台として、チームとして知識や考え方をしっかりまとめ、誰がいつみても良い状態にもしておかなければなりません。

  1. 各サイトのデザインシステム構築・ドキュメント化
  2. 使用する素材の共通化
  3. 社内でデザインが必要なものに対するテンプレート化

上記は今後、弊社デザイナーチームで行っていきたいものです。

 

他にも他社サービスを分析しながら、自社にはないものを補い、

顧客体験の向上と会社の成長に貢献したいと考えています。

 

2. 新人メンバー育成

今年から初の社内デザイナーチームで作成した研修カリキュラムが始まります。

新たなメンバーのジョインも予定しています。

 

いかに先輩として、伝えなくてはいけない部分を伝えていけるか。

これは最初のデザイナーチームの挑戦です。

 

前述した通り、会社としてもさらに大きくなれば、

必然的にデザイナーチームが大きくなっていく必要があるでしょう。

まだまだデザイナーチームの挑戦は続きます!

 

これからデザイナーになりたいと思っている方やこんな仕事をしてみたい!

自分の知識を使ってたくさんの挑戦したいみたい!

という方はぜひ弊社に興味を持っていただけたら嬉しいです!

 

最後までお読みいただきありがとうございました。

 

株式会社アップガレージグループ ITソリューション事業部では、共に勉強し合い、成長し合うメンバーを大募集しています!
www.upgarage-g.co.jp

 

 

 

初めてChromeの拡張機能を作ってみた!

 

はじめまして。株式会社アップガレージグループ ITソリューション事業部 エンジニアの中尾です。

 

今回のテーマは、「Chrome拡張機能作成」について。日々Chrome拡張機能は利用していて、自分でも便利なツールを作成したいと思ったため共有いたします。

 

1.Chrome拡張機能とは

まず、ChromeとはGoogleが開発したWebブラウザであり、現在は世界シェアNo.1で多くの方が利用しています。そのChromeに機能を追加・強化することができるのが、Chrome拡張機能です。

ここでは、作成から導入までの流れをご紹介します。

 

2.実際に作成してみる

今回はGithub上で好きなスタンプを使えるようにしてみます。(デフォルトのスタンプだと味気ないので・・・)

以下のようなフォルダ構成で作成していきます。

2-1. manifest.jsonの書き方

manifest.jsonは、Webアプリケーションやブラウザ拡張機能の開発に使用されるJSONファイルです。

「作成するChrome拡張の設定全般」について書く必要があり、

  • どんなファイルがあるのか
  • どんな機能を利用するのか
  • どんな制限を設けるのか

など定義する必要があります。以下記述例です。

▼manifest.json


    {
        "name": "Githubのスタンプ変換機能",
        "version": "1.0.0",
        "manifest_version": 3,

        "content_scripts": [
            {
                "matches": ["*://github.com/**"],
                "js": [
                    "js/jquery-3.7.1.js",
                    "js/main.js"
                ],
                "run_at": "document_end",
                "all_frames": false
            }
        ],
        "web_accessible_resources": [
            {
                "matches": [ "http://*/*", "https://*/*" ],
                "resources": [ "/images/*" ]
            }
        ]
    }
  

name

拡張機能の名称で、45文字まで入力できます。

version

拡張機能のバージョンです。拡張機能を更新する際には必ずバージョンが上がってなくてはなりません。

manifest_version

マニフェストファイル自身のバージョンです。

content_scripts

コンテントスクリプトを使用する場合に記載します。

matches

content_scriptsに対する必須の項目で、動作対象となるURLをMatch Patternsの形式で設定します。

js

content_scriptsに対する任意の項目で、動作させるスクリプトをリストで設定します。

web_accessible_resources

拡張機能で特定の画像などを使用する場合に記載します。

2-2. 処理したいコードを実装する

上記の画像を見ていただくと、Githubのスタンプはテキスト形式で表示していることがわかります。そのため今回はSVGをテキストに変換して、スタンプを変更していきたいと思います。

▼main.js


    // 新しい画像のURL
    const newImageUrl = chrome.runtime.getURL('../images/shouchi.svg');

    // <g-emoji>要素を取得
    const emojiElement = document.querySelector('g-emoji[alias="-1"]');

    // テキスト内容を置き換える
    emojiElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" width="24" height="24">
      <image xlink:href="${newImageUrl}" width="24" height="24"></image>
    </svg>';
  

詳しい説明は省きますが、SVG形式のスタンプをテキストに変換して置き換えています。これで既存のスタンプが、今回用意したスタンプに置き換わっているはずです。

2-3. 作成した拡張機能を反映させる

さて、それでは実際に作成した機能を確認してみましょう。

Chrome拡張機能ページを開き、デベロッパーモードを有効にします。

②「パッケージ化されていない拡張機能を読み込む」を選択し、拡張機能のルートフォルダを指定します。

③インストールできたら、拡張機能を有効にします。

拡張機能を有効化し、実際の画面を見てみると反映できてますね!

3.まとめ

今回は自由にスタンプを変更できたかのように見えますが、実際スタンプを使用してみるとエラーが発生し反映されません。

スタンプを反映させるにはまだまだ工夫が必要そうですね!今後はスタンプが反映され、自由に使えるまでにしたいですね。

 

ここまでお読みくださりありがとうございました!


株式会社アップガレージグループ ITソリューション事業部では、共に勉強し合い、成長し合うメンバーを大募集しています!
www.upgarage-g.co.jp

ことばの骨片から見るUXデザイン

 

 

 はじめまして。株式会社アップガレージグループ ITソリューション事業部 UXデザイナーのハラです。

 

 今回のテーマは、「UXデザインと言語化の関係」について。会社に所属するデザイナーとしては駆け出しの新卒の目線から、考えたことをまとめてみました。

 

 

1.本質ってなんのこと?

 会社ではよく「本質を見る、本質を探る」という言葉を耳にします。「本質」とは、物事の根本的な性質・要素のことであり、表面上のものではありません。目の前にあるデータや意見を必ずしも鵜呑みにせず、その背景や要因をもとに課題解決に向かう姿勢が求められています。

 UXデザイナーで言えば、「これをこうしたい!」という目の前の課題を来たままに打ち返すのではなく、そこに至った経緯までを紐解いていき、根っこの部分にある課題をいかに解決するか=ユーザー体験を上げるか、が業務の目的になってきます。

 本質。ビジネスシーンにおいて、ぐっと重みのある一単語です。

 

2.UXデザイン業務と言語化のあれこれ

 デザイナーは依頼を受けてものづくりを行うことが多いです。その依頼は、大枠だけが決まっていて細かい部分はデザイナーに任せられる場合も、要素を細かく指定している場合もあります。

 どちらにも言えることは、「文字だけの依頼を読み解くのは難しい」ということです。同様に、デザイナーから依頼側へ確認の連絡をする場合も、文字だけでは情報量が少ないのです。

 とはいっても、文字面だけではわかりにくいですね。頭の中にあるイメージを言語化してもなかなか伝わりづらい経験は、きっと誰にでもあることだと思います。視覚化するために、以下の例で見ていきましょう。

 

<依頼例(仮)>

〇〇の販促のためにバナーを刷新したい。商品名のフォントは大きく太めで、背景には夕焼けの写真を使用したいです。

 

 上記の依頼において、詳細を詰めなければならない部分は数多くあります。大枠としては以下の2点で考えられるでしょう。

 

2-1. 言語化された課題から本質を探る

 UXデザイナー業務において、依頼元の要求および詳細を詰めていく過程で、全てを鵜呑みにしてものづくりを行うことは基本的にありません。

 言語化された依頼を受け取ったら、

  ・その背景には何があるか?

  ・この依頼に至った理由はなにか?

  ・本来の目的をこの作業で達せられるか?

 を考えていきます。

 背景を知る中で、言語化された課題の本質が別のところにある場合もありますし、言語化によって本来の意図がブレている場合もあります。

 

上記の例で言えば、

 ・バナーを刷新することでどんな効果をあげたいのか?

 ・購入者にどのように訴求したいのか?

 ・夕焼けの写真をセレクトした意図は?

などが挙げられるでしょう。

 例えば「商品コンセプトをベースに、〇〇のようなペルソナを設定し、背景画像は空間をイメージさせるようなもので、かつ複数の色のグラデーションが収まったバナー」が本来要求されているのであれば、夕焼けの写真以外を使用したバナーを提案しても良いわけです。 

 

2-2. 言語化が難しい、デザインの細部

 さて、実際に夕焼けの写真を使うことになったとします。ひとくちに夕焼けといっても、言葉が持つ重さや色合いは人によって異なるため、これを紐解いていく必要があります。簡単に色をイメージしただけでも、いろいろな解釈ができてしまうからです。

"夕焼け"の例

この場合は、言葉ではなくビジュアルを使用して確認する方が、よりスピーディーなやり取りが可能なように思います。勿論イメージが固まってからも、その他の掲載情報との兼ね合いを見ながらバランスを取っていきます。画像でなく他の要素ひとつ取っても、イメージや本来訴求したいこと・ビジュアル的な効果を考えて、何パターンか作成することも多いです。

 

2-3. 質問の仕方を工夫する

 入社後のデザイン研修で質問・インタビューの仕方を学びました。個人的に一番難しいのがインタビューの構造を考えることです。

 ・はい・いいえで終わる質問(=クローズドクエスチョン)をしないこと

 ・文脈に沿った質問をすること

この辺りは意識していても、つい誘導のような訊きかたをしてしまう時があります。

 実際にチームメンバーと研修にて、コンテクスチュアル・インクワイヤリー(ユーザーの行動を観察しつつ状況に応じてインタビューを行う調査方法)を行った際も、臨機応変な質問や流れに沿った気遣いが難しかったように思います。

 期日を気にしていたり、目の前だけに集中してしまっていたりすると、気を配れなくなる部分です。当たり前に俯瞰して物事を見ることができるように、日々の業務から気をつける必要があると感じます。

 

つまり必要なこと

 必要なことは、「こういう意図だろう」と推し量るのではなく、わかった気になるのでもなく、直接訊くことです。そうして得たことばの端々に、本来の目的である核のようなものが存在しているように思います。ともすると膨大な単語に散らばりがちなその揺らいではいけないものを、この記事のタイトルでは骨片と表現しています。

 

3.すこしの気遣いが体験をより良くする

3-1 体験のあり方について

 弊社のデザイナーは、作成したデザインをコーディングすることも多いです。基本的にはhtml/cssですが、Vueやphpなども触れる機会があります。知らない部分がたくさんある領域なので都度調べるのですが、頻繁にぶち当たるのが「既に理解を深めている人が書いたHow to記事がわかりづらい」という事象です。

 〇〇をここに読み込んでくるだけ!簡単ですね!

といった文言に何度頭を悩ませたかわかりません。初学者向けと謳っているサイトでも見られる問題です。「何を」「どこに」「どのようなコードで読み込ませるか」を知りたいというのが本来の目的であるはずなのです。あるものに対して理解をしていると、過程や必要なはずの情報までを覆い隠すかのように、熟語や横文字でひとくちに纏めてしまいがちなのが、言語化する際によく見られる傾向だと思います。

 個人的にはこういった問題を解決するのがUXデザインだと思っています。初見の人でもわかりやすく、操作しやすく、目的を達するための道筋が立っているものが私の理想です。

3-2 原点:プロセスを細分化すること

 大学2年生のとき、GUIの授業で作成した図が私らしさをとてもよく表している気がしています。あるプロセスを踏むものをUIを作成することで視覚化する課題で、発想を飛ばして野菜の切り方に行き着いたのが個人的にとても気に入っているものです。

"プロセス"を細分化し視覚化すること

 料理の手順であれば「1. 大根をいちょう切りにする」と一文で書いてあるものを、さらに詳しく区分して、野菜のカットという切り口で広げていった過程図です。今から見れば表現しきれていない部分があり、Illustratorにも不慣れな感じがしますが、「過程にあるものを細分化すること」は当時から好きな作業のひとつでした。スキップしてしまいがちな間にあるものを取り零さずに、要素をアウトプットに起こしていくことは、どのデザインを作るときにも必要なことだと考えています。

 

3-3 現在と、これから

 現在の業務でいえば、私は車業界に明るいわけでもなければ、車の免許すら持っていません。もちろん大前提として知識をつけていくことも必要ですが、一方で、車のことに詳しくない目線だからこそ、より”わかりやすさ”を意識したデザインを作成できるのではないかと思います。コアな方だけでなく、それまであまり深く関わってこなかった方にも間口を広げていけるようなデザインを目指して、精進していきます。

 

 

ここまでお読みくださりありがとうございました!


株式会社アップガレージグループ ITソリューション事業部では、共に勉強し合い、成長し合うメンバーを大募集しています!
www.upgarage-g.co.jp

ジュニアエンジニアが要件定義について体系的に学んで得たこと

こんにちは。株式会社アップガレージグループ ITソリューション事業部のエンジニア加藤です。 最近、弊社の20%ルール内で取り組んでいる「要件定義の体系的な学習」について記事で共有します。

20%ルールとは?

弊社では、業務時間の20%を将来的なチャンスにつながるプロジェクトの探索やスキル習得の時間として使用可能です。 1日の業務稼働時間を8時間とすると、1週間で最大8時間がこれに割り当てられます。

経緯

これまで「要件定義」といった形式で体系立って学習したことが無く、自分の知識の最適化や棚卸しをしたかったため。

学習に使用した本は以下となります。

gihyo.jp

特に学びになったこと

備忘録程度に、それぞれの章ごとに特に学びになったことを記載していきます。

1章 要件定義の基礎知識

段取り8割

  • 開発規模、希望納期を把握する
    • 衝突・トラブルが発生しそうな箇所は予めルールを決めておく

潜在要求の明示化

  • 表明あり、認識あり(顕在要求):網羅的に整理し、より正確に理解する
  • 表明なし、認識あり(暗黙の要求):念を入れて確認を怠らない
  • 表明なし、認識なし(潜在要求):相手からの発信を触発させるよう、複数の角度から質問する
    • ※個人の解釈ですが「あ~そういえば◯◯◯ってよく使うかも!」のような回答が引き出せると良いイメージ

ウォークスルー

  • 意思決定者に全体を流れで理解してもらい、スムーズに承認が得られるよう準備・リハーサルを行う。

ベンダーとしてユーザーをリードする

  • あくまで指針を導き、それを正確に伝えることに徹する
  • ユーザーを置き去りにしたり、強制的に操作すべきでない

2章 要件定義の下調べ・段取りフェーズ

開発開始後の想定外を減らすこと

  • このフェーズでは想定外を減らすことに注力したい
    • 時間は有限なので可能な限り時間を節約する(資料作成など)
  • 把握した内容はユーザー(依頼側)にも共有し、お互いの認識齟齬を減らす

ユーザー側のステークホルダを可視化する

  • ユーザー側にとっての利害関係者・それとのやりとりを理解する

クリティカルパスは押さえる

ステークホルダの識別

  • 本当の顧客、ユーザーを意識する
    • 直接対面するユーザー側担当者の意見のみを鵜呑みにしない
    • 外部のステークホルダの存在に気づかず考慮漏れになりがち

3章 業務要求の分析・定義フェーズ

現行業務の調査

  • 現状の業務を調査し、理想とのギャップを明示化する
    • ここでもユーザー側の潜在要求を掘り出す
  • 各業務を一覧化し、自動化できる作業が無いか洗い出す

当事者(ユーザー)に「組織に不足する仕組みやスキル」を特定してもらうよう、ベンダとしてアプローチする

  • ここで不足している事項が業務要求となる

ビジネス・ルール分析の位置づけ

  • 暗黙的な業務やルールが存在すると開発の漏れとなり、結果的に使えないシステムになる
    • 開発に際して、ベンダ側一元的にでルールを作成→浸透できるようアプローチする

ステークホルダが主体のレビュー会を開催する

  • ベンダー側(要件定義担当)がファシリテーションを行う
  • 目的:要件を文書化した後、実際の事業における目標実現に対し有効か確認する

4章 機能要求の分析・定義フェーズ

必要不可欠なもの、必須でないものを判断する

  • 肥大化を防ぐため、不要なものは可能な限り整理・廃止する方向で進める

既存システムにおける不具合は、業務の仕組みから正しい形にする

  • システムの側面から追求すると期待値がズレることがある
  • 分析中に取り決めたこと、設計に引き続くことは文書化する

帳票

  • ビジネスやオンライン上の業務フローだけでなく、帳票等の集計結果も分析対象
    • 帳票出力では、「目的」「場所」「時間」「ユーザー」をきちんと把握する
    • 例:電子表示だけで良い場合、pdf化が必要な場合、紙として印刷機能が欲しい場合など
  • 一度に出力される量も踏まえ、期待される出力スピードを意識する

通常期と繁忙期のトランザクション量の違いも念頭に置く

外部接続

  • 連携箇所は漏れなく洗い出す
    • 何のために使用しているか、導入時に一覧を作る
  • システム本体に無いロジックを有しているため、開発・保守で考慮漏れになりやすい

5章 非機能要求の分析・定義フェーズ

性能

  • 応答の速さは「5秒以内」など定量的に定義すべきだが、通常のユーザーにはIT知識が無く定義は難しい
    • 仮に定量的に定義できても、通常の場合、要件定義時点で実現可能かは判断できない
    • 基本的に「コストとのバランス(コスパ)」で実施すべきか検討する

可用性

  • 顧客からの信用失墜リスクと、冗長化や高スペック化に必要なコストを天秤に掛ける
    • 基本的に「コストとのバランス(コスパ)」で実施すべきか検討する

保守性

  • 運用・保守は自動化することがよく目標になるが、人の入り込む余地は残しておいた方が良い?
    • 自動化しすぎてログを閲覧できない・ログが多すぎて管理できないケースも有り得る

移行性

  • 新システム導入時、確実に成功させる必要があるイベント
    • 以降前後の規模を見積もり、対策に過不足を作らない

セキュリティ

  • 前提として、セキュリティ対策と性能はトレードオフ
    • 事故による社会的信頼・経済的な損失を見積もった上で、システムに求められる強度を見誤らない

システム環境・エコロジー

  • 近年だとシステムにエコロジーを求めるケースも存在する(温室効果ガスやエネルギー効率など)
  • システムに必要なツールの物理的な制約

6章 要件定義の合意と承認・維持フェーズ

要件に関する討論と合意

意思決定の記録、経緯と理由の記録

  • 承認が得られなかった箇所は、強引な説明は避けて積み残しとして切り離す
  • 一部の問題点のために全体が覆されたり、作業が一時停止することは避けなければならない

討論→合意のサイクルはルーティン化する

  • 一定周期で合意形成する仕組みで、各メンバーの当事者意識を強める
  • 小分けに議論する

ライフサイクル管理

  • 正常稼働だけでなく「要求」が意図通り実現できているか?という観点で考える
    • 逆に要求されていない機能、例えば「開始当初実装されていたが、環境変化により不要になった機能」は、正常稼働の必要性は低い

自分自身に活かせたこと

学習終了後まもないため実際に要件定義書を書いておりませんが、実務において役立ったことは多数あります。

開発時に意識すべきことの順序を明示化できる

  • 要件漏れの防止や業務上の非効率の排除に寄与しました。

自社の価値にまた一つ気づいた

  • 直接役に立ったことではございませんが、開発者と実際の現場(店舗)が近く情報伝達が早い弊社は、ビジネスを進めていく上の障壁が一つ少なくメリットが大きいと感じました。

これからについて

要件定義に限らず開発に必要なスキルを継続的に学習し、品質向上や自分のスキルアップに繋げていきます。

株式会社アップガレージグループ ITソリューション事業部では、共に勉強し合い、成長し合うメンバーを大募集しています! www.upgarage-g.co.jp

開発時によく使う便利コマンドまとめ

株式会社アップガレージグループ ITソリューション事業部でCrooooberをはじめとしたWebサイトの開発・運用をしています、加藤と申します。
今回は、私が開発時によく使う便利コマンドを、備忘録としてブログに残しておこうと思います。

Ubuntu

Ubuntuのデスクトップ環境で開発を行なっているのですが、稀に日本語入力が効かなくなることがあります。
何をやっても解決しない時は際は下記コマンドでibusデーモンを強制的に再起動しています。

ibus-daemon -r -x

Git

ショートカットコマンド

ターミナル上でGitの操作を行なっており、入力の手間に煩わしさを感じたため下記コマンドでショートカットを作ってます。

git config --global alias.co checkout
git config --global alias.b  branch
git config --global alias.cm commit
git config --global alias.mg merge
git config --global alias.st status
git config --global alias.ps push
git config --global alias.pl pull
git config --global alias.rb rebase

使用例

# チェックアウト
git co {branch name}

# ブランチ表示
git b

# コミット
git cm -m "commit name"

# マージ
git mg {branch name}

# ファイルステータス確認
git st

# プッシュ
git ps origin {branch name}

# プル
git pl

# リベース
git rb {branch name}

grep

xargsと組み合わせて一括置換を行なうコマンドです。
大抵エディタの一括置換機能で十分ですが、修正対象が多い場合は時間が掛かるためコマンドを使用することもあります。

git grep -l "置換前" | xargs sed -i "s/置換前/置換後/g"

grep」コマンドではダメなの?という意見もあると思いますが、「git grep」はgitプロジェクト配下のみを探索するためこちらの方が高速です。

AWS S3にREST APIのみで画像アップロードをしてみた

株式会社アップガレージグループ ITソリューション事業部でCrooooberをはじめとしたWebサイトの開発・運用をしています、加藤と申します。

今回は、AWS S3にREST APIのみで画像アップロードをする機能を実現してみたので、記事として公開いたします。

 

 

背景

先日、AWSからこのようなお達しがありました。

2023年6月28日までにすべての AWS リージョンで、すべての AWS APITLS バージョン 1.0 と 1.1 が使用できなくなることを意味します。

aws.amazon.com

弊社にもSDKを用いてS3にファイルアップロードしているSpringアプリがあり、TLSv1.2に対応していない古いバージョンだったためアップグレードを試みたものの、使用しているフレームワークの関係で、期限内でのSDKアップグレードは現実的に困難という判断に至りました。

他の手段を探してみると、S3にはSDKに頼らずRESTのみで完結するAPIが提供されていたため、こちらを使用してアップロード機能を再実装する運びになりました。

PutObject - Amazon Simple Storage Service

 

実装

ファイル内のデータチェックや例外処理は省略しています。

public class S3Util {
/** * S3のRESTでコンテンツをアップロードする */ public static void upload(String s3BucketName, String, s3RegionName, String key, InputStream inputStream) { // ソースコードに秘匿情報を保持するのはセキュリティ的に問題なので、properties等に適宜格納する String awsAccessKey = "アクセスキー"; String awsSecretKey = "セキュリティキー"; URL requestUrl = new URL("https://s3-" + s3RegionName + ".amazonaws.com/" + s3BucketName + "/" + key); // inputStreamをバイナリデータとしてbyte配列に格納する byte[] fileBinaryData = IOUtils.toByteArray(inputStream); // バイナリデータ化されたコンテンツをハッシュ化する byte[] contentHash = AWS4SignerBase.hash(fileBinaryData); String contentHashString = BinaryUtils.toHex(contentHash); Map<String, String> headers = new HashMap<String, String>(); headers.put("x-amz-content-sha256", contentHashString); headers.put("content-length", "" + String.valueOf(fileBinaryData.length)); // S3のストレージクラスも選択可能。今回は頻繁にアクセスするデータのため「STANDARD」を選択 headers.put("x-amz-storage-class", "STANDARD"); // コンテンツのアクセス制限。「public-read」に設定 headers.put("x-amz-acl", "public-read"); // PUTとしてリクエストを準備 AWS4SignerForAuthorizationHeader signer = new AWS4SignerForAuthorizationHeader(requestUrl, "PUT", "s3", s3RegionName); // アクセスキー、シークレットキーを含めて認証情報を生成 String authorization = signer.computeSignature(headers, null, contentHashString, awsAccessKey, awsSecretKey); headers.put("Authorization", authorization); // HttpUtilsにてリクエスト実行 HttpUtils.invokeHttpRequest(requestUrl, "PUT", headers, fileBinaryData); } }

 

public class BinaryUtils {
	/**
	 * バイト配列を16進数でエンコードされた文字列に変換するメソッド
	 */ 
	public static String toHex(byte[] data) {
		StringBuilder sb = new StringBuilder(data.length * 2);
		for (int i = 0; i < data.length; i++) {
			String hex = Integer.toHexString(data[i]);
			if (hex.length() == 1) {
				sb.append("0");
			} else if (hex.length() == 8) {
				hex = hex.substring(6);
			}
			sb.append(hex);
		}
		return sb.toString().toLowerCase(Locale.getDefault());
	}
}

 

public abstract class AWS4SignerBase {
	// 使用する暗号化方式や日時フォーマットなど
	private static final String MAC_ALGORITHM_TYPE = "HmacSHA256";
	private static final String MESSAGE_DIGEST_ALGORITHM_TYPE = "SHA-256";
	private static final String ISO8601BasicFormat = "yyyyMMdd'T'HHmmss'Z'";
	private static final String DateStringFormat = "yyyyMMdd";

	protected URL endpointUrl;
	protected String httpMethod;
	protected String serviceName;
	protected String regionName;

	protected final SimpleDateFormat dateTimeFormat;
	protected final SimpleDateFormat dateStampFormat;

	/**
	 * aws signature v4の共通署名生成用のメソッド
	 */
	public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
		this.endpointUrl = endpointUrl;
		this.httpMethod = httpMethod;
		this.serviceName = serviceName;
		this.regionName = regionName;

		dateTimeFormat = new SimpleDateFormat(ISO8601BasicFormat);
		dateTimeFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
		dateStampFormat = new SimpleDateFormat(DateStringFormat);
		dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
	}

	protected static String getCanonicalizeHeaderNames(Map<String, String> headers) {
		List<String> sortedHeaders = new ArrayList<String>();
		sortedHeaders.addAll(headers.keySet());
		Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

		StringBuilder buffer = new StringBuilder();
		for (String header : sortedHeaders) {
			if (buffer.length() > 0) buffer.append(";");
			buffer.append(header.toLowerCase());
		}

		return buffer.toString();
	}

	/**
	 * 署名に付与するヘッダー内容の正規化とソート
	 */
	protected static String getCanonicalizedHeaderString(Map<String, String> headers) {
		if (headers == null || headers.isEmpty()) {
			return "";
		}

		List<String> sortedHeaders = new ArrayList<String>();
		sortedHeaders.addAll(headers.keySet());
		Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

		StringBuilder buffer = new StringBuilder();
		for (String key : sortedHeaders) {
			buffer.append(key.toLowerCase().replaceAll("\\s+", " ") + ":" + headers.get(key).replaceAll("\\s+", " "));
			buffer.append("\n");
		}

		return buffer.toString();
	}

	/**
	 * 署名に付与する内容の正規化
	 */
	protected static String getCanonicalRequest(URL endpoint, String httpMethod, String queryParameters, String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) {
		String canonicalRequest =
				httpMethod + "\n" +
				getCanonicalizedResourcePath(endpoint) + "\n" +
				queryParameters + "\n" +
				canonicalizedHeaders + "\n" +
				canonicalizedHeaderNames + "\n" +
				bodyHash;
		return canonicalRequest;
	}

	protected static String getCanonicalizedResourcePath(URL endpoint) {
		if (endpoint == null) {
			return "/";
		}
		String path = endpoint.getPath();
		if (path == null || path.isEmpty()) {
			return "/";
		}

		String encodedPath = HttpUtils.urlEncode(path, true);
		if (encodedPath.startsWith("/")) {
			return encodedPath;
		} else {
			return "/".concat(encodedPath);
		}
	}

	/**
	 * パラメータマップの正規化
	 */
	public static String getCanonicalizedQueryString(Map<String, String> parameters) {
		if (parameters == null || parameters.isEmpty()) {
			return "";
		}

		SortedMap<String, String> sorted = new TreeMap<String, String>();

		Iterator<Map.Entry<String, String>> pairs = parameters.entrySet().iterator();
		while (pairs.hasNext()) {
			Map.Entry<String, String> pair = pairs.next();
			String key = pair.getKey();
			String value = pair.getValue();
			sorted.put(HttpUtils.urlEncode(key, false), HttpUtils.urlEncode(value, false));
		}

		StringBuilder builder = new StringBuilder();
		pairs = sorted.entrySet().iterator();
		while (pairs.hasNext()) {
			Map.Entry<String, String> pair = pairs.next();
			builder.append(pair.getKey());
			builder.append("=");
			builder.append(pair.getValue());
			if (pairs.hasNext()) {
				builder.append("&");
			}
		}

		return builder.toString();
	}

	/**
	 * 署名の生成
	 */	
	protected static String getStringToSign(String scheme, String algorithm, String dateTime, String scope, String canonicalRequest) {
		String stringToSign =
				scheme + "-" + algorithm + "\n" +
				dateTime + "\n" +
				scope + "\n" +
				BinaryUtils.toHex(hash(canonicalRequest));
		return stringToSign;
	}

	/**
	 * Stringデータを元にハッシュ化
	 */
	public static byte[] hash(String text) {
		MessageDigest digest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_TYPE);
		return digest.digest(text.getBytes(StandardCharsets.UTF_8));
	}

	/**
	 * Byteデータを元にハッシュ化
	 */
	public static byte[] hash(byte[] data) {
		MessageDigest md = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_TYPE);
		md.update(data);
		return md.digest();
	}

	/**
	 * 指定された文字列をHMACアルゴリズムで暗号化
	 */
	protected static byte[] sign(String stringData, byte[] key) {
		byte[] data = stringData.getBytes("UTF-8");
		Mac mac = Mac.getInstance(MAC_ALGORITHM_TYPE);
		mac.init(new SecretKeySpec(key, MAC_ALGORITHM_TYPE));
		return mac.doFinal(data);
	}
}

 

public class AWS4SignerForAuthorizationHeader extends AWS4SignerBase {

	private static final String ALGORITHM = "HMAC-SHA256";
	private static final String SCHEME = "AWS4";
	private static final String TERMINATOR = "aws4_request";

	public AWS4SignerForAuthorizationHeader(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
		super(endpointUrl, httpMethod, serviceName, regionName);
	}

	/**
	 * 署名生成処理の呼び出し元
	 */
	public String computeSignature(Map<String, String> headers, Map<String, String> queryParameters, String bodyHash, String awsAccessKey, String awsSecretKey) {
		Date now = new Date();
		String dateTimeStamp = dateTimeFormat.format(now);

		headers.put("x-amz-date", dateTimeStamp);
	
		String hostHeader = endpointUrl.getHost();
		int port = endpointUrl.getPort();
		if (port > -1) {
		    hostHeader.concat(":" + Integer.toString(port));
		}
		headers.put("Host", hostHeader);

		String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
		String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
		String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
		String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod, canonicalizedQueryParameters, canonicalizedHeaderNames, canonicalizedHeaders, bodyHash);
	
		String dateStamp = dateStampFormat.format(now);
		String scope =  dateStamp + "/" + regionName + "/" + serviceName + "/" + TERMINATOR;
		String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope, canonicalRequest);

		byte[] kSecret = (SCHEME + awsSecretKey).getBytes();
		byte[] kDate = sign(dateStamp, kSecret);
		byte[] kRegion = sign(regionName, kDate);
		byte[] kService = sign(serviceName, kRegion);
		byte[] kSigning = sign(TERMINATOR, kService);
		byte[] signature = sign(stringToSign, kSigning);
	
		String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
		String signedHeadersAuthorizationHeader = "SignedHeaders=" + canonicalizedHeaderNames;
		String signatureAuthorizationHeader = "Signature=" + BinaryUtils.toHex(signature);

		String authorizationHeader = SCHEME + "-" + ALGORITHM + " "
				+ credentialsAuthorizationHeader + ", "
				+ signedHeadersAuthorizationHeader + ", "
				+ signatureAuthorizationHeader;

		return authorizationHeader;
	}
}
参考

docs.aws.amazon.com

docs.aws.amazon.com

 

わかったこと

今回は苦肉の策で自前で実装しましたが、認証周りは複雑なためAWSからもSDKの使用が推奨されております。

フレームワークやライブラリはこまめにバージョンアップを行なうよう、仕組みづくりをしていきます。

 

おまけ

S3のプロトコル制限

開発期間中はまだAWS側でTLSv1.2の制限が掛かっていなかったため、バケットに以下のポリシーを設定して意図的に制限を掛けておりました。

s3:TlsVersionの箇所を変更すれば、任意のプロトコルに制限を掛けることが可能です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
"Action": "s3:GetObject", "Principal": "*", "Resource": "arn:aws:s3:::{S3のバケット名}/*", "Condition": { "StringNotEquals": { "s3:TlsVersion": "1.2" } } } ] }
参考

docs.aws.amazon.com

 

ポリシーが適用されてされているかの確認

SSLContextなどで任意の通信プロトコルを指定する方法も検討しましたが、サクっと確認したかったのでFireFoxの設定を変更して確認することにしました。

なお、通信プロトコル変更した状態での通信はデータ漏洩に繋がる危険がございますので、使用の際は自己責任でお願いいたします。

 

事前設定

1. アドレスバーに「about:config」と入力、Enter

2. 危険を承知の上で使用しましょう

3. アドレスバーに「security.tls.version」と入力

4. 「security.tls.version.max」と「security.tls.version.min」を任意の値にする

本来はブラウザ側が最適なプロトコルを選択して接続してくれますが、これを固定化して検証ツールとして用いることも可能です。なお、各値とプロトコルの紐付けは以下となっております。

数値 プロトコル
1 TLS 1.0
2 TLS 1.1
3 TLS 1.2
4 TLS 1.3

 

検証

非対応プロトコルでの接続が制限されることを確認

 

対応プロトコルでの接続に成功することを確認

 

作業終了後は元の値(2023年現在で「max: 4」「min: 3」)に戻しておきましょう。

偽ECサイトの存在、その対策について

株式会社アップガレージグループ ITソリューション事業部でCrooooberをはじめとしたWebサイトの開発・運用をしています、加藤と申します。

今回は、最近よく検索結果で偽物ECサイトを見るケースが増えたため、注意喚起も兼ねて記事を公開いたします。

 

はじめに

Google検索で表示される検索結果で、このようなサイトをご覧になったことは無いでしょうか?

 

2023年5月現在、これらのような通常のECサイトのような見た目で、顧客の個人情報取得や詐欺を狙っているWebサイトが蔓延しております。

 

消費者庁 - インターネット通販トラブル

 

ECサイトに限らず、2022年10月には「えきねっと」の偽サイトがGoogle検索トップに表示された事態が記憶に新しいと思います。

www.yomiuri.co.jp

 

偽EC西見分け方

本来の価格を大きく下回る価格での販売

  • 「95%OFF」といった、原価無視の通常有り得ない値引き率で商品を掲載している

URL、ドメイン

  • 日本のサイトにも関わらず、「fr」「cn」「de」など海外のTLDが設定されている

不自然なタイトル

  • 例えば自動車のホイール販売ページなのに、「出産祝い」「美容」など、通常結びつかない不自然なキーワードがタイトルに記載されている(SEO的に強いキーワードを過剰に用いている)

規約面の不正

  • 会社概要
    • 記載されている会社名が存在しない
    • メールアドレスが法人用ではなく、gmailhotmailなどが記載されている
  • 利用規約特定商取引法に関する表記
    • そもそも記載が無いなど

弊社ECサイトにおける影響について

弊社ECサイトであるCrooooberでも、画像と商品情報を不正に取得、無断転載されるケースが発生しておりました。

上記サイトは弊社側で対策済みで、その後も発生ベースで対策を行なっておりますが、新しいサイトが出現したりとイタチごっこであるのが現状です。

対策について

  • 「見分け方」にあるような不審なサイトは利用しない
  • 初めてアクセスしたサイトの場合、運営母体の企業に信頼性があるかを確認する
  • セキュリティソフトやファイアウォールの有効化、アプリのバージョンを最新に保つ

そもそも検索結果や広告側の審査を厳しくして欲しいというのが個人の意見ですが、それが実現されるにはGoogle側のSEO改善や総務省側の指導を待つ必要がありそうです...

被害に合わないためにも、まずは個人単位での注意をお願いいたします。