【人材育成Webサービス】楽に管理 ＆ 仲間意識を持てるように【飲食・小売など店舗運営】

![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/306bced3-6662-1b77-e6be-37b98d07b951.jpeg)


こんにちは！どんどです。人材育成サービスを作りました。
<blockquote class="twitter-tweet">
<p dir="ltr" lang="ja">業務サービス作りました！
飲食・小売などの現場やマネジメントのパフォーマンスを上げるようなサービスをと思い、制作。現場の写真を、コメント付きで共有。コメントしたり、いいねしたりできます。
技術：Laravel・Vue・Vuex・VueRouter・SCSS・FLOCSS・レスポンシブ<a href="https://t.co/cCIA77JSXD">https://t.co/cCIA77JSXD</a> <a href="https://t.co/GjBhr4UPcy">pic.twitter.com/GjBhr4UPcy</a></p>
— イド idoharu (@idoharu_com) <a href="https://twitter.com/idoharu_com/status/1282512483006087168?ref_src=twsrc%5Etfw">July 13, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote>業務サービス作りました！
飲食・小売などの現場やマネジメントのパフォーマンスを上げるようなサービスをと思い、制作。現場の写真を、コメント付きで共有。コメントしたり、いいねしたりできます。
技術：Laravel・Vue・Vuex・VueRouter・SCSS・FLOCSS・レスポンシブ</blockquote>
<span class="b">サービス「aibou」のURLとコード（Github）</span>
<div class="surrounded">
<ul>
 	<li>URL：<a href="https://aibou.don-do.com" target="_blank" rel="noopener">https://aibou.don-do.com</a></li>
 	<li>Github：<a href="https://github.com/don-do/aibou.git" target="_blank" rel="noopener">https://github.com/don-do/aibou.git</a></li>
</ul>
</div>
<span class="b">テスト用アカウント</span>も作ってあるので、ログインしてみるだけもできます。よかったら見てみて下さい。

<span class="b">テスト用アカウント</span>
<div class="surrounded">
<ul>
 	<li>Eメール　test@test.ne.jp</li>
 	<li>パスワード　testtest</li>
</ul>
</div>
サービス名は「aibou」。


組織にて、コミュニケーションをとりつつ、楽に管理し成長していけるサービスを作りたいなと思っていました。

学習塾にて責任者をしていた経験があり、各店舗の運営を通して社員やアルバイトの育成に多くの力を注いでいたので、人材育成と社員の協働の大切さを肌で感じてきました。

また、飲食店や雑貨店、スーパーなどの店舗運営においても、<span class="b">お客さんの目線に</span>立って商売を成り立たせることは重要です。

組織にとっては、仕事に対する<span class="b">意識や考え方を統一</span>することがパフォーマンスの向上につながる。

情報を共有しつつ、マネージャーやスタッフを育てていくことで一体感も生まれます。そんな人材育成サービスとして作りました。

働く人同士のコミュニケーションにおいて、意識した点は主に２つ。コメントとグッジョブ機能（Twitterのリプライ機能と、いいね機能のようなもの）。

ログインすると詳細画面では、コメントの投稿をすることができます。
![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/20edd3df-b6fa-33bf-8c07-b42b719320f1.jpeg)

また、グッジョブ機能を使って、良い仕事をした人にポイントを付与。

![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/5311d4b6-817d-3ac5-5e00-88874c39b364.gif)


一覧、ユーザー登録・ログイン・ログアウト、詳細画面、画像とコメント投稿、コメント投稿と削除、グッジョブ付与と解除までが基本的な機能です。

以下は、当記事の内容です。
<div class="surrounded">
<ul>
 	<li><span class="b">サービス概要：</span>ユーザー登録〜店舗画像の投稿とコメント共有など</li>
 	<li><span class="b">サービスを作った理由：</span>現場を共有し、思考のズレを取り除く。仕事仲間を、相棒のような関係にしたい</li>
 	<li><span class="b">使用技術</span></li>
 	<li><span class="b">サービスの作成にどのように取り組んだか：</span>機能の洗い出しとコンセプトに合わせたデザインに</li>
 	<li><span class="b">工夫したところ・難しかったところ：</span>サービスのUIの工夫と、VPSへのLaravelデプロイ・本番環境（現在はレンタルサーバーに移行）</li>
 	<li><span class="b">やってみて感じたこと、得られたこと：</span>テストコードを使いながら、不具合に対応</li>
 	<li><span class="b">今後の課題として取り組んでいきたいこと：事業の効率化と事業価値の向上を通じた、社会の効率化へ少しでも働きかけたい</span></li>
