アプリ音痴の両親のためにLINEで共有した写真をデジタルフォトフレーム風にするアプリを作った話

この記事は[ぷりぷりあぷりけーしょんず Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ppap)の22日目の記事です。 

# はじめに

大遅刻スミマセン。、
まさかの年越してからアドベントカレンダーに登録するという最早アドベントカレンダーとはなんぞやな状況です。
師走の風に流され、忙しさのあまり年内に完成させ記事にする予定のアプリが新年初コミットとともに完成に至る形になりました。

さて、年を越したと同時にエンジニアに転職して丸3年が経ちました。
1年半ほど前は`丸3年経つまでにサーバー・ネットワークの設定からアプリの開発、何かしらのサービスの一般公開をしたい`というのを目標に業務とは別に個人学習、開発を行なっていました。
結果的に`サーバー・ネットワークの設定`,`何かしらのサービスの一般公開`というのは叶うことはなかったわけですが個人的に家族のための`使うことができる`レベルのアプリを作成することが叶いました。

# 作ったもの

私の両親はまあアプリ音痴でインスタグラムはもちろんのこと、LINEがギリギリ使えるレベルです。その割に父親はガジェット好きでiPadはいっちょ前に４台持っています。
いっちょ前にリビングに立て掛けてあるiPadを有効活用してもらいたいと思いLINEだけで完結するデジタルフォトフレームアプリ`famiphoto`(famip○rtからパクったとかは言わない)を作成しました。

![DSC_2667.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227898/e28613ef-7802-cd18-872e-033b54e13057.jpeg)


## アーキテクチャ

