GraphQL+AppSyncで「currentObservable.query.getCurrentResult is not a function」と出た時の対処法
GraphQL使い始めてみたのですが、結構つまづいたのでメモ。
GraphQLクライアントとして「Apollo」を使ってみたのですが、素直に使うとどうやらappsyncとはあまり相性がよくなさそう。
詳細
基本的な使い方は下記のような感じでApollo Clientを作成して使用する方法だと思います。
import awsmobile from "./aws-exports"; //appsyncの設定ファイル import AWSAppSyncClient from "aws-appsync"; const client = new AWSAppSyncClient({ url: awsmobile.aws_appsync_graphqlEndpoint, region: awsmobile.aws_project_region, auth: { type: awsmobile.aws_appsync_authenticationType, apiKey: awsmobile.aws_appsync_apiKey, } });
「おお、まとまっててええやんけ!」と思ったのですが、useQueryを使うと「currentObservable.query.getCurrentResult is not a function」のエラーが...
早速検索したところ下記の記事がヒットしました。
なるほど、依存関係を明示的に解決する方法があるのかと下記をpackage.jsonに追加しました。
"resolutions": { "apollo-client": "2.6.4" },
しかし、これを追加したところuseQueryは動くけどuseMutationは使えないというバグに出くわしました。
改めて調べてみたところ、これらのバグは「aws-appsync」が古い「apollo client」を参照しているため起こるバグのようです。
上記に沿ってApollo Clientを一から作成する方法を取ってみました。
import awsmobile from "./aws-exports"; //appsyncの設定ファイル import { ApolloLink } from 'apollo-link'; import ApolloClient from 'apollo-client'; import { createAuthLink } from 'aws-appsync-auth-link'; import { createHttpLink } from 'apollo-link-http'; import { InMemoryCache } from "apollo-cache-inmemory"; const url = awsmobile.aws_appsync_graphqlEndpoint; const region = awsmobile.aws_appsync_region; const auth = { type: awsmobile.aws_appsync_authenticationType, apiKey: awsmobile.aws_appsync_apiKey }; const link = ApolloLink.from([ createAuthLink({ url, region, auth }), createHttpLink({ uri: url }) ]); const client = new ApolloClient({ link, cache: new InMemoryCache(), });
Apollo Clientの初期化は長くなりましたが、AppSyncとApolloを使うにはこうするしかなさそう...
早くアップデートしてもらえるといいなあ
ローカル画像から直接フォームのファイル(type=file)に画像情報を渡す方法
今回も小ネタです。
あんま必要とする人いないかもしれないですが、タイトルに際した状況に出くわしたのでメモ。
概要
画像プレビュー機能を実装する際には、「input(type=file)」のイベントをハンドリングして画像情報を引き出してプレビュー用の要素に画像を下記のように設定するような流れが多いですよね。
const target = document.getElementById('input_file'); const preview = document.getElementById('preview'); target.addEventListener('change', (e)=>{ preview.classList.add('drop'); const FR = new FileReader(); FR.onload = (e) => { preview.querySelector('img').src = e.target.result; }; FR.readAsDataURL(e.target.files[0]); });
FileReaderを使って画像のsrcに設定するわけですが、「 e.target.result」には「readAsDataURL」で生成されたbase64形式の文字列が返ってきます。
さて、こんな感じでプレビュー機能を作っていたわけですが、プロフィール画像の変更が不要な時にファイルのプレビュー出すのはいけるけどそのまま送信したら空になるしアカンなと気づいたわけです。
サーバーサイドで処理させてもいいんですが、ちょっとファイルの扱いがまだ勉強不足だったこともあり、フロントで対処しようとなった次第です。
実装コード
const src = thumb.src; const pos = src.lastIndexOf('.'); if (pos === -1) return ''; const ext = src.slice(pos + 1); // 拡張子取得 fetch(src) .then(res => res.blob()) .then( (blob) => { const file = new File([blob], `image.${ext}`); let list = new DataTransfer(); list.items.add(file); target.files = list.files; })
まあやり方があってるのかはわかりませんが..
上の四行目までは拡張子取得してるだけなので気にしなくていいです。
流れとしては、
1. 画像のURLを叩いてBlobに変換
2. blobから新しくFileオブジェクトを作成
3. DataTransferを作成し、そこに作成したFileオブジェクトを追加
4. ファイルinputのfilesにDataTransferを入れる
といった感じです。
まず、画像のsrcを取得し画像の保管場所を取得してそこをfetchで叩きます。
そうするとレスポンスをblobで取得できる「res.blob」を使ってblobに変換します。
blobはバイナリデータが格納されているImmutableなオブジェクトです。生データが入ってるということですね。
そしてFileオブジェクトはblobを引数に新しいファイルを作成することができます。
ファイルinputのfilesはDataTransferオブジェクトだったので、新しくDataTransferを作って渡している、というわけです。
やっぱサーバーサイドで処理すりゃよかったかな..と思いましたが、勉強になりました。
browserSyncのproxyとは?自動リロードが効かないときは試してみよう
今回はちょっとした小ネタです。
browserSyncってありますよね。あのローカルサーバーを簡単に立てられるやつです。
その中でオプションで「proxy」というものがあるのですがその「proxyってなんだ?」というお話です。
proxyと聞いてすぐに思い浮かぶのはプロキシサーバーとかだと思いますが、まさにこのプロキシサーバーを実現するためのオプションのよう。
ざっとプロキシサーバーの概要を説明するとこんな感じです。(大雑把な理解なので間違ってたらすいません...)
まあ中間管理職みたいな感じですよねきっと。どっちかっていうとエージェント...??
ということで、プロキシサーバは上記のように振る舞うことで、二つのシステムを一つのシステムであるかのように動かすことができるという理解に落ち着きました。
では実際にどのような動作になるのか、ということを確認していきます。
まず、今回のプロジェクトはバックエンド・フロントエンド共にLaravelを使用して作成しています。
環境
- サーバーサイド
Dockerにて、「9000」ポートでLaravelプロジェクトをホスティング
- フロントエンド
「3000」ポートでbrowserSyncを起動
もちろんリロードすれば9000番ポートの方でも変更は反映されるのですが、自動リロード効かないんすよね...
ということでオプション「proxy」の出番です!
コード
mix.browserSync({ files: [ "resources/views/**/*.blade.php", "public/**/*.*" ], proxy: { target: "localhost:9000", } });
browserSyncの設定は上記のみ。
filesは監視対象のファイルですね。
さて、proxyオプションを確認すると「 target: "localhost:9000"」と記載されていますね。
このように指定することで現在Laravelプロジェクトがホスティングされている9000番ポートのシステムと同期し、3000番ポートの方でも同じように9000番ポートの内容が反映されることになります。
そして、browserSyncが起動しているのは3000番ポートの方なのでプロジェクトを編集する際は「localhost:3000」を開きながら編集することで自動リロードもできているはずです。
以上、大雑把な小ネタでした...
GoogleMapsAPIで地図を実装! 基本からカスタムパノラマの実装、パノラマ間の遷移までまとめ
以前初めてGoogleMapの実装案件があったので、備忘録的にメモ。
今回の流れは下記の様な流れで進んでいきます。
1. GoogleMapの簡単な表示例
2. ストリートビューの簡単な表示例
3. GoogleMapにマーカーを立てる・マーカーにイベントを設置
4. カスタムパノラマの作り方
まず、簡単な例からいきましょう。
単なるGoogleMapを表示したい場合、必要となるのは下記のようなコードです。
<div id='map'></div> <script async defer src="https://maps.googleapis.com/maps/api/js?key=[自分のAPIキー]"></script>
#map { width: 100vw; height: 100vh; }
const initMap = ()=> { const latLng = {lat: 35.659063, lng: 139.700641}; const map = new google.maps.Map(document.getElementById('map'), { center: latLng, zoom: 14 }); } initMap();
これで緯度: 35.659063, 経度: 139.700641(渋谷ハチ公)を中心とする地図を表示することができました。
まあでもこのあたりはみなさん知っていると思います。
さて、まずGoogleMapAPIには大きく分けて二つのオブジェクトがあります。
1. GoogleMapオブジェクト
2. StreetViewPanoramaオブジェクト
GoogleMapオブジェクトはみなさんがいつもみているような地図のオブジェクトです。
ではStreetViewPanoramaオブジェクトとはどのようなものなのでしょう?
const initMap = ()=> { const latLng = {lat: 35.659063, lng: 139.700641}; const map = new google.maps.Map(document.getElementById('map'), { center: latLng, zoom: 14 }); const stv = map.getStreetView(); // mapのストリートビュー情報を取得 stv.setPosition(latLng); // ストリートビューの位置を指定 stv.setVisible(true); // ストリートビューの表示をアクティブに変更 } initMap();
先ほど地図を表示できたコードにコメントアウトで説明を入れている三行を追加しました。
こちらのコードを実行すると、ハチ公のストリートビューが表示されているはずです。
では一旦ここで解説です。
まず、GoogleMapオブジェクトからそれに紐づくStreetViewオブジェクトを取得するには「getStreetView()」メソッドを使用します。
これによって、変数stvにはStreetViewオブジェクトが代入されています。
そしてStreetViewオブジェクトで使用することのできるメソッドを使って、位置を指定し表示を切り替えている、というわけですね。
setPositionは入れなくてもいい気がしていたのですが、こちらのメソッドで明示的に位置情報をセットしないといけないよう。
これらのメソッドに関してはSyncerの記事がかなりよくまとまっていたのでぜひ参考にしてみてください。
https://lab.syncer.jp/Web/API/Google_Maps/JavaScript/
まず、GoogleMapとストリートビューの切り替えに関しては基本的にこのような流れで進んでいきます。
では次に、必ずと言っていいほど実装するマーカーの実装について説明していきます。
というのも、今回の案件ではマーカーをクリックするとカスタムパノラマが表示されるという実装もあったためです。
const initMap = ()=> { const latLng = {lat: 35.659063, lng: 139.700641}; const map = new google.maps.Map(document.getElementById('map'), { center: latLng, zoom: 14 }); const marker = new google.maps.Marker({ position: new google.maps.LatLng(latLng.lat, latLng.lng), map: map, title: 'サンプルマーカー', icon: { url: 'img/marker-icon.png', scaledSize: new google.maps.Size(128, 128) } }); } initMap();
このマーカーはもちろんGoogleMapの地図上に表示されるため、マーカーを立てることができるのはGoogleMapオブジェクトに対して、ということになります。
iconプロパティで画像を指定するだけで簡単にアイコン画像を変えることができるのは便利ですね。
ただ、「size」ではなく「scaledSize」にしないとアイコン画像がうまいこと拡縮しないので、そこだけは注意が必要です。
さて、次はイベントの指定です。
const initMap = ()=> { const latLng = {lat: 35.659063, lng: 139.700641}; const map = new google.maps.Map(document.getElementById('map'), { center: latLng, zoom: 14 }); const marker = new google.maps.Marker({ position: new google.maps.LatLng(latLng.lat, latLng.lng), map: map, title: 'サンプルマーカー', icon: { url: 'img/marker-icon.png', scaledSize: new google.maps.Size(128, 128) } }); marker.addListener('click',()=>{ // addListenerなのに注意! alert('clicked marker!'); }); } initMap();
追加したのはコメントが付いている箇所ですね。
コメントにも記載しましたが、addEveneListenerではなくaddListenerであることに注意しましょう。
これはGoogleMap独自のメソッドで、GoogleMapオブジェクトやStreetViewオブジェクトに対してイベントを設定する際にはこのaddListenerメソッドを使うことになります。
では、ここまでの内容で次のような実装をしてみましょう。
- GoogleMapの中心座標は渋谷ハチ公
- ハチ公にはマーカーを立てる
- マーカーをクリックするとハチ公のストリートビューが表示される
const initMap = ()=> { const latLng = {lat: 35.659063, lng: 139.700641}; const map = new google.maps.Map(document.getElementById('map'), { center: latLng, zoom: 14 }); const stv = map.getStreetView(); const marker = new google.maps.Marker({ position: new google.maps.LatLng(latLng.lat, latLng.lng), map: map, title: 'サンプルマーカー', icon: { url: 'img/marker-icon.png', scaledSize: new google.maps.Size(128, 128) } }); marker.addListener('click',()=>{ stv.setPosition(latLng); stv.setVisible(true); }); } initMap();
さて、実装できたはいいもののスポットが一つであることは珍しく、現状のままの実装ではいけません。
では複数のスポットに対応してみましょう。
const initMap = ()=> { let markers = []; // マーカーを格納する配列 const spotList = [ // マーカーの位置情報を格納している配列 { lat: 35.659063, lng: 139.700641, name: 'ハチ公', }, { lat: 35.658625, lng: 139.701057, name: 'モヤイ像', } ]; const map = new google.maps.Map(document.getElementById('map'), { center: {lat: spotList[0].lat, lng: spotList[0].lng}, zoom: 14 }); const stv = map.getStreetView(); spotList.forEach((spot, index) => { //spotListの数だけマーカーを設置、イベントを設定 markers[index] = new google.maps.Marker({ position: new google.maps.LatLng(spot.lat, spot.lng), map: map, title: spot.name, icon: { url: 'img/marker-icon.png', scaledSize: new google.maps.Size(128, 128) } }); markers[index].addListener('click',()=>{ stv.setPosition(markers[index].position); stv.setVisible(true); }); }); }
今回も追加したのはコメントが記載されている部分です。
ポイントとなるのは、位置情報を格納している配列「spotList」とマーカーを格納する配列「markers」ですね。
見ていただけるとわかると思いますが、一旦生成したマーカーをmarkersに格納して、その後にクリックイベントを設定しています。
一旦配列に入れることで後々マーカーに対する操作も行いやすくなるので、この方法はオススメです。
さて、ここまでで一旦GoogleMapのデフォルトの機能である「地図」、「ストリートビュー」、「マーカー」を使うことができたと思います。
ただ、なかなかこれで終わりというわけにはいかないんですよね...
そう、今回のメインとなる「カスタムパノラマ 」の実装について見ていきたいと思います。
カスタムパノラマの情報はGoogleMapAPIのドキュメント上に載っているのには載っているのですが、かなり情報が少なかったです...
とりあえず、カスタムパノラマ を表示してみましょう。
<div id='pano'></div> <script async defer src="https://maps.googleapis.com/maps/api/js?key=[自分のAPIキー]"></script>
const initMap = ()=> { const customStv = new google.maps.StreetViewPanorama( document.getElementById('pano'), { pano: 'custompanorama', }); customStv.registerPanoProvider(() => { return { location: { pano: 'custompanorama', description: 'カスタムパノラマ', latLng: new google.maps.LatLng(35.6595126, 139.7005696) }, links: [ { heading: 250, description: 'カスタムパノラマ2', pano: 'custompanorama2' } ], copyright: '©︎2019 Google 日本', tiles: { tileSize: new google.maps.Size(1024, 512), worldSize: new google.maps.Size(1024, 512), centerHeading: 105, getTileUrl: function (pano, zoom, tileX, tileY) { return `img/${pano}_${zoom}_${tileX}_${tileY}.jpg`; } } }; }); } initMap();
今回カスタムパノラマ画像として、下記のような画像を用意しました。
ちなみに、ストリートビューで公開されている画像は下記の手順でDLすることができます。
http://sanshonoki.hatenablog.com/entry/2017/05/12/215744
先ほどと比較するとなんだかわからないものがいっぱい出てきてしまった感があります笑
ただ、流れとしては単純で下記のような流れでカスタムパノラマを表示することができます。
1. StreetViewPanoramaオブジェクトを作成
2. StreetViewPanoramaオブジェクトのregisterPanoProviderメソッドでカスタムパノラマの情報を登録
一番ややこしそうなのは「registerPanoProvider」メソッドの中だと思います。
GoogleMapAPIのドキュメントでは、カスタムパノラマを表示するにはregisterPanoProviderメソッド内でStreetViewPanoramaDataを返却する必要がある、と記載しています。
つまり、returnで返却しているオブジェクトの部分 = StreetViewPanoramaDataということになりますね。
では、StreetViewPanoramaDataの中で重要なものだけピックアップして説明していこうと思います。
- location
StreetViewオブジェクトの位置情報やパノラマID等、大枠に関する情報を指定するプロパティです。
パノラマIDは最重要と言ってもいいかもしれません。
- links
ストリートビュー内で矢印が表示されていると思いますが、矢印の部分がこのlinksです。
linksで指定した通りに矢印が表示され、クリックするとlinksのpanoに記載されているパノラマIDのストリートビューに遷移します。
headingは角度なので、好きな角度を指定することができます。
- tiles
カスタムパノラマ画像の情報を指定するプロパティです。
getTileUrlはよしなに自オブジェクトのパノラマIDを見て画像を探してくれるので、基本的にこのままで問題ないと思います。
さて、簡単に説明していきましたがカスタムパノラマが一枚の場合はいいですが、二枚三枚となると一々この処理をしないといけないのか...と鬱になってきますよね。
ということで、このあたりの処理を関数化していきましょう!
const initMap = ()=> { const customStvList = { // カスタムパノラマの情報を格納する配列 custompanorama: { pano: 'custompanorama', description: 'カスタムパノラマ', links: [ { heading: 195, description: 'カスタムパノラマ2', pano: 'custompanorama2' } ], lat: 35.6595126, lng: 139.7005696, } }; const customStv = new google.maps.StreetViewPanorama(document.getElementById('pano'), {}); const initCustomStv = (pano) => { // カスタムパノラマを生成する関数 customStv.setPano(pano); registerCustomStv(customStv, createCustomStv( customStvList[pano].pano, customStvList[pano].description, customStvList[pano].links, customStvList[pano].lat, customStvList[pano].lng )) }; const registerCustomStv = (stv, stvPanoramaData) => { // カスタムパノラマを登録する関数 stv.registerPanoProvider(() => { return stvPanoramaData; }); }; const createCustomStv = (pano, description, links, lat, lng) => { // streetViewPanoramaDataを返却する関数 return { location: { pano: pano, description: description, latLng: new google.maps.LatLng(lat, lng) }, links: links, copyright: '©︎2019 Google 日本', tiles: { tileSize: new google.maps.Size(1024, 512), worldSize: new google.maps.Size(1024, 512), centerHeading: 105, getTileUrl: function (panoId, zoom, tileX, tileY) { panoId = pano; return `img/${panoId}_${zoom}_${tileX}_${tileY}.jpg`; } } }; }; initCustomStv('custompanorama'); } initMap();
さて、関数化してみるとなんとなくさっきよりも処理が追いやすくなったのではないかなと思います。
まず、カスタムパノラマを生成するにあたって必要な情報をあらかじめ配列「customStvList」として作成します。
で、「registerCustomStv」の中で引数として指定されたパノラマIDをもとに一致するデータをセットしていくわけですね。
最終的には「initCustomStv」の引数としてパノラマIDを指定すると、そのパノラマIDに応じたカスタムパノラマを生成してくれる処理となっています。
これで、あとは「customStvList」の中にどんどんカスタムパノラマの情報を格納していけばカスタムパノラマを生成することができます。
では、カスタムパノラマの情報を追加してカスタムパノラマの中を移動してみましょう。
const initMap = () => { const customStvList = { // カスタムパノラマの情報を格納する配列 custompanorama: { pano: 'custompanorama', description: 'カスタムパノラマ', links: [ { heading: 195, description: 'カスタムパノラマ2', pano: 'custompanorama2' } ], lat: 35.6595126, lng: 139.7005696, }, custompanorama2: { pano: 'custompanorama2', description: 'カスタムパノラマ2', links: [ { heading: 25, description: 'カスタムパノラマ', pano: 'custompanorama' } ], lat: 35.6525126, lng: 139.7001696, } }; const customStv = new google.maps.StreetViewPanorama(document.getElementById('pano'), {}); const initialSpot = 'custompanorama'; const initCustomStv = (pano) => { // カスタムパノラマを生成する関数 registerCustomStv(customStv, createCustomStv( customStvList[pano].pano, customStvList[pano].description, customStvList[pano].links, customStvList[pano].lat, customStvList[pano].lng )) }; const registerCustomStv = (stv, stvPanoramaData) => { // カスタムパノラマを登録する関数 stv.registerPanoProvider(() => { return stvPanoramaData; }); }; const createCustomStv = (pano, description, links, lat, lng) => { // streetViewPanoramaDataを返却する関数 return { location: { pano: pano, description: description, latLng: new google.maps.LatLng(lat, lng) }, links: links, copyright: '©︎2019 Google 日本', tiles: { tileSize: new google.maps.Size(1024, 512), worldSize: new google.maps.Size(1024, 512), centerHeading: 105, getTileUrl: function (panoId, zoom, tileX, tileY) { panoId = pano; return `img/${panoId}_${zoom}_${tileX}_${tileY}.jpg`; } } }; }; const setCustomStvEvent = () => { customStv.addListener('pano_changed',()=>{ let currentPano = customStv.getPano(); initCustomStv(currentPano); }); }; const initialize = () => { customStv.setPano(initialSpot); initCustomStv('custompanorama'); setCustomStvEvent(); }; initialize(); } window.onload = () => { initMap(); }
さて、カスタムパノラマの情報を一つ追加して処理もまとめてみました。
ちゃんとカスタムパノラマ間を移動することができていますね。
矢印をクリックしたら、というイベントはないのですが「pano_changed」というパノラマIDが変わったら発火する、というイベントはあるんですね。
先ほども言った通り、矢印をクリックするとその矢印に紐づいているパノラマIDのカスタムパノラマに遷移しようとする、つまりパノラマIDが書き換わるため、このイベントが発火する、ということになります。
なので、こちらのイベントで先ほど定義したカスタムパノラマを生成する関数「initCustomStv」を次のパノラマIDを引数にして実行してあげれば次のカスタムパノラマに遷移することができるわけです。
ちょっと引っかかるのが「次のパノラマID」だと思いますが、「pano_changed」というパノラマIDが変わったら発火する、というイベントですのでこのイベント内で「getPano()」という現在表示されているカスタムパノラマのパノラマIDを取得するメソッドを実行すると次のパノラマIDが取得できるということですね。
というわけでGoogleMapsAPIの基本的な使い方をご紹介してみました。
有料になっちゃいましたがまだまだ動的に地図を作成したいときには使うことが多いツールだと思いますので少しでも参考になれば嬉しいです。
Javascriptの変数宣言でカギ括弧がついてるやつってなんなん?
変数宣言で見かける、という人はもしかしたら少ないかもしれませんがもっともよく目にするのはパッケージのImport文だと思います。
import {DynamoDB, S3 } from 'aws-sdk';
的なやつですね。
これ、es6の分割代入というものでして普通に変数宣言にも使えます。
const human = { name: 'tanaka', sex: 'male' } const { name } = human; // tanaka
パッケージのインポートは何気なしにやってたんですが、そういや普通に使えるんだなと笑
Nuxt.jsで環境ごとにdotenvで.envファイルの読み込みを切り替える
なんかパッと解決できなかったので備忘録。
Nuxt.jsではcreate-nuxt-appをするときに「dotenv使うかー?」的なこと聞かれてインストールする方も多いと思います。
初めは一つの.envファイルに下記のように一つのファイルに設定してコメントアウトして切り替えていました。
# 本番用 BASE_URL = 'honban.com' API_BASE_URL = 'https://honban/endpoint/' # 開発用 # BASE_URL = 'kaihatsu.com' # API_BASE_URL = 'https://kaihatsu/endpoint/'
ただ、やっぱだるいのでenvファイルを分けたくなり、結局「.env.prod」と「.env.dev」の二つのファイルを作成し、環境変数で読み込めるようにしました。
使うモジュール的には「dotenv」と「cross-env」で、package.jsonにまず下記のように環境変数を指定します。
"scripts": { "dev": "cross-env NODE_ENV=\"dev\" nuxt-ts", },
これで「yarn dev」したときに環境変数には「dev」が入ります。
そのため、nuxt.config.jsで下記のように読み込ませます。
require('dotenv').config(); export default { mode: 'universal', // 省略 buildModules: [ '@nuxt/typescript-build', '@nuxtjs/vuetify', ['@nuxtjs/dotenv', { filename: `.env.${process.env.NODE_ENV}` }] ] }
dotenvモジュールのホムペ見たら書いてありましたねオプション笑
github.com
やっぱドキュメントが一番!
slackの自作アプリで「not_in_channel」のエラーが出る場合の権限
slackのアプリを作って、Botユーザーに権限与えた時にちょっとハマったやつです。
やりたいこととしては、webAPIを使って
- チャンネル一覧取得
- チャンネルに投稿
という二点だったので、権限としては「channels:read」と「chat:write」の二つを「Bot Token Scopes」に設定してました。
ただ、アプリをインストールしているチャンネルでは正常に投稿できるのですが、インストールしていないチャンネルでは投稿できませんでした。
と、そこでもう一度見直してみると「chat:write.public」という権限が。
こちらよく見ると「インストールしてなくても投稿できまっせ」的なことが書いてあったので権限を変更して再度試すと無事投稿できました。
英語ちゃんと読もうね、ってやつでした。笑