</ul>
</div>
<h2>サービス概要：ユーザー登録〜店舗画像の投稿とコメント共有など</h2>
サービス概要について、３点紹介します。
<div class="surrounded">
<ul>
 	<li>ユーザー登録（ログイン・ログアウト）</li>
 	<li>店舗画像投稿と、コメント共有</li>
 	<li>グッジョブ機能</li>
</ul>
</div>
では、それぞれ見ていきます。


<h3>ユーザー登録（ログイン・ログアウト）</h3>
ユーザーを登録し、ログインして使い、終わったらログアウト。

社員・アルバイトなど<span class="b">仕事の関係者のみ登録させることができます</span>。
<h3>店舗画像投稿と、コメント共有</h3>
ユーザーは店舗の様子を投稿。コメントにて報告したり、フィードバックを受けたりします。

![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/350aea50-e6fa-917d-fb07-47d55385a7eb.gif)



担当者名と<span class="b">現場写真の一覧から、画像クリックするとコメント一覧に</span>いくので、一目で現場を共有できます。
<h3>コメント投稿者のみ、自身のコメントを消すことができる</h3>
コメントを投稿した人だけが、自分自身のコメントを消すことができるようにしています。

消す際には、確認のアラートが出るので安心。


![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/026017fd-6f26-6303-ba46-ba45df883640.gif)

このコメント削除機能について、コードや詳細は下記の「工夫したところ・難しかったところ」に記載しています。
<h3>グッジョブ機能</h3>
Twitterのいいね機能と同じです。上司と部下だけでなく、同僚も押せるので、グッジョブをたくさんもらえると嬉しいです ^^
<h2>サービスを作った理由：現場を共有し、指導。仕事仲間を、相棒のような関係にしたい</h2>

![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/c38ac7d6-fec1-ce66-566f-00afbf0b5cd2.jpeg)

ただの仕事の関係者というドライなものではなく、<span class="b">同じ理想を目指す相棒に</span>したいと思いました。

お互いの理想をリスペクトして、仕事をいくことで良い結果につながると考えているからです。

経営者・上司の理想とする仕事の結果と、スタッフの理想とする仕事の結果がすり合わされることで、同じ理想を追求する同志となります。

「こんな仕事をしたら喜んでもらえるかなあ」というそれぞれの<span class="b">考え方を共有してもらいたい</span>です。
<h2>使用技術</h2>
今回、組織管理サービスを作るということの他に、技術の習得という目的がありました。

使用した技術は、Laravel・Vue（Vuex・VueRouter）・SCSS・FLOCSS。

Laravelは、PHPの<span class="b">MVCフレームワークの習得</span>・DBのテーブル定義のルールにおいて<span class="b">主キーを文字列型とするパターンの習得</span>のために使用。

Vue（Vuex・VueRouter）は、マルチページアプリケーションとは違ったUXを実現でき、大規模なシステム開発において扱われる<span class="b">SPAへの理解を深める</span>ために使いました。

デザインについては、FLOCSSでCSS設計をしつつSCSSでレスポンシブ対応しています。

使用した技術について、初期設定やポイントなどを紹介します。
<h3>Laravel：ルーティングの準備。サービスプロバイダにて、初期処理を設定</h3>
当サービスは、APIと画面のルート定義を別々のファイルに記述しています（APIの<span class="b">ルート定義をapi.phpに</span>記載）。そのため、<span class="b">サービスプロバイダにて、初期処理を設定</span>しました。

app/Providers/RouteServiceProvider.phpにて、ルート定義に適用される<span class="b">ミドルウェアグループを「web」に</span>変更。

```php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
・・・
  
    /**
     * Define the "api" routes for the application.
     *
     * These routes are typically stateless.
     *
     * @return void
     */
    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('web') // 認証系をwebに変更。APIのルート定義を、api.phpにて行う
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }
}
```

今回実装したAPIは、外部のアプリケーションから呼び出されるようなステートレスなWeb APIを使いません。