![Untitled Diagram (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227898/09ed82b1-45c8-8fe4-8454-421b1cfd0af6.jpeg)

ざっくり上記のようになっており、アプリのフローとしては以下の通りです。

1. LINEグループにMessagingAPIで作成したbotを招待  
UUIDを作成し、グループIDをキーにDynamoDBに保存、S3にUUIDのディレクトリを作成
2. **今まで通り**画像を共有(ここ重要)  
グループIDからUUIDを取得し、画像をUUIDのディレクトリに保存
3. `famiphoto url`とメッセージを送るとデジタルフォトフレームのurlを返す  
urlは`https://***firebase.app/family/{uuid}`
4. urlにアクセスするとデジタルフォトフレームとして起動しリビングに飾る  
裏では`{uuid}`をリクエストパラメータにAPIGatewayからLambdaを起動し、S3の画像URL一覧を取得してる
5. PWAなのでホームに落としてあげれば、普通にiPad使いたいときは普通に使ってもらい、飾っておくときはアプリを起動して置くだけで良い

使用している技術は
- AWSLambda Runtime-Go
- DynamoDB
- S3
- Firebase hosting
- Vue.js
- PWA

AWS側のインフラ周りはCDKで管理しておりTypescriptで記述しています。

# 実装

実装の面で特別記述するような話はないのですが、初めて挑戦したような箇所もありそれぞれでハマった所、苦労した所を自分へのメモとして残しておきます。
もし参考になれば幸いです。

## CDK

APIGateway, Lambdaを使ったCDKは何度か構築したことがありますが今回初めて`クエリパラメータ`使うことになりました

```typescript:stack.ts
    // API Gateway
    const api = new apigateway.RestApi(this, `api`, {
      restApiName: `api`,
    })

    // Lambda
    const lambdaHandler = new lambda.Function(this, `api`, {
      code: lambda.Code.fromAsset("../bin"),
      handler: "main",
      runtime: lambda.Runtime.GO_1_X,
    })

    const lambdaHandlerIntegration = new apigateway.LambdaIntegration(lambdaHandler, {proxy: true})
    api.root.addResource(`api`)
    api.root.addMethod("GET", lambdaHandlerIntegration, {
      requestParameters: {
        "method.request.querystring.{parameter_name}": true
      }
    })
```

一部を適当に抜粋していますが大体上記の通り設定していきます。  
`apigateway.root.addMethod`のoptionに`requestParameters`を定義し、`"method.request.querystring.{parameter_name}"`でクエリパラメータを設定できます。
今回自分の場合は`uuid`で処理を行っていくので`"method.request.querystring.uuid"`となります。

lambda側で取得する際は以下の通りです

```go:lambda.go
func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	uuid = req.QueryStringParameters["uuid"])
    ....
}
```

## Lambda × Go

LambdaでのGoというより、Lambdaでのline-sdk-goの話ですがこれは以前の記事でも書かせていただきました。

[APIGatewayProxyRequestをline-bot-sdk-go/linebot.Eventにパース](https://qiita.com/aoking_Ba/items/ee65d15acd2ddc45fb8a#apigatewayproxyrequest%E3%82%92line-bot-sdk-golinebotevent%E3%81%AB%E3%83%91%E3%83%BC%E3%82%B9)

それ以外にもBodyを返す際にちょいと苦労（[ドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-programming-model-handler-types.html)読めば良い話だったわけですが）したので何をしてるかそのまま転載します。

```go:controller.go

type ContentsResponse struct {
	Body []string `json:"contents"`
}

func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	contents, err := c.photo.GetContentURLs(req.QueryStringParameters["uuid"])
	if err != nil {
		err = fmt.Errorf("Failed get contents: %v ", err)
		return errorResponse(err), err
	}

	body, err := json.Marshal(&ContentsResponse{
		Body: contents,
	})
	if err != nil {
		return events.APIGatewayProxyResponse{}, err
	}

	return events.APIGatewayProxyResponse{
		StatusCode:      200,
		Body:            string(body),
		IsBase64Encoded: false,
	}, nil
}
```

## PWA

フロントの知識がほぼ皆無な私が今回PWAを選定した理由はアプリのフローであげている`今まで通り画像を共有(ここ重要)`してもらい`普通にiPad使いたいときは普通に使ってもらい、飾っておくときはアプリを起動して置くだけで良い`ようにしたかったからです。

PWAのアプリをFirebaseでhostingする記事はググればたくさん出てくるのでハマることは特にありませんでした。（ありがとうございます）

firebaseにhostingする際はbuild時に生成される`dist`を参照するように設定されているわけですがURLは先述の通り`https://***firebase.app/family/{uuid}`となります。
これはuuidで画像のURLのリストを取るためなのですがアプリごとにuuidを切り替えられるようにしたいためPWAとして起動する際の`start_url`を動的に変更する必要があります。

[PWAのmanifest.jsonを動的に生成する](https://qiita.com/yjtakahagi/items/c247223ca35f5bf1f310)
↑こちらの記事を参考にさせていただき、ほぼそのまま使わせていただきました。
しかしこれだとbuildするたびに`dist/index.html`が毎回書き変わってしまい、`<link rel=manifest>`にidを付与する必要ができるため下記のようなスクリプトを用意してbuild時はこれを使うようにしています。

```sh:build.sh
#bin/bash
npm run build
sed -i  's/href=\/manifest.json/id="my-manifest"/g' ./dist/index.html
```

# 終わりに
個人開発というとどうしても途中で投げ出してしまったり、アイデアで出しで設計終わったらそれっきりだったり、設計拘ってたら飽きたりと色々続かないことが多いです。
今回の開発で思ったのは

1. 初挑戦は1つ〜2つくらい（規模によると思う）
2. 今までの知識のアップデートを中心に
3. 時間のかかりそうな箇所は得意分野で
4. わからなそうな領域はググってコピペでいいから終わらせる

というのがアプリを作る際のモチベーション維持の要になるように思います。
今回の私のアプリでいうと

1. PWA
2. Go, Vue.js
3. CDK, Firebase
4. 画像のフルスクリーン表示やスライドショーぽくするところ

といったところでしょうか。

エンジニア丸3年経過時の目標へのコミットは残念ながら届きませんでしたが全くの未経験から相談なしにエンジニアへの転職をしてやって見せてやることはできたかと思います。（父親は令和の世を生きる人間とは思えない程固い人間）

ここまで作ることができ、あとはやる気次第だと思うので２０２０年のアドベントカレンダーでは一般公開したサービスの記事を書けるように変わらずマイペースに個人開発をしていきたいと思います。

# 参考

[Vue CLI 3 で PWA チュートリアル（Service Workers / Add to Home Screen / Push Notifications）](https://qiita.com/n11sh1/items/5d64c337ef927ac8d5d6)
[とっても簡単！Vueでフルスクリーン機能を実装する](https://blog.capilano-fw.com/?p=4882)