こんにちは!どんどです。人材育成サービスを作りました。
業務サービス作りました! 飲食・小売などの現場やマネジメントのパフォーマンスを上げるようなサービスをと思い、制作。現場の写真を、コメント付きで共有。コメントしたり、いいねしたりできます。 技術:Laravel・Vue・Vuex・VueRouter・SCSS・FLOCSS・レスポンシブhttps://t.co/cCIA77JSXD pic.twitter.com/GjBhr4UPcy
— イド idoharu (@idoharu_com) July 13, 2020
業務サービス作りました! 飲食・小売などの現場やマネジメントのパフォーマンスを上げるようなサービスをと思い、制作。現場の写真を、コメント付きで共有。コメントしたり、いいねしたりできます。 技術:Laravel・Vue・Vuex・VueRouter・SCSS・FLOCSS・レスポンシブサービス「aibou」のURLとコード(Github) テスト用アカウントも作ってあるので、ログインしてみるだけもできます。よかったら見てみて下さい。
テスト用アカウント
- Eメール test@test.ne.jp
- パスワード testtest
組織にて、コミュニケーションをとりつつ、楽に管理し成長していけるサービスを作りたいなと思っていました。
学習塾にて責任者をしていた経験があり、各店舗の運営を通して社員やアルバイトの育成に多くの力を注いでいたので、人材育成と社員の協働の大切さを肌で感じてきました。
また、飲食店や雑貨店、スーパーなどの店舗運営においても、お客さんの目線に立って商売を成り立たせることは重要です。
組織にとっては、仕事に対する意識や考え方を統一することがパフォーマンスの向上につながる。
情報を共有しつつ、マネージャーやスタッフを育てていくことで一体感も生まれます。そんな人材育成サービスとして作りました。
働く人同士のコミュニケーションにおいて、意識した点は主に2つ。コメントとグッジョブ機能(Twitterのリプライ機能と、いいね機能のようなもの)。
ログインすると詳細画面では、コメントの投稿をすることができます。
また、グッジョブ機能を使って、良い仕事をした人にポイントを付与。
一覧、ユーザー登録・ログイン・ログアウト、詳細画面、画像とコメント投稿、コメント投稿と削除、グッジョブ付与と解除までが基本的な機能です。
以下は、当記事の内容です。
- サービス概要:ユーザー登録〜店舗画像の投稿とコメント共有など
- サービスを作った理由:現場を共有し、思考のズレを取り除く。仕事仲間を、相棒のような関係にしたい
- 使用技術
- サービスの作成にどのように取り組んだか:機能の洗い出しとコンセプトに合わせたデザインに
- 工夫したところ・難しかったところ:サービスのUIの工夫と、VPSへのLaravelデプロイ・本番環境(現在はレンタルサーバーに移行)
- やってみて感じたこと、得られたこと:テストコードを使いながら、不具合に対応
- 今後の課題として取り組んでいきたいこと:事業の効率化と事業価値の向上を通じた、社会の効率化へ少しでも働きかけたい
サービス概要:ユーザー登録〜店舗画像の投稿とコメント共有など
サービス概要について、3点紹介します。- ユーザー登録(ログイン・ログアウト)
- 店舗画像投稿と、コメント共有
- グッジョブ機能
ユーザー登録(ログイン・ログアウト)
ユーザーを登録し、ログインして使い、終わったらログアウト。社員・アルバイトなど仕事の関係者のみ登録させることができます。
店舗画像投稿と、コメント共有
ユーザーは店舗の様子を投稿。コメントにて報告したり、フィードバックを受けたりします。担当者名と現場写真の一覧から、画像クリックするとコメント一覧にいくので、一目で現場を共有できます。
コメント投稿者のみ、自身のコメントを消すことができる
コメントを投稿した人だけが、自分自身のコメントを消すことができるようにしています。消す際には、確認のアラートが出るので安心。
このコメント削除機能について、コードや詳細は下記の「工夫したところ・難しかったところ」に記載しています。
グッジョブ機能
Twitterのいいね機能と同じです。上司と部下だけでなく、同僚も押せるので、グッジョブをたくさんもらえると嬉しいです ^^サービスを作った理由:現場を共有し、指導。仕事仲間を、相棒のような関係にしたい
ただの仕事の関係者というドライなものではなく、同じ理想を目指す相棒にしたいと思いました。
お互いの理想をリスペクトして、仕事をいくことで良い結果につながると考えているからです。
経営者・上司の理想とする仕事の結果と、スタッフの理想とする仕事の結果がすり合わされることで、同じ理想を追求する同志となります。
「こんな仕事をしたら喜んでもらえるかなあ」というそれぞれの考え方を共有してもらいたいです。
使用技術
今回、組織管理サービスを作るということの他に、技術の習得という目的がありました。使用した技術は、Laravel・Vue(Vuex・VueRouter)・SCSS・FLOCSS。
Laravelは、PHPのMVCフレームワークの習得・DBのテーブル定義のルールにおいて主キーを文字列型とするパターンの習得のために使用。
Vue(Vuex・VueRouter)は、マルチページアプリケーションとは違ったUXを実現でき、大規模なシステム開発において扱われるSPAへの理解を深めるために使いました。
デザインについては、FLOCSSでCSS設計をしつつSCSSでレスポンシブ対応しています。
使用した技術について、初期設定やポイントなどを紹介します。
Laravel:ルーティングの準備。サービスプロバイダにて、初期処理を設定
当サービスは、APIと画面のルート定義を別々のファイルに記述しています(APIのルート定義をapi.phpに記載)。そのため、サービスプロバイダにて、初期処理を設定しました。app/Providers/RouteServiceProvider.phpにて、ルート定義に適用されるミドルウェアグループを「web」に変更。
<?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を使いません。
内部から呼び出して、クッキー認証を行うステートフルなものにするためミドルウェアグループは画面と同じwebにしています。
Vue(Vuex・VueRouter):データ管理・ルーティング
データの流れを追いやすく、疎結合なコンポーネントを作成するためVuexを導入。npmでライブラリをインストール。$ npm install --save-dev vuex
resources/js/store ディレクトリ を作成し、ストアを配置。
SPAを実現するため、VueRouterを使用。npmでインストール。
$ npm install --save-dev vue-Router
ルートコンポーネントresources/js/App.vue内で、にて画面の切り替わる部分を定義。
ルーティングを定義。resources/js/router.jsにて、パスとコンポーネントのマッピングなど。
Web API利用に伴うCSRF対策:クッキーとHTTPヘッダーを利用
クッキーからトークンを取り出し、HTTPヘッダーのトークンを含めてリクエストを送信します。resources/js/bootstrap.jsにて、Ajaxリクエストであることを示すX-Requested-Withヘッダーを付与。
// クッキーの値をインポート
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がフォームではなく、ヘッダーを見てCSRFトークンチェックを行うようにさせます。
FLOCSS
CSS設計のFLOCSSを採用。こちらも技術習得のためです。以前にBEMにて、以下のサービスを作っています。
(現在作成中)
今回はCSS設計において主流なものの一つFLOCSSを使い、CSSを作りました。
app.scssにインポートする形で、foundation・layout・objectの各ディレクトリを作成。
プレフィックスにp-、c-、u-などを用いて、objectを分けていきました。
クラスの命名に悩みながら・・^^;
SCSSにてレスポンシブ対応
SCSSにて、レスポンシブに対応(ブレークポイント560px未満はスマホ)しています。サービスの作成にどのように取り組んだか:機能の洗い出しとデザインを編集
設計からUXUIを考えつつ、サービスの実現に向け作っていきました。
DBテーブルは、users・photos(画像)・comments(コメント)・praises(グッジョブ機能)とし、親子関係で紐付け。
また、投稿時は写真と一緒にコメントも投稿できるようにし、仕事現場を報告するという形にしました。
それから、一覧に担当者の名前を表示し、だれが投稿したかすぐに分かるようにしています。
なお、デザインはシンプルで調和を表す緑色。FLOCSSを用いて、CSSを設計しつつ、SCSSにてレスポンシブ化。
工夫したところ・難しかったところ:サービスのUIの工夫と、VPSへのLaravelデプロイ・本番環境(現在はレンタルサーバーに移行)
認証機能を使ったハンドリングでの工夫
ユーザーのみが操作できる部分を工夫した方が、使い勝手が良くなると思いました。たとえば、上記の「サービス概要」でもgifで紹介していますが、ログインユーザーが自分のコメントのみ削除できるようにしています。
PhotoDetail.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) // 削除するコメントを、1つ分削除
}
},
・・・
}
}
</script>
また、PhotoController.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
}
あとグッジョブ機能も、同じようなロジック。
自分が画像投稿したものには、グッジョブできないようにデータの受け渡しを工夫しています。
投稿フォームの表示・非表示のUI
投稿フォームにおいて、フォーム以外の場所を押したら、フォームを非表示にするように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>
VPSにて、laravelアプリのデプロイ
(現在は、VPSからレンタルサーバに移行しています).envファイルやindex.phpなどの設定やVPSのドキュメントルートの設定などは当然必要なのですが、それ以外にlaravelをVPSで導入するときは色々あって大変でした。
- VPSへのcomposerのインストール
- laravelプロジェクト用ディレクトリと、公開用publicディレクトリを分ける
- Laravelアプリ側と、publicディレクトリとの連携のためシンボリックリンク を貼る
- 画面表示のため、storageディレクトリの権限変更
npm install が効かない!
ここがかなりの詰まりどころで、npm installが効かず、npm run devなどのnpmコマンドでエラーが出るようになってしまいました。結局、node_modulesを削除して再インストールすることでクリアすることができました。
本番環境にあげれたはいいものの、ページ速度の遅延が気になる。。
ローカル環境と異なり、ロード時に画面切り替えで遅れることがありました。ヘッダーとフッター以外が、真っ白になる・・。
そこでローディングコンポーネントを使い回して、ロード中だとわかるようにしました。一覧から詳細や、一覧のページ送り時にロード画面を差し込みました。
<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>
最後にフォームリクエストを使ってバリデーションの多言語化対応で整えたという感じです。
やってみて感じたこと、得られたこと:テストコードを使いながら、不具合に対応していくこと
テストコードを扱えたことが有意義でした。
例えば、以下は認証状態のテスト。ユーザーを取得するAPIを実装するときに使いました。
<?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());
}
}
コードを作成した後に、不具合があるかどうかがすぐに分かるので、慣れていけばいくほど開発がスムーズになるのを感じました。
今後の課題として取り組んでいきたいこと:事業の効率化と事業価値の向上を通じた、社会の効率化へ少しでも働きかけたい
事業の効率化を通じて、社会の効率化へ働きかけていきたいです。
事業の価値を大きくすることで、余計な負担が減りストレスを軽減していくことができます。集客・単価のUPとコストカットは、ストレスのカットにつながっていきます。
すると、継続的な事業を行えることになります。
コロナの流行を追い風とした働き方改革の中、企業のデジタル化・DX(デジタルトランスフォーメーション)をニュースで見かける機会が増えました。
そんな中、企業価値の向上に向け、プログラミングを使った既存サービスの保守・改修や、新規サービスの開発は行っていきたい課題です。
さらに現在プログラミングに限らず、ノーコードツールやクラウド、AIツールなど、モノの価値を伸ばす道具が世の中に溢れています。
そういったテクノロジーを使いつつ、企業活動の効率化を通じて社会全体の効率化・しくみ化に貢献していけるように取り組んでいきたいです。また、そういった情報発信をしていくことによって価値提供をしていきたいと考えています。
以上となります。最後まで読んでいただきありがとうございます。