内部から呼び出して、クッキー認証を行う<span class="b">ステートフルなものにするためミドルウェアグループは画面と同じwebに</span>しています。
<h3>Vue（Vuex・VueRouter）：データ管理・ルーティング</h3>
データの流れを追いやすく、疎結合なコンポーネントを作成するためVuexを導入。npmでライブラリをインストール。

```vue
$ npm install --save-dev vuex
```

resources/js/store　ディレクトリ を作成し、<span class="b">ストアを配置</span>。

SPAを実現するため、VueRouterを使用。npmでインストール。

```vue
$ npm install --save-dev vue-Router
```

ルートコンポーネントresources/js/App.vue内で、にて画面の切り替わる部分を定義。

ルーティングを定義。resources/js/router.jsにて、<span class="b">パスとコンポーネントのマッピング</span>など。
<h3>Web API利用に伴うCSRF対策：クッキーとHTTPヘッダーを利用</h3>
<span class="b">クッキーからトークンを取り出し</span>、<span class="b">HTTPヘッダーのトークンを含め</span>てリクエストを送信します。

resources/js/bootstrap.jsにて、Ajaxリクエストであることを示すX-Requested-Withヘッダーを付与。


```php
// クッキーの値をインポート
import { getCookieValue } from './util'

window.axios = require('axios')

// Axiosライブラリの設定を記述

// Ajaxリクエストであることを示すヘッダー（X-Requested-With）を付与
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

// フォームではなく、ヘッダーを見てCSRFトークンチェックを行うようにする
window.axios.interceptors.request.use(config => {
  // クッキーからトークンを取り出してヘッダーに添付。トークンをX-XSRF-TOKENヘッダーに含める
  config.headers['X-XSRF-TOKEN'] = getCookieValue('XSRF-TOKEN')

  return config
})

```

クッキーから取り出したトークンをX-XSRF-TOKENヘッダーに含めることで、

Laravelがフォームではなく、<span class="b">ヘッダーを見てCSRFトークンチェック</span>を行うようにさせます。
<h3>FLOCSS</h3>
CSS設計の<span class="b">FLOCSS</span>を採用。こちらも技術習得のためです。

以前に<span class="b">BEM</span>にて、以下のサービスを作っています。


（現在作成中）

今回はCSS設計において主流なものの一つFLOCSSを使い、CSSを作りました。

app.scssに<span class="b">インポートする形で、foundation・layout・objectの各ディレクトリを作成</span>。

<span class="b">プレフィックスにp-、c-、u-</span>などを用いて、objectを分けていきました。

クラスの命名に悩みながら・・^^;
<h3>SCSSにてレスポンシブ対応</h3>
SCSSにて、レスポンシブに対応（ブレークポイント560px未満はスマホ）しています。
<h2>サービスの作成にどのように取り組んだか：機能の洗い出しとデザインを編集</h2>
![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/5d180fcb-6d62-0d80-d819-7e4d3bae807e.jpeg)


設計からUXUIを考えつつ、サービスの実現に向け作っていきました。

DBテーブルは、users・photos（画像）・comments（コメント）・praises（グッジョブ機能）とし、親子関係で紐付け。

また、投稿時は写真と一緒にコメントも投稿できるようにし、<span class="b">仕事現場を報告するという形に</span>しました。

それから、<span class="b">一覧に担当者の名前を表示</span>し、だれが投稿したかすぐに分かるようにしています。

なお、デザインはシンプルで調和を表す緑色。FLOCSSを用いて、CSSを設計しつつ、SCSSにてレスポンシブ化。
<h2>工夫したところ・難しかったところ：サービスのUIの工夫と、VPSへのLaravelデプロイ・本番環境（現在はレンタルサーバーに移行）</h2>
![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/04591a37-aadb-813a-9608-197637962246.jpeg)

<h3>認証機能を使ったハンドリングでの工夫</h3>
<span class="b">ユーザーのみが操作できる部分</span>を工夫した方が、使い勝手が良くなると思いました。

たとえば、上記の「サービス概要」でもgifで紹介していますが、ログインユーザーが<span class="b">自分のコメントのみ削除できる</span>ようにしています。

PhotoDetail.vueコンポーネントにて、<span class="b">ログイン中のユーザーのコメントのみに削除ボタンを表示</span>。そして、削除後、すぐに表示中のコメントを削除するようvue側に指示。

