こんにちは。21年4月に新卒として入社しましたエンジニアの黒田、宮崎、野田と申します。
4月から新卒研修が始まり、社会人の基礎やプログラミングの基礎的な事柄を学びました。5,6月はエンジニアチームのマネージャーの元、実践的な応用研修に取り組みました。そして6月末から約1ヶ月間、デザイナーとの合同研修を行いました。
前回の新卒合同研修デザイナー編に続いて、今回はエンジニア目線で作成したブログシステムについて振り返りたいと思います。実際のアプリケーションのリンクはこちらになります。
目次
要件定義
技術選定
フロントエンドにはUIを細かい単位に分割しつつ、静的型定義によりエラーを減らしながら開発できることからReact+TypeScriptとし、スタイルにはReactと相性が良く、提供されているコンポーネントが豊富なMaterial UIを使用しました。
バックエンドでは、フレームワークには現在まで社内で多く利用されており、ドキュメントが豊富なLaravel、データベースにはMySQLを用いました。
作成する機能
このサービスを実現するために必要なオブジェクトを抽出し、それらに関連する機能をそれぞれ洗い出しました。
データベース設計
下の画像は作成する機能を元に設計したER図を簡略化したものです。
ER図とは、データベースの関係性を表した設計図のことです。
テーブルの中身にユーザー名や記事の本文、ペットのプロフィール情報などのデータを保存します。そして、このER図から、主にusers、posts、petsの3つのオブジェクトに分けられると考えました。そして、その3つのオブジェクトを3人(黒田、宮崎、野田)で役割分担しました。
実装の説明
いいねボタン(黒田)
記事にはいいねボタンがついていてユーザーがクリックするとハートの中が黄色く塗り潰されます。ボタンの横にはお気に入りの合計数が表示されます。
お気に入りにボタンに関するフロント側のソースコードを下記に載せました。LikeButtonコンポーネントはlikeでお気に入りされているか、されていないかの状態を、numberOfLikesでお気に入りの合計数の状態を管理しています。likeの初期値はfalse(お気に入りされていない状態)、numberOfLikesの初期値はprops.likes.length(propsで渡された、記事についているいいねの数)としています。useEffectでは記事にいいねを押した人の中に自分(ログインしているユーザー)が入っていれば、likeの値をtrueに変えてボタンを黄色く塗りつぶした状態で表示される処理を行っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
export const LikeButton: React.FC<Props> = (props) => { const [like, setLike] = useState(false) const [numberOfLikes, setNumberOfLikes] = useState<number>(props.likes.length) const createLike = async () => { await Axios.post(`/api/like/${props.postId}`) setLike(true) props.setNumberOfLike(numberOfLikes + 1) setNumberOfLikes(numberOfLikes + 1) } const deleteLike = async () => { await Axios.delete(`/api/like/${props.postId}`) setLike(false) props.setNumberOfLike(numberOfLikes - 1) setNumberOfLikes(numberOfLikes - 1) } const handleClick = () => { if (!like) { createLike() return } deleteLike() } useEffect(() => { for (const likes of props.likes) { if (likes.user.id === props.userId) { setLike(true) return } } }, [props.userId]) return ( <> <IconButton aria-label="like" onClick={() => handleClick()} color="primary"> {like ? <FavoriteIcon color="primary" /> : <FavoriteBorderOutlinedIcon color="primary" />} </IconButton> </> ) } |
画像アップロード(宮崎)
今回作成したブログシステムでは、記事作成関連の機能の実装を担当しました。作成した機能の中から、サーバーサイドでの画像アップロードの実装方法を解説したいと思います。
全体の流れ
- モデル、マイグレーション、コントローラファイルを作成
- サーバーサイドでの画像ファイルアップロードの機能の記述
画像アップロードのイメージ
- フロントエンド側で画像ファイルをアップロード(smilingdog.dog.png)
- サーバ側の指定したディレクトリ(post_images/smilingdog.png 等)にファイルが移動
(ローカルのストレージに画像ファイルを保存していく)
- データベースに画像のファイル名(smilingdog.png 等)のみを保存
以下の手順に沿って画像処理の機能を実装しました。
- 画像のサイズが条件を満たしていたら、次の処理、条件を満たさない場合エラー処理
- 画像のファイル名をユニークな名前に変更
- 画像ファイルをpublic/post_images に格納
- ブログ関連画像を登録(Post_Imageテーブル)
また、今回はローカルのストレージに画像を保存するということで、mime type validation ruleを使って画像ファイルのサイズを2048バイト以上は登録ができないよう、検証しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public function store(Request $request ){ //バリデーション if($request->hasFile('post_images')){ $validator = Validator::make($request->all(), [ 'post_images' => 'required', //画像の最大サイズを2048に設定 'post_images.*' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048', ]); //バリデーションエラー発生した場合 if($validator->fails()){ return response()->json(["status" => "failed" , "message" => "Validation Error" , "errors" => $validator->errors()]); } //関連画像を一枚ずつ処理 foreach($request->file('post_images') as $post_image) { //画像のファイル名をユニークな名前に変更→public/post_images/ にファイルを保存(移動) $filePostImagename = $post_image->getClientOriginalName().'.'.time().rand(1,20). '.'.$post_image->getClientOriginalExtension(); $post_image->move('post_images/', $filePostImagename); //Post_Imageテーブルに画像のファイル名を保存 Post_Image::create([ 'image_name' => $filePostImagename, 'post_id' => $post->id ]); } } return $post ? response()->json($post , 201) : response()->json([] , 500); } |
犬種の選択機能(野田)
ペットの登録では、アイコンや名前、性別、犬種などのプロフィール情報を設定することができます。その中でも特に難しかった部分は、犬種の選択機能です。
犬種のデータはみんなの犬図鑑(https://www.min-inuzukan.com/)を参考して作成しました。犬種はプルダウンで選ぶ予定でしたが、全部で131種類もあるため、1つのプルダウンでは選択肢が多すぎて選びづらくなることが予想できました。よって、まず最初に犬のサイズを選び、選択した犬のサイズに応じた犬種を2つ目のプルダウンの選択肢とすることで、ユーザーにとって操作しやすいように工夫しました。
犬のサイズや犬種のデータは、Laravelのシーダーを用いてあらかじめデータベースに登録しておきます。シーダーとは、データベースにデータを一斉挿入する操作のことです。
プルダウンの選択肢に犬のサイズ名や犬種名を表示させるため、APIでデータベースからデータを取得します。取得したデータは配列の中にidとnameというキーを持ったオブジェクトが複数個ある状態なので、mapメソッドで全ての要素を呼び出します。
サイズが選択されるとhandleSizeChangeメソッドが実行され、サイズに応じた犬種のデータを取得して犬種名を2つ目のプルダウンに表示します。
プルダウンの実装にはMaterial UIのNativeSelectコンポーネントを使用しました。Selectコンポーネントは他にも種類がいくつかあり、<InputStyle />のスタイルを変更することでフォントや枠線の色なども変えられるので便利でした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{/* 犬をサイズを選ぶ(大型犬, 中型犬, 小型犬, 超小型犬) */} <FormControl className={classes.selectElem}> <NativeSelect id="demo-customized-select-native-1" input={<InputStyle />} onChange={handleSizeChange} style={{ fontFamily: "sans-serif" }} > <option aria-label="None" value="" style={{ fontFamily: "sans-serif" }}/> {sizes.map((size) => ( <option key={size.id} value={size.id} style={{ fontFamily: "sans-serif" }}>{size.name}</option> ))} </NativeSelect> </FormControl> {/* 犬種を選ぶ */} <FormControl> <NativeSelect id="demo-customized-select-native-2" input={<BootstrapInput />} onChange={handleBreedChange} > <option aria-label="None" value="" /> {breedArray.map((breed: any) => ( <option key={breed.id} value={[breed.id, breed.name]}>{breed.name}</option> ))} </NativeSelect> </FormControl> |
研修の感想
今回、私たち3人にとってはじめてのWebアプリケーションの開発を行いました。今回の開発で良かった点や反省点がいくつか見つかったのでそれらを紹介したいと思います。
良かった点として頻繁なコミュニケーションをとることにより、エラーの共有や実装の相談をこまめに行えたため、効率よく開発することができました。反省点としては、機能を実装できるまでの工数の判断が難しく、ガントチャートの内容を実際かかる時間より短く設定してしまいました。その結果、開発が期日ぎりぎりまでかかってしまいテストがしっかり行えなかったことがあげられます。また、事前にコーディング規則を決めなかったことで、ファイル名やスタイルの当て方が3人ばらばらになってしまいました。今後はこれらの良かった点や反省点を活かして業務に取り組んでいきたいです。
最後に
最初は不安でいっぱいでしたが、最終的にはチームで満足のいくWebアプリケーションが作成でき、反省点や学びも多い充実した研修でした。現在は新卒研修が終了し、3人それぞれプロジェクトに入って業務に取り組んでいます。