```vue
            <!-- 自分の投稿しか削除できないように。コメントの投稿者名とログインユーザー名が一致する場合のみ、コメント横に削除ボタンを表示 -->
            <div v-if="comment.author.name === username" >
              <form @submit.prevent="delComment(comment)" class="p-form">
                <div class="p-form__button">
                  <button type="submit" class="c-button c-button__del">コメントを削除</button>
                </div>
              </form>
            </div>
・・・
<script>
・・・

export default {
・・・

  methods: {
・・・
  
    async delComment (comment) {

      if(confirm('削除します。よろしいですか？')){ //　削除する前に、アラートで確認

      const response = await axios.delete(`/api/comments/${comment.id}`)

      // 削除してすぐに、表示中のコメントを削除
      const index = this.photo.comments.indexOf(comment) // 削除するコメントの配列が何番目かを取得
      this.photo.comments.splice(index, 1) // 削除するコメントを、１つ分削除
      }
    },
・・・

  }
}
</script>
```

また、PhotoController.phpの削除ロジックにおいても、認証しています。

```php
<?php

namespace App\Http\Controllers;

use App\Photo;
use App\Comment;
use App\Http\Requests\StorePhoto;
use App\Http\Requests\StoreComment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

class PhotoController extends Controller
{
・・・

    /**
     * コメント削除
     */
    public function delComment($comment_id)
    {
        // ログインユーザーに紐づくコメントのみを削除できる
        Auth::user()->userComments()->find($comment_id)->delete();

        return response(204); // NO_CONTENT
    }
```


あとグッジョブ機能も、同じようなロジック。

<span class="b">自分が画像投稿したものには、グッジョブできないように</span>データの受け渡しを工夫しています。
<h3>投稿フォームの表示・非表示のUI</h3>
投稿フォームにおいて、<span class="b">フォーム以外の場所を押したら、フォームを非表示に</span>するようにvueを実装。

![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/721e05d5-d7f9-0f29-6fd3-4a6edcce63e9.gif)

```vue

        <div class="root" ref="elRoot">
          <button class="c-button c-button__report" @click="showForm = ! showForm">
            <i class="c-icon ion-md-add"></i>
            報告へ
          </button>
          <PhotoForm v-model="showForm" />
        </div>

・・・

<script>
import PhotoForm from './PhotoForm.vue'
export default {
  components: {
    PhotoForm
  },
  data () {
    return {
      showForm: false
    }
  },

・・・
        
  mounted() { // フォームの外枠をクリックすると、フォームを閉じる
    // windowにイベントリスナーをセット
    window.addEventListener('click', this._onBlurHandler = (event) => {
      // ログイン状態なら、フォームの表示・非表示の操作を行えるようにする。
      if (this.isLogin) {
        // フォームをクリックしても、何も操作しない。targetがコンポーネントの中に含まれているものならreturn
        if (this.$refs.elRoot.contains(event.target)) {
          return;
        }
      }
      this.$data.showForm = false;
    });
  },
  beforeDestroy() {
    // コンポーネントが破棄されるタイミングにイベントリスナーも消去
    window.removeEventListener('click', this._onBlurHandler);
  }
}
</script>
```

<h3>VPSにて、laravelアプリのデプロイ</h3>
（現在は、VPSからレンタルサーバに移行しています）

.envファイルやindex.phpなどの設定やVPSのドキュメントルートの設定などは当然必要なのですが、それ以外にlaravelをVPSで導入するときは色々あって大変でした。
<ul>
 	<li>VPSへのcomposerのインストール</li>
 	<li>laravelプロジェクト用ディレクトリと、公開用publicディレクトリを分ける</li>
 	<li>Laravelアプリ側と、publicディレクトリとの連携のためシンボリックリンク を貼る</li>
 	<li>画面表示のため、storageディレクトリの権限変更</li>
</ul>
上記のあたりを、ググりながら一つ一つ解決。
<h4>npm install が効かない！</h4>
ここがかなりの詰まりどころで、npm installが効かず、npm run devなどのnpmコマンドでエラーが出るようになってしまいました。

結局、<span class="b">node_modulesを削除して再インストール</span>することでクリアすることができました。
<h3>本番環境にあげれたはいいものの、ページ速度の遅延が気になる。。</h3>
ローカル環境と異なり、ロード時に画面切り替えで遅れることがありました。

ヘッダーとフッター以外が、真っ白になる・・。

そこでローディングコンポーネントを使い回して、ロード中だとわかるようにしました。一覧から詳細や、一覧のページ送り時に<span class="b">ロード画面を差し込み</span>ました。

```vue
<template>
・・・
  
    <div v-show="loading" class="p-photo-detail" style="display: inherit;">
        <!-- Loader.vueテンプレートが当て込まれ、ローディング画面が表示される -->
        <Loader></Loader>
    </div>
  
・・・
</template>

<script>
・・・
import Loader from '../components/Loader.vue' // <Loader> コンポーネントをインポート

export default {
  components: {
    ・・・
    Loader // <Loader> コンポーネントを登録
  },
    ・・・
  data () {
    return {
      ・・・
      loading: false // ローディングを表示させるかどうか
    }
  },
  methods: {
    async fetchPhotos () {
      // ローディングを表示
      this.loading = true

        const response = await axios.get(`/api/photos/?page=${this.page}`)

        if (response.status !== OK) {
          this.$store.commit('error/setCode', response.status)
          return false
        }

      // 通信が終わったら、ローディングを非表示
      this.loading = false

        this.photos = response.data.photos.data // レスポンスのJSON取得（response.data）後、photosの中の配列dataを取得
        // APIのレスポンスから「現在ページ」と「総ページ数」を取り出し、data変数に代入
        this.currentPage = response.data.photos.current_page
        this.lastPage = response.data.photos.last_page
    },
・・・
}
</script>
```


最後にフォームリクエストを使って<span class="b">バリデーションの多言語化対応</span>で整えたという感じです。
<h2>やってみて感じたこと、得られたこと：テストコードを使いながら、不具合に対応していくこと</h2>

![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/cbd8f74e-7829-152d-3668-8cacf1070fbe.jpeg)

テストコードを扱えたことが有意義でした。

例えば、以下は認証状態のテスト。ユーザーを取得するAPIを実装するときに使いました。

```php
<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserApiTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();

        // テストユーザー作成
        $this->user = factory(User::class)->create();
    }

    /**
     * @test
     */
    public function should_ログイン中のユーザーを返却する()
    {
        $response = $this->actingAs($this->user)->json('GET', route('user'));

        $response
            ->assertStatus(200)
            ->assertJson([
                'name' => $this->user->name,
            ]);
    }

    /**
     * @test
     */
    public function should_ログインされていない場合は空文字を返却する()
    {
        $response = $this->json('GET', route('user'));

        $response->assertStatus(200);
        $this->assertEquals("", $response->content());
    }
}
```
コードを作成した後に、不具合があるかどうかがすぐに分かるので、慣れていけばいくほど開発がスムーズになるのを感じました。
<h2>今後の課題として取り組んでいきたいこと：<span class="b">事業の効率化と事業価値の向上を通じた、社会の効率化へ少しでも働きかけたい</span></h2>
![image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675914/5379ca0b-015e-e165-0648-3a8708c75dc8.jpeg)



<span class="b">事業の効率化を通じて、社会の効率化へ</span>働きかけていきたいです。

事業の価値を大きくすることで、余計な負担が減りストレスを軽減していくことができます。集客・単価のUPとコストカットは、ストレスのカットにつながっていきます。

すると、継続的な事業を行えることになります。

コロナの流行を追い風とした働き方改革の中、企業のデジタル化・DX（デジタルトランスフォーメーション）をニュースで見かける機会が増えました。

そんな中、<span class="b">企業価値の向上</span>に向け、プログラミングを使った既存サービスの保守・改修や、新規サービスの開発は行っていきたい課題です。

さらに現在プログラミングに限らず、ノーコードツールやクラウド、AIツールなど、モノの価値を伸ばす道具が世の中に溢れています。

そういったテクノロジーを使いつつ、<span class="b">企業活動の効率化を通じて社会全体の効率化・しくみ化に</span>貢献していけるように取り組んでいきたいです。また、そういった<span class="b">情報発信</span>をしていくことによって価値提供をしていきたいと考えています。

以上となります。最後まで読んでいただきありがとうございます。