[{"data":1,"prerenderedAt":8223},["ShallowReactive",2],{"articles-page-2":3},{"count":4,"content":5},63,[6,2806,3698,5102,5332,6153,6565,7650,7962,8148],{"id":7,"title":8,"body":9,"category":2792,"createdAt":2794,"description":8,"extension":2795,"index":2796,"meta":2797,"navigation":690,"path":2798,"publish":690,"seo":2799,"series":2796,"seriesTitle":2796,"stem":2800,"tag":2801,"thumbnail":2804,"updatedAt":2794,"__hash__":2805},"articles\u002Farticles\u002Fzoom-api-laravel.md","Zoom APIとLaravelを使って自動ミーティング作成フォームを構築する。",{"type":10,"value":11,"toc":2746},"minimark",[12,24,32,35,40,43,46,52,56,59,64,67,70,73,79,83,86,89,92,95,101,104,107,112,115,131,134,138,141,144,147,150,153,156,159,162,165,168,171,174,182,191,224,228,232,241,244,253,256,259,264,267,270,273,282,285,302,311,330,341,344,347,350,354,357,360,363,366,369,372,376,379,383,401,411,416,422,438,442,445,563,566,580,587,590,596,599,607,613,617,620,630,701,781,796,799,812,842,849,863,870,901,904,910,913,916,920,923,926,935,1116,1128,1131,1189,1200,1206,1216,1219,1253,1272,1275,1281,1288,1402,1405,1408,1411,1414,1417,1492,1507,1520,1526,1529,1532,1535,1538,1660,1663,1666,1670,1673,1676,1680,1908,1914,1920,1930,1933,1935,1939,1942,2314,2317,2320,2323,2384,2401,2405,2462,2471,2474,2477,2488,2491,2495,2652,2655,2658,2661,2664,2667,2670,2673,2676,2679,2682,2685,2688,2691,2694,2697,2700,2703,2718,2721,2724,2727,2735,2739,2742],[13,14,15,16,23],"p",{},"こんにちはJuneです。年明け早々コロナが本気出してきて、ますます外に出れない日々が続きます。まあエンジニアは家にいてもプログラムでいろんなもの作れるのでいい暇つぶしになります。そこでコロナで株が爆上がりの",[17,18,22],"a",{"href":19,"rel":20},"https:\u002F\u002Fmarketplace.zoom.us\u002Fdocs\u002Fapi-reference\u002Fintroduction",[21],"nofollow","zoomにAPI","があるということを知って、早速使ってみました。会社でも「zoom APIには金の匂いがする！」とみんなで盛り上がったので是非探索してみます。",[13,25,26,27,31],{},"zoom API自体は2018年ごろから出ていたらしく、現在はv2がリリースされています。",[17,28,30],{"href":19,"rel":29},[21],"ドキュメント","を一通り読んだ所、zoomで行えることは一通りAPIを通じて行えるそうです。zoom APIについてわかった箇所を詳細に説明したいですが、この記事では実際の活用例を説明したいので部分省略します。",[13,33,34],{},"しかし、zoom APIを使用する認証フローや仕組みについては簡単に解説します。その説明から始めますので、「webアプリのはよ動き見せろや！」「そんなの知っとんじゃ！」という方は「Laravelを立ち上げる」から読み始めてください。",[36,37,39],"h2",{"id":38},"zoom-apiの概要","Zoom APIの概要",[13,41,42],{},"詳しくはZoom API レファランスを見ればわかりますが、zoomのGUIでできることは基本的に可能です。ミーティングを作ったり、ウェビナーを開催したり、ユーザー情報を取ったり、開催中のzoomに対してチャットを送るなどなんでもできます。他にもwebhookや独自のコードでブラウザ上に映像・音声を出力できるSDKやエンドポイントもあるみたいです。",[13,44,45],{},"これらのAPIはRestAPIであり、特定のルートに対してGET\u002FPOST\u002FPUT\u002FDELTEでリクエストし、付属の情報はGETパラメータやPOSTパラメータで送信します。",[13,47,48,49],{},"参考：",[17,50,19],{"href":19,"rel":51},[21],[53,54,55],"h3",{"id":55},"認証方法",[13,57,58],{},"zoom APIにアクセスするには、JWTとOAuth2.0が使用できます。二者の違いはアクセスできる機能の量とマーケットプレイスに公開できるかが主になります。",[60,61,63],"h4",{"id":62},"jwt","JWT",[13,65,66],{},"ます。JSONで認証情報をやりとりします。",[13,68,69],{},"JWTによる認証はOAuthで使用できる権限範囲より狭く、自分自身で簡単にライトに使用したい場合に使うらしいです。また開発したzoom アプリはマーケットプレイスを通じて公開することができますが、JWT認証の場合はその公開ができません。",[13,71,72],{},"独自のwebアプリとZoomを連携させたい場合は次のOAuth2.0認証をお勧めします。",[13,74,48,75],{},[17,76,77],{"href":77,"rel":78},"https:\u002F\u002Fmarketplace.zoom.us\u002Fdocs\u002Fapi-reference\u002Fusing-zoom-apis#using-jwt",[21],[60,80,82],{"id":81},"oauth-20","OAuth 2.0",[13,84,85],{},"OAuthは自身のwebサービスの資格情報を第三者のサービスへ提供する際に使用される認証フローです。ここでいう「webサービス」は「zoom」で「第三者のサービス」は「私のLaravelアプリ」です。",[13,87,88],{},"つまりOAuth認証を用いることで「私のLaravelアプリ」は連携させた人の「zoom」の資格情報を利用できる様になります。よって「私のLaravelアプリ」は連携させた人のzoomのミーティング情報を読み取ったり、作成したり、ユーザー情報を取得することができます。",[13,90,91],{},"資格情報を与え、操作権限も付与するのでかなり厳重な認証システムが必要となりそこで、秘密鍵や特定のプロトコルなどを用いたOAuthが使用されます。上記のJWTよりセキュアであり、また様々なAPIを利用できる様になります。",[13,93,94],{},"OAuthで実装したzoom APIはマーケットプレイスに出して公開することができます。今回の説明ではこのOAuth認証をLaravelを用いて実装していきます。まだ認証・連携のイメージがつかないと思いますが、読んでいくうちにわかると思います（多分）。",[13,96,48,97],{},[17,98,99],{"href":99,"rel":100},"https:\u002F\u002Fmarketplace.zoom.us\u002Fdocs\u002Fapi-reference\u002Fusing-zoom-apis#using-oauth",[21],[53,102,103],{"id":103},"連携の流れ",[13,105,106],{},"OAuthでの認証は以下の様に行われます。",[108,109],"image-render",{":src":110,":width":111},"'_mix\u002F1570826762485.png'","'100%'",[13,113,114],{},"もう少し具体的に解説すると",[116,117,118,122,125,128],"ol",{},[119,120,121],"li",{},"ユーザー（zoomにログイン済み）を認証画面へリダイレクトさせる",[119,123,124],{},"確認後、ユーザーが承認したという証であるcodeを取得。",[119,126,127],{},"OAuthプロトコルに従い作成した認証キーとリクエストをzoomに送る",[119,129,130],{},"アクセストークンを得る。そのアクセストークン でzoom APIにアクセスする。アクセストークン などはアプリのDBに保存しておく。",[13,132,133],{},"こちらも後で実際の画面のスクショ付きで解説しますので、今は「ヘェ〜」程度の理解で大丈夫です。",[53,135,137],{"id":136},"apiへのアクセス方法","APIへのアクセス方法",[13,139,140],{},"OAuthでアクセストークン を取得したら、そのトークンをリクエストヘッダーに仕込んでAPIにアクセスします。",[36,142,143],{"id":143},"今回作るアプリ",[13,145,146],{},"まずアプリの機能と概要を説明しておきます。今回作るアプリは「匿名ユーザーが入力したフォームの内容に応じてzoomミーティングが作成され、それを管理できるwebアプリ」です。見た目は以下の感じです。",[108,148],{":src":149,":width":111},"'_mix\u002Fsch-2021-01-10-20.51.27-768x445.png'",[13,151,152],{},"アプリを管理し連携させるzoomアカウントを持っている「管理者」と、フォームに入力する「匿名ユーザー（お客さん）」が存在するとします。",[53,154,155],{"id":155},"機能の概要",[13,157,158],{},"場面としては「zoomでのご相談はこちら」的な会社用のフォームであると思ってください。最初にお客さんはフォームにて名前・アドレス、zoomの希望開始時間を入力します。",[13,160,161],{},"フォーム入力内容が正しければ、zoom APIを通じて指定の時間で始まるzoom ミーティングを連携先のアカウントで作成。",[13,163,164],{},"APIからのレスポンスよりミーティングURLを取得し、DBに保存すると共にお客さんへミーティング情報をメールで飛ばす。（メールはローカルの環境でできなかったので、実装は割愛。ソースはあります。）",[13,166,167],{},"管理者は管理画面にて誰が・いつミーティングを開くのかを一覧で確認できる。またお客さんは時間変更用・削除用URLにアクセスして時間の変更・ミーティングのキャンセルが可能。",[13,169,170],{},"以上の様な機能を持たせたいと思います。とりあえずこれらの機能を実装する程度なので、厳密なバリデーションや細かい機能は割愛します。",[36,172,173],{"id":173},"開発環境",[13,175,176,177,181],{},"私が慣れているLaravel 6を用いて作成します。Laravelのインストール方法は省略します。一応",[17,178,180],{"href":179},"\u002Farticles\u002Fbuild-lamp-with-docker","こちら","で作ったDocker開発環境を用いています。またzoomへのHTTPアクセスをよく行うので、PHPのHTTPライブラリであるguzzlehttpをインストールしてください。",[13,183,184,185,190],{},"また、以下詳細な開発環境情報です。",[17,186,189],{"href":187,"rel":188},"https:\u002F\u002Fgithub.com\u002FjunjiIshii\u002Flaravel_zoom",[21],"開発したリポジトリも開放してます","のでぜひどうぞ。",[192,193,194,197,200,203,206,209,212,215,218,221],"ul",{},[119,195,196],{},"MacOS Catalina 10.15.5",[119,198,199],{},"Docker 20.10.0",[119,201,202],{},"Docker-compose 1.27.4",[119,204,205],{},"（コンテナ内）composer 1.10.19",[119,207,208],{},"（コンテナ内）Laravel 6.20",[119,210,211],{},"（コンテナ内）guzzlehttp 7.2",[119,213,214],{},"（コンテナ内）centod 8",[119,216,217],{},"（コンテナ内）httpd 2.4.37",[119,219,220],{},"（コンテナ内）php 7.4.7",[119,222,223],{},"（コンテナ内・公式イメージ）mysql:5.7",[36,225,227],{"id":226},"zoom-apiキーを手に入れる","Zoom APIキーを手に入れる",[53,229,231],{"id":230},"zoom-アプリの作成","zoom アプリの作成",[13,233,234,235,240],{},"それでは初めていきましょう。Laravelを実装する前に自身のZoomアカウントにて、zoomアプリを作成していきましょう。",[17,236,239],{"href":237,"rel":238},"https:\u002F\u002Fmarketplace.zoom.us\u002F",[21],"zoom マーケットプレイス","へ移動します。そして画面上部の「Develop」をクリックし「Build App」をクリックします。",[108,242],{":src":243,":width":111},"'_mix\u002Fzoom_marget-768x330.jpeg'",[13,245,246,247,252],{},"すると",[17,248,251],{"href":249,"rel":250},"https:\u002F\u002Fmarketplace.zoom.us\u002Fdevelop\u002Fcreate",[21],"zoomアプリを選択する以下の様な画面","が表示されますので、「OAuth」の「Create」をクリック",[108,254],{":src":255,":width":111},"'_mix\u002Fzoom_app_create-768x385.jpeg'",[13,257,258],{},"「Create」を押すとアプリの名前などを入力するモーダルが出現します。任意の名前を入力し、アプリのタイプ、マーケットプレイスに公表するかを決定します。とりあえず私は以下の様にしました。",[108,260],{":src":261,":width":262,":center":263},"'_mix\u002Fzoom_mordal-768x637.jpeg'","'500px'","true",[13,265,266],{},"名前とマーケットプレイスへの公開は後から変更できます。ひとまず入力したら「Create」を押します。",[53,268,269],{"id":269},"諸所の設定を入力",[60,271,272],{"id":272},"アプリの認証情報",[13,274,275,276,281],{},"作成後には",[17,277,280],{"href":278,"rel":279},"https:\u002F\u002Fmarketplace.zoom.us\u002Fuser\u002Fbuild",[21],"アプリの管理画面","に飛ばされると思います。そこから作成したアプリを選択して「App Credentials」を選択",[108,283],{":src":284,":width":111},"'_mix\u002Fzoom_app_info-768x637.jpeg'",[13,286,287,288,292,293,297,298,301],{},"Larvel側には ",[289,290,291],"strong",{},"「Client ID」「Client Secret」"," が必要となります。後で",[294,295,296],"code",{},".env","ファイルに記載します。ちなみに ",[289,299,300],{},"「Client Secret」"," は絶対に外に漏れてはいけません。",[13,303,304,307,308,310],{},[289,305,306],{},"「Redirect URL for OAuth」"," にて承認画面からのリダイレクト先を指定しておきます。承認画面でアプリ連携を許可した際にはトークンなどが ",[289,309,306],{}," あてへ送信され、ユーザーもリダイレクトされます。ここの値が異なっているとエラーで認証が進みません。",[13,312,313,314,317,318,321,322,325,326,329],{},"今は開発環境なのでドメインに",[294,315,316],{},"localhost","を指定しています。（私の環境では",[294,319,320],{},"localhost:9000","でLaravelの画面が表示される様に設定しています。",[294,323,324],{},"php artisan serve","などの場合は",[294,327,328],{},"localhost:8000","になると思いますので、ポートの指定に気をつけてください。）",[13,331,332,333,336,337,340],{},"「whitelist URL」はOAuthのリダイレクト先として許可するURLを指定できます。OAuthリダイレクトのURLに完全一致させるか、前方一致させる必要があります。設定したリダイレクト先は",[294,334,335],{},"http:\u002F\u002Flocalhost:9000\u002Fzoomauth\u002Fcheck","としていたので、その前方を含める様に",[294,338,339],{},"http:\u002F\u002Flocalhost:9000\u002F","に設定しておきます。",[60,342,343],{"id":343},"アプリの公開情報",[108,345],{":src":346,":width":111},"'_mix\u002Fzoom_information-572x1024.jpeg'",[13,348,349],{},"「Information」にて「Optional」と書かれている箇所以外を入力し記述します。",[60,351,353],{"id":352},"アプリのスコープアクセス範囲","アプリのスコープ（アクセス範囲）",[13,355,356],{},"ここが結構重要です。「Scopes」という箇所ではアプリの操作権限、アクセス範囲を設定できます。ユーザー情報の取得やミーティングの作成にもそれぞれスコープが用意されて、スコープ外の操作へのアクセスは401と認証エラーとなります。「Add Scopes」でスコープを追加します。",[13,358,359],{},"よくわからなければ全部追加してもいいですが、「Meeting」「User」のスコープを全て追加しておけば今回のアプリの実装は可能です。",[108,361],{":src":362,":width":111},"'_mix\u002Fzoom_scopes-768x431.jpeg'",[60,364,365],{"id":365},"アプリのアクティベート",[13,367,368],{},"最後に「Activation」にて確認します。不足箇所は以下の様にオレンジ文字で指摘されるので直しましょう。全てが入力できていれば後は問題ありません。「Install」などは押さなくても問題ありません。",[108,370],{":src":371,":width":111},"'_mix\u002Fzoom_activation-768x412.jpeg'",[36,373,375],{"id":374},"lravelを立ち上げるdocker","Lravelを立ち上げる(Docker）",[13,377,378],{},"それではLaravelを立ち上げましょう。インストールはされており、ユーザーテーブルのマイグレーションを行う前だと仮定します。",[53,380,382],{"id":381},"clien-id-と-client-secretを設定","Clien ID と Client Secretを設定",[13,384,385,386,388,389,392,393,396,397,400],{},"Larvelのプロジェクトルートに",[294,387,296],{},"という環境変数を記述するファイルがありますので、そちらに ",[289,390,391],{},"「App Credentials」"," で取得できる",[294,394,395],{},"Client ID","と",[294,398,399],{},"Client Secret","を設定します。",[402,403,409],"pre",{"className":404,"code":406,"filename":296,"language":407,"meta":408},[405],"language-text","APP_NAME=Laravel\nAPP_ENV=local\n...\nMIX_PUSHER_APP_CLUSTER=\"${PUSHER_APP_CLUSTER}\"\n\nZOOM_CLIENT_ID=clientid\nZOOM_CLIENT_SECRET=clientsecret\n","text","",[294,410,406],{"__ignoreMap":408},[13,412,413,415],{},[294,414,296],{},"を更新したらキャッシュをクリアして反映させます。",[402,417,420],{"className":418,"code":419,"language":407},[405],"php artisan config:clear\nphp artisan cache:clear\n",[294,421,419],{"__ignoreMap":408},[13,423,424,426,427,430,431,433,434,437],{},[294,425,296],{},"に記述することで他のソースコード内で",[294,428,429],{},"env('ZOOM_CLIENT_ID')","と行った形で出力できます。また",[294,432,296],{},"は基本的に",[294,435,436],{},".gitignore","に登録されているので公開リポジトリに秘密鍵が載ってしまうという様な事故を防げます。",[53,439,441],{"id":440},"user-tableをちょっと改造","User tableをちょっと改造",[13,443,444],{},"今回のアプリの管理者は一人ですが、もし複数人に使用してもらいたい時に「ユーザーごとにトークンを分けたいな」と思ったのでその改造をします。初期で用意されているユーザーテーブルを以下の様に書き換えます。",[402,446,451],{"className":447,"code":448,"filename":449,"language":450,"meta":408,"style":408},"language-php shiki shiki-themes material-theme-ocean","public function up()\n{\n    Schema::create('users', function (Blueprint $table) {\n        $table->bigIncrements('id');\n        $table->string('name');\n        $table->string('email')->unique();\n        $table->timestamp('email_verified_at')->nullable();\n        $table->string('password');\n    \n        \u002F\u002Fここから追加\n        $table->longText('zoom_code')->nullable()->default(null);\n        $table->longText('access_token')->nullable()->default(null);\n        $table->longText('refresh_token')->nullable()->default(null);\n        $table->timestamp('zoom_expires_in', 0)->nullable()->default(null);\n        $table->rememberToken();\n        $table->timestamps();\n    });\n}\n","database\u002Fmigrations\u002F2014_10_12_000000_create_users_table.php","php",[294,452,453,461,467,473,479,485,491,497,503,509,515,521,527,533,539,545,551,557],{"__ignoreMap":408},[454,455,458],"span",{"class":456,"line":457},"line",1,[454,459,460],{},"public function up()\n",[454,462,464],{"class":456,"line":463},2,[454,465,466],{},"{\n",[454,468,470],{"class":456,"line":469},3,[454,471,472],{},"    Schema::create('users', function (Blueprint $table) {\n",[454,474,476],{"class":456,"line":475},4,[454,477,478],{},"        $table->bigIncrements('id');\n",[454,480,482],{"class":456,"line":481},5,[454,483,484],{},"        $table->string('name');\n",[454,486,488],{"class":456,"line":487},6,[454,489,490],{},"        $table->string('email')->unique();\n",[454,492,494],{"class":456,"line":493},7,[454,495,496],{},"        $table->timestamp('email_verified_at')->nullable();\n",[454,498,500],{"class":456,"line":499},8,[454,501,502],{},"        $table->string('password');\n",[454,504,506],{"class":456,"line":505},9,[454,507,508],{},"    \n",[454,510,512],{"class":456,"line":511},10,[454,513,514],{},"        \u002F\u002Fここから追加\n",[454,516,518],{"class":456,"line":517},11,[454,519,520],{},"        $table->longText('zoom_code')->nullable()->default(null);\n",[454,522,524],{"class":456,"line":523},12,[454,525,526],{},"        $table->longText('access_token')->nullable()->default(null);\n",[454,528,530],{"class":456,"line":529},13,[454,531,532],{},"        $table->longText('refresh_token')->nullable()->default(null);\n",[454,534,536],{"class":456,"line":535},14,[454,537,538],{},"        $table->timestamp('zoom_expires_in', 0)->nullable()->default(null);\n",[454,540,542],{"class":456,"line":541},15,[454,543,544],{},"        $table->rememberToken();\n",[454,546,548],{"class":456,"line":547},16,[454,549,550],{},"        $table->timestamps();\n",[454,552,554],{"class":456,"line":553},17,[454,555,556],{},"    });\n",[454,558,560],{"class":456,"line":559},18,[454,561,562],{},"}\n",[13,564,565],{},"それぞれのカラムの説明はこの通り。",[192,567,568,571,574,577],{},[119,569,570],{},"zoom_code：連携許可の際に得られる許可コード。",[119,572,573],{},"access_token：APIにアクセスするためのアクセストークン これを手にしたら勝ち。",[119,575,576],{},"refresh_token：access_tokenを更新するためのトークン。",[119,578,579],{},"zoom_expires_in：access_tokenの期限を記録しておく。APIにアクセスする前にこれでチェックする。",[13,581,582,583,586],{},"ユーザーごとのトークンが管理できる様になり、",[294,584,585],{},"$user->auth()->access_token","みたいな感じでトークンを使用できます。",[13,588,589],{},"それではマイグレーションをしましょう。（仮ユーザーのseedも忘れずに）",[402,591,594],{"className":592,"code":593,"language":407},[405],"php artisan migrate --seed\nMigrating: 2014_10_12_000000_create_users_table\nMigrated:  2014_10_12_000000_create_users_table (0.02 seconds)\nMigrating: 2014_10_12_100000_create_password_resets_table\nMigrated:  2014_10_12_100000_create_password_resets_table (0.01 seconds)\nMigrating: 2019_08_19_000000_create_failed_jobs_table\nMigrated:  2019_08_19_000000_create_failed_jobs_table (0.01 seconds)\nSeeding: UsersTableSeeder\nSeeded:  UsersTableSeeder (0.06 seconds)\nDatabase seeding completed successfully.\n",[294,595,593],{"__ignoreMap":408},[53,597,598],{"id":598},"フォームと管理画面を適当に作る",[13,600,601,602,606],{},"詳しくは",[17,603,605],{"href":187,"rel":604},[21],"アップしたリポジトリ","を見てください。スタイルはbootstrapで調整しています。ルート情報だけ載せておきます。",[402,608,611],{"className":609,"code":610,"language":407},[405],"\u002F                   フォームを表示  \n\u002Fconfirm            フォームの受付完了確認画面\n\u002Fadmin              管理者用ページ\n\u002Flogin              ログインページ\n\u002Flogout             ログアウトルート\n\u002Fzoomoatuh\u002Fcheck    zoom OAuthリダイレクト先\n\u002Fform\u002Falter         ミーティングの時間変更画面\n\u002Fform\u002Fdelete        ミーティングのキャンセル画面\n",[294,612,610],{"__ignoreMap":408},[36,614,616],{"id":615},"lravel-と-zoomのoatuh-2-認証連携","Lravel と ZoomのOatuh 2 認証・連携",[53,618,619],{"id":619},"管理画面から連携確認画面へ誘導とユーザー認証",[13,621,622,623,626,627,629],{},"それではLaravelとZoomの連携処理を実装していきます。連携処理はログインした管理者のアクセス配下で行います。管理画面は",[294,624,625],{},"\u002Fadmin","です。",[294,628,625],{},"のコントローラーとビューは以下の通りです。",[402,631,634],{"className":447,"code":632,"filename":633,"language":450,"meta":408,"style":408},"public function index(Request $request){\n    $user = auth()->user();\n    $noZoomCode = $user->zoom_code == null; \u002F\u002F連携を行っているか\n    $zoomOuthLink = 'https:\u002F\u002Fzoom.us\u002Foauth\u002Fauthorize?'.http_build_query([\n        'response_type'=>'code',\n        'redirect_uri'=>env('APP_URL').'\u002Fzoomoatuh\u002Fcheck',\n        'client_id'=>env('ZOOM_CLIENT_ID'),\n    ]);\n    $oauthSuccess=false;\n    $meetings = Meeting::all();\n\n    return view('admin',compact('noZoomCode','zoomOuthLink','oauthSuccess','meetings'));\n}\n","app\u002FHttp\u002FControllers\u002FAdminController.php",[294,635,636,641,646,651,656,661,666,671,676,681,686,692,697],{"__ignoreMap":408},[454,637,638],{"class":456,"line":457},[454,639,640],{},"public function index(Request $request){\n",[454,642,643],{"class":456,"line":463},[454,644,645],{},"    $user = auth()->user();\n",[454,647,648],{"class":456,"line":469},[454,649,650],{},"    $noZoomCode = $user->zoom_code == null; \u002F\u002F連携を行っているか\n",[454,652,653],{"class":456,"line":475},[454,654,655],{},"    $zoomOuthLink = 'https:\u002F\u002Fzoom.us\u002Foauth\u002Fauthorize?'.http_build_query([\n",[454,657,658],{"class":456,"line":481},[454,659,660],{},"        'response_type'=>'code',\n",[454,662,663],{"class":456,"line":487},[454,664,665],{},"        'redirect_uri'=>env('APP_URL').'\u002Fzoomoatuh\u002Fcheck',\n",[454,667,668],{"class":456,"line":493},[454,669,670],{},"        'client_id'=>env('ZOOM_CLIENT_ID'),\n",[454,672,673],{"class":456,"line":499},[454,674,675],{},"    ]);\n",[454,677,678],{"class":456,"line":505},[454,679,680],{},"    $oauthSuccess=false;\n",[454,682,683],{"class":456,"line":511},[454,684,685],{},"    $meetings = Meeting::all();\n",[454,687,688],{"class":456,"line":517},[454,689,691],{"emptyLinePlaceholder":690},true,"\n",[454,693,694],{"class":456,"line":523},[454,695,696],{},"    return view('admin',compact('noZoomCode','zoomOuthLink','oauthSuccess','meetings'));\n",[454,698,699],{"class":456,"line":529},[454,700,562],{},[402,702,705],{"className":447,"code":703,"filename":704,"language":450,"meta":408,"style":408},"@extends('layouts.layout')\n\n@section('main-content')\n    \u003Cdiv class=\"main-content\">\n        @if($noZoomCode)\n        \u003Cdiv class=\"alert alert-danger mb-3\" role=\"alert\">\n            \u003Ch4 class=\"alert-heading\">Zoomとの連携が行われていません。\u003C\u002Fh4>\n            \u003Cp>このシステムをご利用する場合、Zoomとの連携を行ってください。\u003C\u002Fp>\n            \u003Ca href=\"{{$zoomOuthLink}}\" class=\"btn btn-danger\">Zoomと連携\u003C\u002Fa>\n        \u003C\u002Fdiv>\n        @else\n        \u003Ch1>予約一覧\u003C\u002Fh1>\n        @endif\n    \u003C\u002Fdiv>\n@endsection\n","resources\u002Fviews\u002Fadmin.blade.php",[294,706,707,712,716,721,726,731,736,741,746,751,756,761,766,771,776],{"__ignoreMap":408},[454,708,709],{"class":456,"line":457},[454,710,711],{},"@extends('layouts.layout')\n",[454,713,714],{"class":456,"line":463},[454,715,691],{"emptyLinePlaceholder":690},[454,717,718],{"class":456,"line":469},[454,719,720],{},"@section('main-content')\n",[454,722,723],{"class":456,"line":475},[454,724,725],{},"    \u003Cdiv class=\"main-content\">\n",[454,727,728],{"class":456,"line":481},[454,729,730],{},"        @if($noZoomCode)\n",[454,732,733],{"class":456,"line":487},[454,734,735],{},"        \u003Cdiv class=\"alert alert-danger mb-3\" role=\"alert\">\n",[454,737,738],{"class":456,"line":493},[454,739,740],{},"            \u003Ch4 class=\"alert-heading\">Zoomとの連携が行われていません。\u003C\u002Fh4>\n",[454,742,743],{"class":456,"line":499},[454,744,745],{},"            \u003Cp>このシステムをご利用する場合、Zoomとの連携を行ってください。\u003C\u002Fp>\n",[454,747,748],{"class":456,"line":505},[454,749,750],{},"            \u003Ca href=\"{{$zoomOuthLink}}\" class=\"btn btn-danger\">Zoomと連携\u003C\u002Fa>\n",[454,752,753],{"class":456,"line":511},[454,754,755],{},"        \u003C\u002Fdiv>\n",[454,757,758],{"class":456,"line":517},[454,759,760],{},"        @else\n",[454,762,763],{"class":456,"line":523},[454,764,765],{},"        \u003Ch1>予約一覧\u003C\u002Fh1>\n",[454,767,768],{"class":456,"line":529},[454,769,770],{},"        @endif\n",[454,772,773],{"class":456,"line":535},[454,774,775],{},"    \u003C\u002Fdiv>\n",[454,777,778],{"class":456,"line":541},[454,779,780],{},"@endsection\n",[13,782,783,784,787,788,791,792,795],{},"管理画面では管理者がzoomと連携しているかで表示を変更しています。連携しているかは",[294,785,786],{},"user","テーブルの",[294,789,790],{},"zoom_code","が",[294,793,794],{},"null","かで確認しています。",[13,797,798],{},"連携が済んでいない場合はzoomの連携確認画面へ飛ばすリンクボタンを表示させています。",[13,800,801,802,805,806,811],{},"この",[294,803,804],{},"$zoomOuthLink","の作成は",[17,807,810],{"href":808,"rel":809},"https:\u002F\u002Fmarketplace.zoom.us\u002Fdocs\u002Fguides\u002Fauth\u002Foauth#getting-access-token",[21],"こちらのドキュメント","にある通り、ルールがあります。",[402,813,815],{"className":447,"code":814,"filename":633,"language":450,"meta":408,"style":408},"$zoomOuthLink = 'https:\u002F\u002Fzoom.us\u002Foauth\u002Fauthorize?'.http_build_query([\n    'response_type'=>'code',\n    'redirect_uri'=>env('APP_URL').'\u002Fzoomoatuh\u002Fcheck',\n    'client_id'=>env('ZOOM_CLIENT_ID'),\n]);\n",[294,816,817,822,827,832,837],{"__ignoreMap":408},[454,818,819],{"class":456,"line":457},[454,820,821],{},"$zoomOuthLink = 'https:\u002F\u002Fzoom.us\u002Foauth\u002Fauthorize?'.http_build_query([\n",[454,823,824],{"class":456,"line":463},[454,825,826],{},"    'response_type'=>'code',\n",[454,828,829],{"class":456,"line":469},[454,830,831],{},"    'redirect_uri'=>env('APP_URL').'\u002Fzoomoatuh\u002Fcheck',\n",[454,833,834],{"class":456,"line":475},[454,835,836],{},"    'client_id'=>env('ZOOM_CLIENT_ID'),\n",[454,838,839],{"class":456,"line":481},[454,840,841],{},"]);\n",[13,843,844,845,848],{},"今はOAuthのステップの中で「ユーザー認証」というユーザーへ「このアプリ（Laravel）とzoomを連携してもいい？」とzoomが聞いている段階です。そのユーザー認証にはまず",[294,846,847],{},"https:\u002F\u002Fzoom.us\u002Foauth\u002Fauthorize","へGETで管理者本人がアクセスします。",[13,850,851,852,855,856,855,859,862],{},"その際にGETパラメータに",[294,853,854],{},"response_type","、",[294,857,858],{},"redirect_uri",[294,860,861],{},"client_id","を入力します。",[864,865,869],"div",{"className":866},[867,868],"alert","alert-info","\nresponse_typeAccess response type being requested. The supported authorization workflow requires the value `code`.\n",[13,871,872,873,875,876,878,879,881,882,887,888,890,891,893,894,896,897,900],{},"とある様に",[294,874,854],{},"には",[294,877,294],{},"という文字を設定します。そして",[294,880,858],{},"はzoom ",[17,883,886],{"href":884,"rel":885},"https:\u002F\u002Fjun-app.com\u002Fzoom-api-laravel\u002F#zoom-redirect-url",[21],"アプリ作成時にも設定した通りのURL","を入力しますので、",[294,889,335],{},"を設定。",[294,892,861],{},"は",[294,895,296],{},"で設定値を",[294,898,899],{},"env()","で呼び出します。",[13,902,903],{},"それらをGETパラメータとして一つのURLにまとめます。以下の様な感じです。",[402,905,908],{"className":906,"code":907,"language":407},[405],"https:\u002F\u002Fzoom.us\u002Foauth\u002Fauthorize?response_type=code&redirect_uri=http:\u002F\u002Flocalhost:9000\u002Fzoomauth\u002Fcheck&client_id=clientid\n",[294,909,907],{"__ignoreMap":408},[13,911,912],{},"予めサーバーサイドで作っておき、ボタンのリンクにはめ込んでおきます。画面では以下の様に表示されます。",[108,914],{":src":915,":width":111},"'_mix\u002Fsch-2021-01-10-18.08.06.png'",[53,917,919],{"id":918},"認証画面からのリダイレクトurlでの処理","認証画面からのリダイレクトURLでの処理",[13,921,922],{},"ボタンをクリックすると以下の画面が表示されます。（正確には承認画面のGETを叩く）",[108,924],{":src":925,":width":262,":center":263},"'_mix\u002Fzoom_approve.jpeg'",[13,927,928,929,931,932,934],{},"管理者に対してこのアプリが自身のzoomアカウントに対して、何をするのかが書かれています。管理者はこの「認可」を押すと、",[294,930,858],{},"のリダイレクト先に飛ばされます。OAuthではこのリダイレクト先の処理が大切です！",[294,933,335],{},"のコントローラーは以下の通りです。（ビューはなし）",[402,936,938],{"className":447,"code":937,"filename":633,"language":450,"meta":408,"style":408},"public function zoomOauth(Request $request){\n    $user = auth()->user();\n\n    if($user->zoom_code==null){\n        $code = $request['code'];\n\n        $user->zoom_code = $code;\n        $user->save();\n\n        $basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));\n        $client = new \\GuzzleHttp\\Client([\n            'headers' => ['Authorization' => 'Basic '.$basic]\n        ]);\n        $res = $client->request('POST','https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken',[\n            'query' => [\n                'grant_type'=>'authorization_code',\n                'code'=>$code,\n                'redirect_uri'=>'http:\u002F\u002Flocalhost:9000\u002Fzoomoatuh\u002Fcheck'\n            ]\n        ]);\n        $result = json_decode($res->getBody()->getContents());\n\n        $user->access_token= $result->access_token;\n        $user->refresh_token= $result->refresh_token;\n        $unixTime = time();\n        $user->zoom_expires_in= date(\"Y-m-d H:i:s\",$unixTime+$result->expires_in);\n        $user->save();\n\n        return redirect()->route('amdin')->with([\n            'noZoomCode'=>false,\n            'oauthSuccess'=>true\n        ]);\n    }\n}\n",[294,939,940,945,949,953,958,963,967,972,977,981,986,991,996,1001,1006,1011,1016,1021,1026,1032,1037,1043,1048,1054,1060,1066,1072,1077,1082,1088,1094,1100,1105,1111],{"__ignoreMap":408},[454,941,942],{"class":456,"line":457},[454,943,944],{},"public function zoomOauth(Request $request){\n",[454,946,947],{"class":456,"line":463},[454,948,645],{},[454,950,951],{"class":456,"line":469},[454,952,691],{"emptyLinePlaceholder":690},[454,954,955],{"class":456,"line":475},[454,956,957],{},"    if($user->zoom_code==null){\n",[454,959,960],{"class":456,"line":481},[454,961,962],{},"        $code = $request['code'];\n",[454,964,965],{"class":456,"line":487},[454,966,691],{"emptyLinePlaceholder":690},[454,968,969],{"class":456,"line":493},[454,970,971],{},"        $user->zoom_code = $code;\n",[454,973,974],{"class":456,"line":499},[454,975,976],{},"        $user->save();\n",[454,978,979],{"class":456,"line":505},[454,980,691],{"emptyLinePlaceholder":690},[454,982,983],{"class":456,"line":511},[454,984,985],{},"        $basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));\n",[454,987,988],{"class":456,"line":517},[454,989,990],{},"        $client = new \\GuzzleHttp\\Client([\n",[454,992,993],{"class":456,"line":523},[454,994,995],{},"            'headers' => ['Authorization' => 'Basic '.$basic]\n",[454,997,998],{"class":456,"line":529},[454,999,1000],{},"        ]);\n",[454,1002,1003],{"class":456,"line":535},[454,1004,1005],{},"        $res = $client->request('POST','https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken',[\n",[454,1007,1008],{"class":456,"line":541},[454,1009,1010],{},"            'query' => [\n",[454,1012,1013],{"class":456,"line":547},[454,1014,1015],{},"                'grant_type'=>'authorization_code',\n",[454,1017,1018],{"class":456,"line":553},[454,1019,1020],{},"                'code'=>$code,\n",[454,1022,1023],{"class":456,"line":559},[454,1024,1025],{},"                'redirect_uri'=>'http:\u002F\u002Flocalhost:9000\u002Fzoomoatuh\u002Fcheck'\n",[454,1027,1029],{"class":456,"line":1028},19,[454,1030,1031],{},"            ]\n",[454,1033,1035],{"class":456,"line":1034},20,[454,1036,1000],{},[454,1038,1040],{"class":456,"line":1039},21,[454,1041,1042],{},"        $result = json_decode($res->getBody()->getContents());\n",[454,1044,1046],{"class":456,"line":1045},22,[454,1047,691],{"emptyLinePlaceholder":690},[454,1049,1051],{"class":456,"line":1050},23,[454,1052,1053],{},"        $user->access_token= $result->access_token;\n",[454,1055,1057],{"class":456,"line":1056},24,[454,1058,1059],{},"        $user->refresh_token= $result->refresh_token;\n",[454,1061,1063],{"class":456,"line":1062},25,[454,1064,1065],{},"        $unixTime = time();\n",[454,1067,1069],{"class":456,"line":1068},26,[454,1070,1071],{},"        $user->zoom_expires_in= date(\"Y-m-d H:i:s\",$unixTime+$result->expires_in);\n",[454,1073,1075],{"class":456,"line":1074},27,[454,1076,976],{},[454,1078,1080],{"class":456,"line":1079},28,[454,1081,691],{"emptyLinePlaceholder":690},[454,1083,1085],{"class":456,"line":1084},29,[454,1086,1087],{},"        return redirect()->route('amdin')->with([\n",[454,1089,1091],{"class":456,"line":1090},30,[454,1092,1093],{},"            'noZoomCode'=>false,\n",[454,1095,1097],{"class":456,"line":1096},31,[454,1098,1099],{},"            'oauthSuccess'=>true\n",[454,1101,1103],{"class":456,"line":1102},32,[454,1104,1000],{},[454,1106,1108],{"class":456,"line":1107},33,[454,1109,1110],{},"    }\n",[454,1112,1114],{"class":456,"line":1113},34,[454,1115,562],{},[13,1117,1118,1120,1121,1123,1124,1127],{},[294,1119,847],{}," から ",[294,1122,335],{}," へリダイレクトされると自動的にGETパラメータに",[294,1125,1126],{},"?code=~~~~","というものが付与されています。このcodeは後の認証に必要になります。",[13,1129,1130],{},"リダイレクトURLからcodeの値を取得します。いったんDBに保存してから、実際にAPIへリクエストするのに必要なアクセストークンの取得処理を行います。そこで以下の様なリクエストを行います。",[402,1132,1134],{"className":447,"code":1133,"filename":633,"language":450,"meta":408,"style":408},"$basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));\n$client = new \\GuzzleHttp\\Client([\n    'headers' => ['Authorization' => 'Basic '.$basic]\n]);\n$res = $client->request('POST','https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken',[\n    'query' => [\n        'grant_type'=>'authorization_code',\n        'code'=>$code,\n        'redirect_uri'=>'http:\u002F\u002Flocalhost:9000\u002Fzoomoatuh\u002Fcheck'\n    ]\n]);\n",[294,1135,1136,1141,1146,1151,1155,1160,1165,1170,1175,1180,1185],{"__ignoreMap":408},[454,1137,1138],{"class":456,"line":457},[454,1139,1140],{},"$basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));\n",[454,1142,1143],{"class":456,"line":463},[454,1144,1145],{},"$client = new \\GuzzleHttp\\Client([\n",[454,1147,1148],{"class":456,"line":469},[454,1149,1150],{},"    'headers' => ['Authorization' => 'Basic '.$basic]\n",[454,1152,1153],{"class":456,"line":475},[454,1154,841],{},[454,1156,1157],{"class":456,"line":481},[454,1158,1159],{},"$res = $client->request('POST','https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken',[\n",[454,1161,1162],{"class":456,"line":487},[454,1163,1164],{},"    'query' => [\n",[454,1166,1167],{"class":456,"line":493},[454,1168,1169],{},"        'grant_type'=>'authorization_code',\n",[454,1171,1172],{"class":456,"line":499},[454,1173,1174],{},"        'code'=>$code,\n",[454,1176,1177],{"class":456,"line":505},[454,1178,1179],{},"        'redirect_uri'=>'http:\u002F\u002Flocalhost:9000\u002Fzoomoatuh\u002Fcheck'\n",[454,1181,1182],{"class":456,"line":511},[454,1183,1184],{},"    ]\n",[454,1186,1187],{"class":456,"line":517},[454,1188,841],{},[13,1190,1191,1192,1195,1196,1199],{},"Zoomにも書いてある通りの処理ですが、アクセストークンを得る ",[294,1193,1194],{},"https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken"," というルートにアクセスするときは、まずリクエストヘッダーを付与します。リクエストヘッダーは ",[294,1197,1198],{},"'headers' => ['Authorization' => 'Basic '.$basic]"," です。ここに client IDとclient secretをコロンで付けて一つの文字列にし、それをbase64エンコードをします。つまり明示的に処理を表示すると以下の様な感じです。",[402,1201,1204],{"className":1202,"code":1203,"language":407},[405],"client_id:cilent_secret \u002F\u002Fこれで一行の文字列\n↓\nこの値を64base encode\n↓\nsi84nf7435934jdfsdfi... \u002F\u002Fエンコード化された文字。これを送る\n",[294,1205,1203],{"__ignoreMap":408},[13,1207,1208,1209,1211,1212,1215],{},"そしてそれをリクエストヘッダーに付与します。",[294,1210,1198],{}," これを文字列として表示すると、",[294,1213,1214],{},"Authorization: Basic si84nf7435934jdfsdfi…"," みたいな感じです。ちなみに Basicとエンコード文字の間は半角が空いていますので注意。",[13,1217,1218],{},"リクエストヘッダーを付けたら先ほどと似た感じでGETパラメータを以下の様に設定します。",[402,1220,1222],{"className":447,"code":1221,"language":450,"meta":408,"style":408},"$res = $client->request('POST','https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken',[\n    'query' => [\n        'grant_type'=>'authorization_code',\n        'code'=>$code,\n        'redirect_uri'=>'http:\u002F\u002Flocalhost:9000\u002Fzoomoatuh\u002Fcheck'\n    ]\n])\n",[294,1223,1224,1228,1232,1236,1240,1244,1248],{"__ignoreMap":408},[454,1225,1226],{"class":456,"line":457},[454,1227,1159],{},[454,1229,1230],{"class":456,"line":463},[454,1231,1164],{},[454,1233,1234],{"class":456,"line":469},[454,1235,1169],{},[454,1237,1238],{"class":456,"line":475},[454,1239,1174],{},[454,1241,1242],{"class":456,"line":481},[454,1243,1179],{},[454,1245,1246],{"class":456,"line":487},[454,1247,1184],{},[454,1249,1250],{"class":456,"line":493},[454,1251,1252],{},"])\n",[13,1254,1255,791,1258,1261,1262,1265,1266,1268,1269,1271],{},[294,1256,1257],{},"grant_type",[294,1259,1260],{},"authorization_code","という文字とし、codeにはリダイレクト時についてきた値である",[294,1263,1264],{},"$request['code']","を用います。",[294,1267,858],{},"は先ほどと同じです。（",[294,1270,858],{},"を別にすると認証が通りません！）",[13,1273,1274],{},"これでセットアップが完了です。実際のURLとしては以下の感じです。",[402,1276,1279],{"className":1277,"code":1278,"language":407},[405],"https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken?grant_type=authorization_code&code=~~~~~&redirect_uri=http:\u002F\u002Flocalhost:9000\u002Fzoomoatuh\u002Fcheck\n（そして直接は見えないですが、リクエストヘッダーには 「Authorization: Basic si84nf7435934jdfsdfi…」 という値がついています！\n",[294,1280,1278],{"__ignoreMap":408},[13,1282,1283,1284,1287],{},"リクエストが成功するとアクセストークン を含んだレスポンスがJSONで戻ってきます。それを展開してDBへ保存します。",[294,1285,1286],{},"zoom_expires_in","は現在時刻と足し合わせて、期限切れ時刻を計算してから格納しています。",[402,1289,1291],{"className":447,"code":1290,"language":450,"meta":408,"style":408},"\u002F*\n$resultの中身の例\n{\n    \"access_token\": \"eyJhbGciOiJIUz...\",\n    \"token_type\": \"bearer\",\n    \"refresh_token\": \"eyJhbGciOiJI..\",\n    \"expires_in\": 3599,\n    \"scope\": \"user:read\"\n}\n*\u002F\n\n$result = json_decode($res->getBody()->getContents());\n\n$user->access_token= $result->access_token;\n$user->refresh_token= $result->refresh_token;\n$unixTime = time();\n$user->zoom_expires_in= date(\"Y-m-d H:i:s\",$unixTime+$result->expires_in);\n$user->save();\n\nreturn redirect()->route('amdin')->with([\n         'noZoomCode'=>false,\n         'oauthSuccess'=>true\n]);\n",[294,1292,1293,1298,1303,1307,1312,1317,1322,1327,1332,1336,1341,1345,1350,1354,1359,1364,1369,1374,1379,1383,1388,1393,1398],{"__ignoreMap":408},[454,1294,1295],{"class":456,"line":457},[454,1296,1297],{},"\u002F*\n",[454,1299,1300],{"class":456,"line":463},[454,1301,1302],{},"$resultの中身の例\n",[454,1304,1305],{"class":456,"line":469},[454,1306,466],{},[454,1308,1309],{"class":456,"line":475},[454,1310,1311],{},"    \"access_token\": \"eyJhbGciOiJIUz...\",\n",[454,1313,1314],{"class":456,"line":481},[454,1315,1316],{},"    \"token_type\": \"bearer\",\n",[454,1318,1319],{"class":456,"line":487},[454,1320,1321],{},"    \"refresh_token\": \"eyJhbGciOiJI..\",\n",[454,1323,1324],{"class":456,"line":493},[454,1325,1326],{},"    \"expires_in\": 3599,\n",[454,1328,1329],{"class":456,"line":499},[454,1330,1331],{},"    \"scope\": \"user:read\"\n",[454,1333,1334],{"class":456,"line":505},[454,1335,562],{},[454,1337,1338],{"class":456,"line":511},[454,1339,1340],{},"*\u002F\n",[454,1342,1343],{"class":456,"line":517},[454,1344,691],{"emptyLinePlaceholder":690},[454,1346,1347],{"class":456,"line":523},[454,1348,1349],{},"$result = json_decode($res->getBody()->getContents());\n",[454,1351,1352],{"class":456,"line":529},[454,1353,691],{"emptyLinePlaceholder":690},[454,1355,1356],{"class":456,"line":535},[454,1357,1358],{},"$user->access_token= $result->access_token;\n",[454,1360,1361],{"class":456,"line":541},[454,1362,1363],{},"$user->refresh_token= $result->refresh_token;\n",[454,1365,1366],{"class":456,"line":547},[454,1367,1368],{},"$unixTime = time();\n",[454,1370,1371],{"class":456,"line":553},[454,1372,1373],{},"$user->zoom_expires_in= date(\"Y-m-d H:i:s\",$unixTime+$result->expires_in);\n",[454,1375,1376],{"class":456,"line":559},[454,1377,1378],{},"$user->save();\n",[454,1380,1381],{"class":456,"line":1028},[454,1382,691],{"emptyLinePlaceholder":690},[454,1384,1385],{"class":456,"line":1034},[454,1386,1387],{},"return redirect()->route('amdin')->with([\n",[454,1389,1390],{"class":456,"line":1039},[454,1391,1392],{},"         'noZoomCode'=>false,\n",[454,1394,1395],{"class":456,"line":1045},[454,1396,1397],{},"         'oauthSuccess'=>true\n",[454,1399,1400],{"class":456,"line":1050},[454,1401,841],{},[13,1403,1404],{},"そして最後は管理画面へリダイレクトしてあげます。管理者からしてみるとzoomの画面で「許可」を押すと元のサイトに戻って、グルグルローディングしてるなーと思ったら管理画面に戻ってきた感覚となります。実際の画面ではzoom連携の警告がなくなり以下の様な感じになります。",[108,1406],{":src":1407,":width":111},"'_mix\u002Fsch-2021-01-10-20.30.08-768x155.png'",[13,1409,1410],{},"これでOAuthは完了です。意外と簡単ですね。access_tokenは1時間で切れてしまうので、APIアクセスの際は期限切れでないかをチェック、そしてダメならtokenをリフレッシュする機能が必要となります。次はaccess_tokenのチェック方ら連携した人のユーザー情報を取得するとともに、リフレッシュ機能を実装します。",[36,1412,1413],{"id":1413},"ユーザー情報を取得",[13,1415,1416],{},"ミーティングを作成したりなどはユーザーIDが必要となります。他のAPIで使用するのでコントローラー内の共通メソッドとして分離しておきましょう。以下の様にします。",[402,1418,1421],{"className":447,"code":1419,"filename":1420,"language":450,"meta":408,"style":408},"class ZoomApiController extends Controller\n{\n    \u002F\u002F\n    protected function me(){\n        $user = auth()->user();\n        $client = new \\GuzzleHttp\\Client([\n            'headers' => ['Authorization' => 'Bearer '.$user->access_token]\n        ]);\n        $res = $client->request('GET','https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002Fme');\n        $result = json_decode($res->getBody()->getContents());\n        \u002F\u002F dd($result);\n        return $result;\n    }\n...\n}\n","app\u002FHttp\u002FControllers\u002FZoomApiController.php",[294,1422,1423,1428,1432,1437,1442,1447,1451,1456,1460,1465,1469,1474,1479,1483,1488],{"__ignoreMap":408},[454,1424,1425],{"class":456,"line":457},[454,1426,1427],{},"class ZoomApiController extends Controller\n",[454,1429,1430],{"class":456,"line":463},[454,1431,466],{},[454,1433,1434],{"class":456,"line":469},[454,1435,1436],{},"    \u002F\u002F\n",[454,1438,1439],{"class":456,"line":475},[454,1440,1441],{},"    protected function me(){\n",[454,1443,1444],{"class":456,"line":481},[454,1445,1446],{},"        $user = auth()->user();\n",[454,1448,1449],{"class":456,"line":487},[454,1450,990],{},[454,1452,1453],{"class":456,"line":493},[454,1454,1455],{},"            'headers' => ['Authorization' => 'Bearer '.$user->access_token]\n",[454,1457,1458],{"class":456,"line":499},[454,1459,1000],{},[454,1461,1462],{"class":456,"line":505},[454,1463,1464],{},"        $res = $client->request('GET','https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002Fme');\n",[454,1466,1467],{"class":456,"line":511},[454,1468,1042],{},[454,1470,1471],{"class":456,"line":517},[454,1472,1473],{},"        \u002F\u002F dd($result);\n",[454,1475,1476],{"class":456,"line":523},[454,1477,1478],{},"        return $result;\n",[454,1480,1481],{"class":456,"line":529},[454,1482,1110],{},[454,1484,1485],{"class":456,"line":535},[454,1486,1487],{},"...\n",[454,1489,1490],{"class":456,"line":541},[454,1491,562],{},[13,1493,1494,1495,1498,1499,1502,1503,1506],{},"ユーザーテーブルに",[294,1496,1497],{},"access_token","があるので",[294,1500,1501],{},"$user = auth()->user();","で現在のログインユーザーを取り出して、",[294,1504,1505],{},"$user->access_token","にて出力します。",[13,1508,1509,1511,1512,1515,1516,1519],{},[294,1510,1497],{},"があればAPIへのアクセスはリクエストヘッダーにトークンを入れるだけでアクセスできます。リクエストヘッダーは",[294,1513,1514],{},"'headers' => ['Authorization' => 'Bearer '.$user->access_token]","です。さっきはBasicだったのが、",[294,1517,1518],{},"Bearer（ベアラー）","になっていますのでタイポに注意。",[13,1521,1522,1525],{},[294,1523,1524],{},"dd($result)","を有効にして出力してみると",[108,1527],{":src":1528,":width":262,":center":263},"'_mix\u002Fzoom_me.jpeg'",[13,1530,1531],{},"こんな感じのJSONが返ってきますので、適宜IDなどを使用します。",[36,1533,1534],{"id":1534},"リフレッシュ機能を実装",[13,1536,1537],{},"access tokenは1時間しか持たないのでもし期限切れになった際にはリフレッシュトークンを使用してトークンを更新します。ちなみにリフレッシュトークンの有効期限は15年です笑私の場合は以下の様に実装しました。",[402,1539,1541],{"className":447,"code":1540,"filename":1420,"language":450,"meta":408,"style":408},"protected function checkRefresh(){\n    $user = auth()->user();\n    $token_expires =  new \\DateTime($user->zoom_expires_in);\n    $now = new \\DateTime();\n\n    if($now >= $token_expires){\n        $basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));\n        $client = new \\GuzzleHttp\\Client([\n            'headers' => ['Authorization' => 'Basic '.$basic]\n        ]);\n        $res = $client->request('POST','https:\u002F\u002Fzoom.us\u002Foauth\u002Ftoken',[\n            'query' => [\n                'grant_type'=>'refresh_token',\n                'refresh_token'=>$user->refresh_token\n            ]\n        ]);\n        $result = json_decode($res->getBody()->getContents());\n\n        $user->access_token= $result->access_token;\n        $user->refresh_token= $result->access_token;\n        $unixTime = time();\n        $user->zoom_expires_in= date(\"Y-m-d H:i:s\",$unixTime+$result->expires_in);\n        $user->save();\n        return $user;\n    }\n    return $user;\n}\n",[294,1542,1543,1548,1552,1557,1562,1566,1571,1575,1579,1583,1587,1591,1595,1600,1605,1609,1613,1617,1621,1625,1630,1634,1638,1642,1647,1651,1656],{"__ignoreMap":408},[454,1544,1545],{"class":456,"line":457},[454,1546,1547],{},"protected function checkRefresh(){\n",[454,1549,1550],{"class":456,"line":463},[454,1551,645],{},[454,1553,1554],{"class":456,"line":469},[454,1555,1556],{},"    $token_expires =  new \\DateTime($user->zoom_expires_in);\n",[454,1558,1559],{"class":456,"line":475},[454,1560,1561],{},"    $now = new \\DateTime();\n",[454,1563,1564],{"class":456,"line":481},[454,1565,691],{"emptyLinePlaceholder":690},[454,1567,1568],{"class":456,"line":487},[454,1569,1570],{},"    if($now >= $token_expires){\n",[454,1572,1573],{"class":456,"line":493},[454,1574,985],{},[454,1576,1577],{"class":456,"line":499},[454,1578,990],{},[454,1580,1581],{"class":456,"line":505},[454,1582,995],{},[454,1584,1585],{"class":456,"line":511},[454,1586,1000],{},[454,1588,1589],{"class":456,"line":517},[454,1590,1005],{},[454,1592,1593],{"class":456,"line":523},[454,1594,1010],{},[454,1596,1597],{"class":456,"line":529},[454,1598,1599],{},"                'grant_type'=>'refresh_token',\n",[454,1601,1602],{"class":456,"line":535},[454,1603,1604],{},"                'refresh_token'=>$user->refresh_token\n",[454,1606,1607],{"class":456,"line":541},[454,1608,1031],{},[454,1610,1611],{"class":456,"line":547},[454,1612,1000],{},[454,1614,1615],{"class":456,"line":553},[454,1616,1042],{},[454,1618,1619],{"class":456,"line":559},[454,1620,691],{"emptyLinePlaceholder":690},[454,1622,1623],{"class":456,"line":1028},[454,1624,1053],{},[454,1626,1627],{"class":456,"line":1034},[454,1628,1629],{},"        $user->refresh_token= $result->access_token;\n",[454,1631,1632],{"class":456,"line":1039},[454,1633,1065],{},[454,1635,1636],{"class":456,"line":1045},[454,1637,1071],{},[454,1639,1640],{"class":456,"line":1050},[454,1641,976],{},[454,1643,1644],{"class":456,"line":1056},[454,1645,1646],{},"        return $user;\n",[454,1648,1649],{"class":456,"line":1062},[454,1650,1110],{},[454,1652,1653],{"class":456,"line":1068},[454,1654,1655],{},"    return $user;\n",[454,1657,1658],{"class":456,"line":1074},[454,1659,562],{},[13,1661,1662],{},"APIリクエストごとにトークンをチェックできる様にしています。有効期限をテーブルに保存してあるのでそれを比較して、現在時刻が有効期限を過ぎていたらリフレッシュ処理を行う様します。そして戻ってきたトークンをテーブルで更新させ、ユーザーモデルをreturnします。",[13,1664,1665],{},"有効期限ないであればそのままユーザーモデルを返却するという感じです。",[36,1667,1669],{"id":1668},"apiからミーティングを作成","APIからミーティングを作成",[13,1671,1672],{},"それではフォームから入力された値を元にミーティングを作れる様にしましょう。メール通知は機能してはいませんが、実装したコードはコメントアウトさせてますので、頑張れる人はメールも実装してみてください。",[13,1674,1675],{},"まずフォームから取得したミーティング情報を格納するテーブルを以下の様に定義して、マイグレーションを実施します。",[53,1677,1679],{"id":1678},"フォームミーティング管理テーブルを作成","フォーム＆ミーティング管理テーブルを作成",[402,1681,1684],{"className":447,"code":1682,"filename":1683,"language":450,"meta":408,"style":408},"\u003C?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass CreateMeeting extends Migration\n{\n    \u002F**\n     * Run the migrations.\n     *\n     * @return void\n     *\u002F\n    public function up()\n    {\n        Schema::create('meeting', function (Blueprint $table) {\n            $table->bigIncrements('id');\n            $table->string('name');\n            $table->string('company_name');\n            $table->string('email');\n            $table->longText('content');\n\n            $table->timestamp('start_at', 0)->default(DB::raw('CURRENT_TIMESTAMP'));\n            $table->longText('hash');\n            $table->boolean('is_canceled');\n\n            $table->longText('zoom_meeting_id');\n            $table->longText('zoom_join_url');\n            $table->longText('zoom_start_url');\n            $table->longText('zoom_password');\n            $table->timestamps();\n        });\n    }\n\n    \u002F**\n     * Reverse the migrations.\n     *\n     * @return void\n     *\u002F\n    public function down()\n    {\n        Schema::dropIfExists('meeting');\n    }\n}\n","database\u002Fmigrations\u002F2021_01_10_005145_create_meeting.php",[294,1685,1686,1691,1695,1700,1705,1710,1715,1719,1724,1728,1733,1738,1743,1748,1753,1758,1763,1768,1773,1778,1783,1788,1793,1797,1802,1807,1812,1816,1821,1826,1831,1836,1841,1846,1850,1855,1860,1866,1871,1876,1881,1887,1892,1898,1903],{"__ignoreMap":408},[454,1687,1688],{"class":456,"line":457},[454,1689,1690],{},"\u003C?php\n",[454,1692,1693],{"class":456,"line":463},[454,1694,691],{"emptyLinePlaceholder":690},[454,1696,1697],{"class":456,"line":469},[454,1698,1699],{},"use Illuminate\\Database\\Migrations\\Migration;\n",[454,1701,1702],{"class":456,"line":475},[454,1703,1704],{},"use Illuminate\\Database\\Schema\\Blueprint;\n",[454,1706,1707],{"class":456,"line":481},[454,1708,1709],{},"use Illuminate\\Support\\Facades\\Schema;\n",[454,1711,1712],{"class":456,"line":487},[454,1713,1714],{},"use Illuminate\\Support\\Facades\\DB;\n",[454,1716,1717],{"class":456,"line":493},[454,1718,691],{"emptyLinePlaceholder":690},[454,1720,1721],{"class":456,"line":499},[454,1722,1723],{},"class CreateMeeting extends Migration\n",[454,1725,1726],{"class":456,"line":505},[454,1727,466],{},[454,1729,1730],{"class":456,"line":511},[454,1731,1732],{},"    \u002F**\n",[454,1734,1735],{"class":456,"line":517},[454,1736,1737],{},"     * Run the migrations.\n",[454,1739,1740],{"class":456,"line":523},[454,1741,1742],{},"     *\n",[454,1744,1745],{"class":456,"line":529},[454,1746,1747],{},"     * @return void\n",[454,1749,1750],{"class":456,"line":535},[454,1751,1752],{},"     *\u002F\n",[454,1754,1755],{"class":456,"line":541},[454,1756,1757],{},"    public function up()\n",[454,1759,1760],{"class":456,"line":547},[454,1761,1762],{},"    {\n",[454,1764,1765],{"class":456,"line":553},[454,1766,1767],{},"        Schema::create('meeting', function (Blueprint $table) {\n",[454,1769,1770],{"class":456,"line":559},[454,1771,1772],{},"            $table->bigIncrements('id');\n",[454,1774,1775],{"class":456,"line":1028},[454,1776,1777],{},"            $table->string('name');\n",[454,1779,1780],{"class":456,"line":1034},[454,1781,1782],{},"            $table->string('company_name');\n",[454,1784,1785],{"class":456,"line":1039},[454,1786,1787],{},"            $table->string('email');\n",[454,1789,1790],{"class":456,"line":1045},[454,1791,1792],{},"            $table->longText('content');\n",[454,1794,1795],{"class":456,"line":1050},[454,1796,691],{"emptyLinePlaceholder":690},[454,1798,1799],{"class":456,"line":1056},[454,1800,1801],{},"            $table->timestamp('start_at', 0)->default(DB::raw('CURRENT_TIMESTAMP'));\n",[454,1803,1804],{"class":456,"line":1062},[454,1805,1806],{},"            $table->longText('hash');\n",[454,1808,1809],{"class":456,"line":1068},[454,1810,1811],{},"            $table->boolean('is_canceled');\n",[454,1813,1814],{"class":456,"line":1074},[454,1815,691],{"emptyLinePlaceholder":690},[454,1817,1818],{"class":456,"line":1079},[454,1819,1820],{},"            $table->longText('zoom_meeting_id');\n",[454,1822,1823],{"class":456,"line":1084},[454,1824,1825],{},"            $table->longText('zoom_join_url');\n",[454,1827,1828],{"class":456,"line":1090},[454,1829,1830],{},"            $table->longText('zoom_start_url');\n",[454,1832,1833],{"class":456,"line":1096},[454,1834,1835],{},"            $table->longText('zoom_password');\n",[454,1837,1838],{"class":456,"line":1102},[454,1839,1840],{},"            $table->timestamps();\n",[454,1842,1843],{"class":456,"line":1107},[454,1844,1845],{},"        });\n",[454,1847,1848],{"class":456,"line":1113},[454,1849,1110],{},[454,1851,1853],{"class":456,"line":1852},35,[454,1854,691],{"emptyLinePlaceholder":690},[454,1856,1858],{"class":456,"line":1857},36,[454,1859,1732],{},[454,1861,1863],{"class":456,"line":1862},37,[454,1864,1865],{},"     * Reverse the migrations.\n",[454,1867,1869],{"class":456,"line":1868},38,[454,1870,1742],{},[454,1872,1874],{"class":456,"line":1873},39,[454,1875,1747],{},[454,1877,1879],{"class":456,"line":1878},40,[454,1880,1752],{},[454,1882,1884],{"class":456,"line":1883},41,[454,1885,1886],{},"    public function down()\n",[454,1888,1890],{"class":456,"line":1889},42,[454,1891,1762],{},[454,1893,1895],{"class":456,"line":1894},43,[454,1896,1897],{},"        Schema::dropIfExists('meeting');\n",[454,1899,1901],{"class":456,"line":1900},44,[454,1902,1110],{},[454,1904,1906],{"class":456,"line":1905},45,[454,1907,562],{},[13,1909,1910,1913],{},[294,1911,1912],{},"start_at","はお客さんが入力した希望zoom開催時間です。本来は管理側の都合を合わせた機能にすべきですが今回はプロトタイプなので割愛。フロントからはdatetime形式で来たものを受け取ります。",[13,1915,1916,1919],{},[294,1917,1918],{},"hash","は後でお客さんがミーティングをキャンセルしたり、時間を変更するときにアクセスするURLに付けるランダムな文字列です。一種のパスワードみたいなものです。後ほど使い方を解説します。",[13,1921,1922,1923,396,1926,1929],{},"そしてzoomへCreate Meeting API を送信すると、ミーティングURLなどが返ってきますのでそれを",[294,1924,1925],{},"zoom_join_url",[294,1927,1928],{},"zoom_start_url","に入れておきます 。他にフォームの内容を格納する箇所を定義してマイグレーションします。",[13,1931,1932],{},"フォームの画面以下の様に実装しました。",[108,1934],{":src":149,":width":111},[53,1936,1938],{"id":1937},"バリデーションとmeeting-apiをリクエスト","バリデーションとmeeting apiをリクエスト",[13,1940,1941],{},"フォームのビューがPOSTリクエストを受けたら以下のコントローラーが実行されます。",[402,1943,1945],{"className":447,"code":1944,"filename":1420,"language":450,"meta":408,"style":408},"public function createMeeting(Request $request){\n    $validator = Validator::make($request->all(),[\n        'email'=>'required|email:rfc',\n        'yourname'=>'required',\n        'companyname'=>'required',\n        'startAt'=>'date|required',\n        'content'=>'required|max:1000',\n    ]);\n\n    $error = $validator->getMessageBag()->toArray();\n    \n    \u002F\u002Fバリデーションエラーがあれば元の画面へ\n    if ($validator->fails()) {\n        return view('form',compact('error'));\n    }\n    \n    $user = $this->checkRefresh();\n    $user = auth()->user();\n\n    $zoom_user = $this->me();\n\n    $url = 'https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002F'.$zoom_user->id.'\u002Fmeetings';\n    $client = new \\GuzzleHttp\\Client([\n        'headers' => [\n            'Authorization' => 'Bearer '.$user->access_token,\n            'Content-Type'=>'application\u002Fjson'\n        ],\n    ]);\n\n    $topic = $request->companyname.' '.$request->yourname.'様 ご相談';\n    $meeting_password = substr(base_convert(bin2hex(openssl_random_pseudo_bytes(9)),16,36),0,9);\n    $res = $client->request('POST',$url,[\n        \\GuzzleHttp\\RequestOptions::JSON => [\n            'topic'=>$topic,\n            'type'=>2,\n            'start_time'=>$request->startAt,\n            'password'=>$meeting_password\n        ]\n    ]);\n    $result = json_decode($res->getBody()->getContents());\n\n    $meeting = new Meeting();\n    $meeting->name=$request->yourname;\n    $meeting->company_name=$request->companyname;\n    $meeting->email=$request->email;\n    $meeting->content=$request->content;\n\n    $start = new \\DateTime($result->start_time);\n    $meeting->start_at=$start;\n    $meeting->hash=substr(base_convert(bin2hex(openssl_random_pseudo_bytes(64)),16,36),0,64);\n    $meeting->is_canceled=false;\n\n    $meeting->zoom_meeting_id=$result->id;\n    $meeting->zoom_join_url=$result->join_url;\n    $meeting->zoom_start_url=$result->start_url;\n    $meeting->zoom_password=$result->password;\n    $meeting->save();\n\n    $format = $start->format('Y年m月d日 H時i分');\n    \u002F\u002F $meeting->start_at = $format;\n    \u002F\u002F $mail = new ContactMail($meeting);\n    \u002F\u002F Mail::to($request->email)->send($mail);\n\n\n    return redirect('\u002Fconfirm')->with([\n        'form_id'=>$meeting->id,\n        'name'=>$request->yourname,\n        'companyname'=>$request->companyname,\n        'content'=>$request->content,\n        'start_time'=>$format\n    ]);\n}\n",[294,1946,1947,1952,1957,1962,1967,1972,1977,1982,1986,1990,1995,1999,2004,2009,2014,2018,2022,2027,2031,2035,2040,2044,2049,2054,2059,2064,2069,2074,2078,2082,2087,2092,2097,2102,2107,2112,2117,2122,2127,2131,2136,2140,2145,2150,2155,2160,2166,2171,2177,2183,2189,2195,2200,2206,2212,2218,2224,2230,2235,2241,2247,2253,2259,2263,2268,2274,2280,2286,2292,2298,2304,2309],{"__ignoreMap":408},[454,1948,1949],{"class":456,"line":457},[454,1950,1951],{},"public function createMeeting(Request $request){\n",[454,1953,1954],{"class":456,"line":463},[454,1955,1956],{},"    $validator = Validator::make($request->all(),[\n",[454,1958,1959],{"class":456,"line":469},[454,1960,1961],{},"        'email'=>'required|email:rfc',\n",[454,1963,1964],{"class":456,"line":475},[454,1965,1966],{},"        'yourname'=>'required',\n",[454,1968,1969],{"class":456,"line":481},[454,1970,1971],{},"        'companyname'=>'required',\n",[454,1973,1974],{"class":456,"line":487},[454,1975,1976],{},"        'startAt'=>'date|required',\n",[454,1978,1979],{"class":456,"line":493},[454,1980,1981],{},"        'content'=>'required|max:1000',\n",[454,1983,1984],{"class":456,"line":499},[454,1985,675],{},[454,1987,1988],{"class":456,"line":505},[454,1989,691],{"emptyLinePlaceholder":690},[454,1991,1992],{"class":456,"line":511},[454,1993,1994],{},"    $error = $validator->getMessageBag()->toArray();\n",[454,1996,1997],{"class":456,"line":517},[454,1998,508],{},[454,2000,2001],{"class":456,"line":523},[454,2002,2003],{},"    \u002F\u002Fバリデーションエラーがあれば元の画面へ\n",[454,2005,2006],{"class":456,"line":529},[454,2007,2008],{},"    if ($validator->fails()) {\n",[454,2010,2011],{"class":456,"line":535},[454,2012,2013],{},"        return view('form',compact('error'));\n",[454,2015,2016],{"class":456,"line":541},[454,2017,1110],{},[454,2019,2020],{"class":456,"line":547},[454,2021,508],{},[454,2023,2024],{"class":456,"line":553},[454,2025,2026],{},"    $user = $this->checkRefresh();\n",[454,2028,2029],{"class":456,"line":559},[454,2030,645],{},[454,2032,2033],{"class":456,"line":1028},[454,2034,691],{"emptyLinePlaceholder":690},[454,2036,2037],{"class":456,"line":1034},[454,2038,2039],{},"    $zoom_user = $this->me();\n",[454,2041,2042],{"class":456,"line":1039},[454,2043,691],{"emptyLinePlaceholder":690},[454,2045,2046],{"class":456,"line":1045},[454,2047,2048],{},"    $url = 'https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002F'.$zoom_user->id.'\u002Fmeetings';\n",[454,2050,2051],{"class":456,"line":1050},[454,2052,2053],{},"    $client = new \\GuzzleHttp\\Client([\n",[454,2055,2056],{"class":456,"line":1056},[454,2057,2058],{},"        'headers' => [\n",[454,2060,2061],{"class":456,"line":1062},[454,2062,2063],{},"            'Authorization' => 'Bearer '.$user->access_token,\n",[454,2065,2066],{"class":456,"line":1068},[454,2067,2068],{},"            'Content-Type'=>'application\u002Fjson'\n",[454,2070,2071],{"class":456,"line":1074},[454,2072,2073],{},"        ],\n",[454,2075,2076],{"class":456,"line":1079},[454,2077,675],{},[454,2079,2080],{"class":456,"line":1084},[454,2081,691],{"emptyLinePlaceholder":690},[454,2083,2084],{"class":456,"line":1090},[454,2085,2086],{},"    $topic = $request->companyname.' '.$request->yourname.'様 ご相談';\n",[454,2088,2089],{"class":456,"line":1096},[454,2090,2091],{},"    $meeting_password = substr(base_convert(bin2hex(openssl_random_pseudo_bytes(9)),16,36),0,9);\n",[454,2093,2094],{"class":456,"line":1102},[454,2095,2096],{},"    $res = $client->request('POST',$url,[\n",[454,2098,2099],{"class":456,"line":1107},[454,2100,2101],{},"        \\GuzzleHttp\\RequestOptions::JSON => [\n",[454,2103,2104],{"class":456,"line":1113},[454,2105,2106],{},"            'topic'=>$topic,\n",[454,2108,2109],{"class":456,"line":1852},[454,2110,2111],{},"            'type'=>2,\n",[454,2113,2114],{"class":456,"line":1857},[454,2115,2116],{},"            'start_time'=>$request->startAt,\n",[454,2118,2119],{"class":456,"line":1862},[454,2120,2121],{},"            'password'=>$meeting_password\n",[454,2123,2124],{"class":456,"line":1868},[454,2125,2126],{},"        ]\n",[454,2128,2129],{"class":456,"line":1873},[454,2130,675],{},[454,2132,2133],{"class":456,"line":1878},[454,2134,2135],{},"    $result = json_decode($res->getBody()->getContents());\n",[454,2137,2138],{"class":456,"line":1883},[454,2139,691],{"emptyLinePlaceholder":690},[454,2141,2142],{"class":456,"line":1889},[454,2143,2144],{},"    $meeting = new Meeting();\n",[454,2146,2147],{"class":456,"line":1894},[454,2148,2149],{},"    $meeting->name=$request->yourname;\n",[454,2151,2152],{"class":456,"line":1900},[454,2153,2154],{},"    $meeting->company_name=$request->companyname;\n",[454,2156,2157],{"class":456,"line":1905},[454,2158,2159],{},"    $meeting->email=$request->email;\n",[454,2161,2163],{"class":456,"line":2162},46,[454,2164,2165],{},"    $meeting->content=$request->content;\n",[454,2167,2169],{"class":456,"line":2168},47,[454,2170,691],{"emptyLinePlaceholder":690},[454,2172,2174],{"class":456,"line":2173},48,[454,2175,2176],{},"    $start = new \\DateTime($result->start_time);\n",[454,2178,2180],{"class":456,"line":2179},49,[454,2181,2182],{},"    $meeting->start_at=$start;\n",[454,2184,2186],{"class":456,"line":2185},50,[454,2187,2188],{},"    $meeting->hash=substr(base_convert(bin2hex(openssl_random_pseudo_bytes(64)),16,36),0,64);\n",[454,2190,2192],{"class":456,"line":2191},51,[454,2193,2194],{},"    $meeting->is_canceled=false;\n",[454,2196,2198],{"class":456,"line":2197},52,[454,2199,691],{"emptyLinePlaceholder":690},[454,2201,2203],{"class":456,"line":2202},53,[454,2204,2205],{},"    $meeting->zoom_meeting_id=$result->id;\n",[454,2207,2209],{"class":456,"line":2208},54,[454,2210,2211],{},"    $meeting->zoom_join_url=$result->join_url;\n",[454,2213,2215],{"class":456,"line":2214},55,[454,2216,2217],{},"    $meeting->zoom_start_url=$result->start_url;\n",[454,2219,2221],{"class":456,"line":2220},56,[454,2222,2223],{},"    $meeting->zoom_password=$result->password;\n",[454,2225,2227],{"class":456,"line":2226},57,[454,2228,2229],{},"    $meeting->save();\n",[454,2231,2233],{"class":456,"line":2232},58,[454,2234,691],{"emptyLinePlaceholder":690},[454,2236,2238],{"class":456,"line":2237},59,[454,2239,2240],{},"    $format = $start->format('Y年m月d日 H時i分');\n",[454,2242,2244],{"class":456,"line":2243},60,[454,2245,2246],{},"    \u002F\u002F $meeting->start_at = $format;\n",[454,2248,2250],{"class":456,"line":2249},61,[454,2251,2252],{},"    \u002F\u002F $mail = new ContactMail($meeting);\n",[454,2254,2256],{"class":456,"line":2255},62,[454,2257,2258],{},"    \u002F\u002F Mail::to($request->email)->send($mail);\n",[454,2260,2261],{"class":456,"line":4},[454,2262,691],{"emptyLinePlaceholder":690},[454,2264,2266],{"class":456,"line":2265},64,[454,2267,691],{"emptyLinePlaceholder":690},[454,2269,2271],{"class":456,"line":2270},65,[454,2272,2273],{},"    return redirect('\u002Fconfirm')->with([\n",[454,2275,2277],{"class":456,"line":2276},66,[454,2278,2279],{},"        'form_id'=>$meeting->id,\n",[454,2281,2283],{"class":456,"line":2282},67,[454,2284,2285],{},"        'name'=>$request->yourname,\n",[454,2287,2289],{"class":456,"line":2288},68,[454,2290,2291],{},"        'companyname'=>$request->companyname,\n",[454,2293,2295],{"class":456,"line":2294},69,[454,2296,2297],{},"        'content'=>$request->content,\n",[454,2299,2301],{"class":456,"line":2300},70,[454,2302,2303],{},"        'start_time'=>$format\n",[454,2305,2307],{"class":456,"line":2306},71,[454,2308,675],{},[454,2310,2312],{"class":456,"line":2311},72,[454,2313,562],{},[13,2315,2316],{},"長いですが、バリデーションからAPIのアクセスまで一通り行われています。",[60,2318,2319],{"id":2319},"有効期限チェックとエンドポイントリクエストの作成",[13,2321,2322],{},"まずは最初の方では有効期限のチェックを行い、そしてミーティングを作成するユーザー情報を取得しています。",[402,2324,2326],{"className":447,"code":2325,"filename":1420,"language":450,"meta":408,"style":408},"$user = $this->checkRefresh();\n$user = auth()->user();\n\n$zoom_user = $this->me();\n\n$url = 'https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002F'.$zoom_user->id.'\u002Fmeetings';\n$client = new \\GuzzleHttp\\Client([\n    'headers' => [\n        'Authorization' => 'Bearer '.$user->access_token,\n        'Content-Type'=>'application\u002Fjson'\n    ],\n]);\n",[294,2327,2328,2333,2338,2342,2347,2351,2356,2360,2365,2370,2375,2380],{"__ignoreMap":408},[454,2329,2330],{"class":456,"line":457},[454,2331,2332],{},"$user = $this->checkRefresh();\n",[454,2334,2335],{"class":456,"line":463},[454,2336,2337],{},"$user = auth()->user();\n",[454,2339,2340],{"class":456,"line":469},[454,2341,691],{"emptyLinePlaceholder":690},[454,2343,2344],{"class":456,"line":475},[454,2345,2346],{},"$zoom_user = $this->me();\n",[454,2348,2349],{"class":456,"line":481},[454,2350,691],{"emptyLinePlaceholder":690},[454,2352,2353],{"class":456,"line":487},[454,2354,2355],{},"$url = 'https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002F'.$zoom_user->id.'\u002Fmeetings';\n",[454,2357,2358],{"class":456,"line":493},[454,2359,1145],{},[454,2361,2362],{"class":456,"line":499},[454,2363,2364],{},"    'headers' => [\n",[454,2366,2367],{"class":456,"line":505},[454,2368,2369],{},"        'Authorization' => 'Bearer '.$user->access_token,\n",[454,2371,2372],{"class":456,"line":511},[454,2373,2374],{},"        'Content-Type'=>'application\u002Fjson'\n",[454,2376,2377],{"class":456,"line":517},[454,2378,2379],{},"    ],\n",[454,2381,2382],{"class":456,"line":523},[454,2383,841],{},[13,2385,2386,2387,626,2390,875,2393,2396,2397,2400],{},"ミーティングの作成を行うエンドポイントは ",[294,2388,2389],{},"https:\u002F\u002Fapi.zoom.us\u002Fv2\u002Fusers\u002F{zoom_user_id}\u002Fmeetings",[294,2391,2392],{},"{zoom_user_id}",[294,2394,2395],{},"me","で取得した",[294,2398,2399],{},"user_id","（連携したzoomアカウントのuser id）を挿入します。そしてリクエストヘッダーを付けてひとまず、GuzzleHttpのインスタンスを作成します。",[60,2402,2404],{"id":2403},"ミーティングパスワードを設定してapiへリクエスト","ミーティングパスワードを設定してAPIへリクエスト",[402,2406,2408],{"className":447,"code":2407,"filename":1420,"language":450,"meta":408,"style":408},"$topic = $request->companyname.' '.$request->yourname.'様 ご相談';        \n$meeting_password = substr(base_convert(bin2hex(openssl_random_pseudo_bytes(9)),16,36),0,9);\n$res = $client->request('POST',$url,[\n    \\GuzzleHttp\\RequestOptions::JSON => [\n        'topic'=>$topic,\n        'type'=>2,\n        'start_time'=>$request->startAt,\n        'password'=>$meeting_password\n    ]\n]);\n$result = json_decode($res->getBody()->getContents());\n",[294,2409,2410,2415,2420,2425,2430,2435,2440,2445,2450,2454,2458],{"__ignoreMap":408},[454,2411,2412],{"class":456,"line":457},[454,2413,2414],{},"$topic = $request->companyname.' '.$request->yourname.'様 ご相談';        \n",[454,2416,2417],{"class":456,"line":463},[454,2418,2419],{},"$meeting_password = substr(base_convert(bin2hex(openssl_random_pseudo_bytes(9)),16,36),0,9);\n",[454,2421,2422],{"class":456,"line":469},[454,2423,2424],{},"$res = $client->request('POST',$url,[\n",[454,2426,2427],{"class":456,"line":475},[454,2428,2429],{},"    \\GuzzleHttp\\RequestOptions::JSON => [\n",[454,2431,2432],{"class":456,"line":481},[454,2433,2434],{},"        'topic'=>$topic,\n",[454,2436,2437],{"class":456,"line":487},[454,2438,2439],{},"        'type'=>2,\n",[454,2441,2442],{"class":456,"line":493},[454,2443,2444],{},"        'start_time'=>$request->startAt,\n",[454,2446,2447],{"class":456,"line":499},[454,2448,2449],{},"        'password'=>$meeting_password\n",[454,2451,2452],{"class":456,"line":505},[454,2453,1184],{},[454,2455,2456],{"class":456,"line":511},[454,2457,841],{},[454,2459,2460],{"class":456,"line":517},[454,2461,1349],{},[13,2463,2464,2465,2470],{},"お客さんに入力してもらう様のパスワードを生成し、そして指定したエンドポイントへPOSTします。ここでPOSTパラメーター内にミーティング設定情報をJSONで記入します。どんな値が設定できるかは",[17,2466,2469],{"href":2467,"rel":2468},"https:\u002F\u002Fmarketplace.zoom.us\u002Fdocs\u002Fapi-reference\u002Fzoom-api\u002Fmeetings\u002Fmeetingcreate",[21],"ここで確認","できます。",[13,2472,2473],{},"上手く想像できない方は以下のGUIで行うzoom画面を参考にするといいです",[108,2475],{":src":2476,":width":262,":center":263},"'_mix\u002Fsch-2021-01-10-21.02.47-768x480.png'",[13,2478,2479,2480,2483,2484,2487],{},"ここで入力できる値は全て、APIでも入力できますのでリファランスでフォーマットなど確認しながら自分なりの設定をしましょう。私の場合、まずトピックを",[294,2481,2482],{},"「{会社名}　{客名様}　ご相談」","として必ず定義しており、そこは",[294,2485,2486],{},"'topic'=>$topic,","と定義してます。",[13,2489,2490],{},"start_timeは開催日時であり、フォームで入力された値を入れています。passwordは念のため付与しています。そしてAPIをリクエストします。",[60,2492,2494],{"id":2493},"api処理終了後","API処理終了後",[402,2496,2498],{"className":447,"code":2497,"filename":1420,"language":450,"meta":408,"style":408},"$result = json_decode($res->getBody()->getContents());\n\n$meeting = new Meeting();\n$meeting->name=$request->yourname;\n$meeting->company_name=$request->companyname;\n$meeting->email=$request->email;\n$meeting->content=$request->content;\n\n$start = new \\DateTime($result->start_time);\n$meeting->start_at=$start;\n$meeting->hash=substr(base_convert(bin2hex(openssl_random_pseudo_bytes(64)),16,36),0,64);\n$meeting->is_canceled=false;\n\n$meeting->zoom_meeting_id=$result->id;\n$meeting->zoom_join_url=$result->join_url;\n$meeting->zoom_start_url=$result->start_url;\n$meeting->zoom_password=$result->password;\n$meeting->save();\n\n$format = $start->format('Y年m月d日 H時i分');\n\u002F\u002F $meeting->start_at = $format;\n\u002F\u002F $mail = new ContactMail($meeting);\n\u002F\u002F Mail::to($request->email)->send($mail);\n\n\nreturn redirect('\u002Fconfirm')->with([\n    'form_id'=>$meeting->id,\n    'name'=>$request->yourname,\n    'companyname'=>$request->companyname,\n    'content'=>$request->content,\n    'start_time'=>$format\n]);\n",[294,2499,2500,2504,2508,2513,2518,2523,2528,2533,2537,2542,2547,2552,2557,2561,2566,2571,2576,2581,2586,2590,2595,2600,2605,2610,2614,2618,2623,2628,2633,2638,2643,2648],{"__ignoreMap":408},[454,2501,2502],{"class":456,"line":457},[454,2503,1349],{},[454,2505,2506],{"class":456,"line":463},[454,2507,691],{"emptyLinePlaceholder":690},[454,2509,2510],{"class":456,"line":469},[454,2511,2512],{},"$meeting = new Meeting();\n",[454,2514,2515],{"class":456,"line":475},[454,2516,2517],{},"$meeting->name=$request->yourname;\n",[454,2519,2520],{"class":456,"line":481},[454,2521,2522],{},"$meeting->company_name=$request->companyname;\n",[454,2524,2525],{"class":456,"line":487},[454,2526,2527],{},"$meeting->email=$request->email;\n",[454,2529,2530],{"class":456,"line":493},[454,2531,2532],{},"$meeting->content=$request->content;\n",[454,2534,2535],{"class":456,"line":499},[454,2536,691],{"emptyLinePlaceholder":690},[454,2538,2539],{"class":456,"line":505},[454,2540,2541],{},"$start = new \\DateTime($result->start_time);\n",[454,2543,2544],{"class":456,"line":511},[454,2545,2546],{},"$meeting->start_at=$start;\n",[454,2548,2549],{"class":456,"line":517},[454,2550,2551],{},"$meeting->hash=substr(base_convert(bin2hex(openssl_random_pseudo_bytes(64)),16,36),0,64);\n",[454,2553,2554],{"class":456,"line":523},[454,2555,2556],{},"$meeting->is_canceled=false;\n",[454,2558,2559],{"class":456,"line":529},[454,2560,691],{"emptyLinePlaceholder":690},[454,2562,2563],{"class":456,"line":535},[454,2564,2565],{},"$meeting->zoom_meeting_id=$result->id;\n",[454,2567,2568],{"class":456,"line":541},[454,2569,2570],{},"$meeting->zoom_join_url=$result->join_url;\n",[454,2572,2573],{"class":456,"line":547},[454,2574,2575],{},"$meeting->zoom_start_url=$result->start_url;\n",[454,2577,2578],{"class":456,"line":553},[454,2579,2580],{},"$meeting->zoom_password=$result->password;\n",[454,2582,2583],{"class":456,"line":559},[454,2584,2585],{},"$meeting->save();\n",[454,2587,2588],{"class":456,"line":1028},[454,2589,691],{"emptyLinePlaceholder":690},[454,2591,2592],{"class":456,"line":1034},[454,2593,2594],{},"$format = $start->format('Y年m月d日 H時i分');\n",[454,2596,2597],{"class":456,"line":1039},[454,2598,2599],{},"\u002F\u002F $meeting->start_at = $format;\n",[454,2601,2602],{"class":456,"line":1045},[454,2603,2604],{},"\u002F\u002F $mail = new ContactMail($meeting);\n",[454,2606,2607],{"class":456,"line":1050},[454,2608,2609],{},"\u002F\u002F Mail::to($request->email)->send($mail);\n",[454,2611,2612],{"class":456,"line":1056},[454,2613,691],{"emptyLinePlaceholder":690},[454,2615,2616],{"class":456,"line":1062},[454,2617,691],{"emptyLinePlaceholder":690},[454,2619,2620],{"class":456,"line":1068},[454,2621,2622],{},"return redirect('\u002Fconfirm')->with([\n",[454,2624,2625],{"class":456,"line":1074},[454,2626,2627],{},"    'form_id'=>$meeting->id,\n",[454,2629,2630],{"class":456,"line":1079},[454,2631,2632],{},"    'name'=>$request->yourname,\n",[454,2634,2635],{"class":456,"line":1084},[454,2636,2637],{},"    'companyname'=>$request->companyname,\n",[454,2639,2640],{"class":456,"line":1090},[454,2641,2642],{},"    'content'=>$request->content,\n",[454,2644,2645],{"class":456,"line":1096},[454,2646,2647],{},"    'start_time'=>$format\n",[454,2649,2650],{"class":456,"line":1102},[454,2651,841],{},[13,2653,2654],{},"$resultにzoomからのレスポンスがあるので適宜Meetingモデルやメールに格納します。そして最後にユーザーは確認画面が表示されます。",[36,2656,2657],{"id":2657},"自分のアカウントで実験",[13,2659,2660],{},"では実験してみます。連携した管理者のzoomアカウントでミーティング一覧をみています。リクエスト前はこの様に何もありません。",[108,2662],{":src":2663,":width":111},"'_mix\u002Fzoom_meeting_index-768x322.jpeg'",[13,2665,2666],{},"そこでフォームにこの様に入力していきます。（今回は管理者自身が入力）",[108,2668],{":src":2669,":width":111},"'_mix\u002Fsch-2021-01-10-21.10.26-768x699.png'",[13,2671,2672],{},"そして送信を押すとちょっとロードして、こちらの画面にリダイレクトされます。",[108,2674],{":src":2675,":width":111},"'_mix\u002Fzoom_confirm-768x902.jpeg'",[13,2677,2678],{},"そして先ほどのzoom一覧を見てみると、",[108,2680],{":src":2681,":width":111},"'_mix\u002Fzoom_meeting_create-768x412.jpeg'",[13,2683,2684],{},"JUNE様ですね。きちんとミーティングが作られています。（時間がずれているのはコンテナ側のタイムゾーンの設定をすっかり忘れていたからです。)そしてテーブルを見てみると",[108,2686],{":src":2687,":width":111},"'_mix\u002Fsch-2021-01-10-21.20.37-768x75.png'",[13,2689,2690],{},"きちんと作られていました。zoom_join_urlの値にアクセスすると",[108,2692],{":src":2693,":width":111},"'_mix\u002Fzoom_url_test-768x467.jpeg'",[13,2695,2696],{},"管理者が直接言っているのでミーティングの開始となっていますが、きちんと有効なミーティングURLを取得し保存できています。本来であればメールでお客様にこのURLとパスワードをお知らせします。",[13,2698,2699],{},"また管理画面では",[108,2701],{":src":2702,":width":111},"'_mix\u002Fsch-2021-01-10-21.24.07-768x143.png'",[13,2704,2705,2706,2709,2710,2713,2714,2717],{},"この様にして一覧で確認ができます。ちなみに「ミーティングを削除」などのボタンは",[294,2707,2708],{},"http:\u002F\u002Flocalhost:9000\u002Fform\u002Fdelete?hash=cgckkwc040okko..","へリンクされています。",[294,2711,2712],{},"form\u002Fdelete","でミーティングを削除する確認画面へ飛べます。そして照合のために",[294,2715,2716],{},"hash=cgckkwc040okko..","の値を用いています。（途中にあった$hashの値です。）",[13,2719,2720],{},"お客様が匿名であり、ユーザーセッションによる識別ができない時は、予測困難なハッシュ付きURLをお客様だけのメールに渡してミーティングの制御が可能です。",[36,2722,2723],{"id":2723},"まとめ",[13,2725,2726],{},"以上がアプリの実装の流れです。今回作成したOAuth認証zoom アプリはプライベートなので作った本人しか今は利用できませんが、しっかり実装して審査を受けることでマーケットプレイスに出店して自由に使用してもらうことが可能になります。",[13,2728,2729,2730,2734],{},"OAuthも意外と簡単でしたが、公式のAPIリクエストライブラリが出ているわけでないので本格的な開発の際には独自のzoom APIライブラリを作っておくといいかもしれません。zoomでできることはこれだけでないので、もっと色々調べてみようと思います。ひとまず今回のzoomアプリはここまでとします。",[17,2731,2733],{"href":187,"rel":2732},[21],"一応リポジトリに公開してある","のでぜひクローンして遊んでみてください。",[53,2736,2738],{"id":2737},"追記-2021-3-31","追記 2021 3 31",[13,2740,2741],{},"なんか、これぐらいの規模で特定のアプリでの利用であればJWTでも十分でした汗。JWT編もそのうちやろうと思います。",[2743,2744,2745],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":408,"searchDepth":469,"depth":469,"links":2747},[2748,2756,2759,2760,2769,2774,2778,2779,2780,2788,2789],{"id":38,"depth":463,"text":39,"children":2749},[2750,2754,2755],{"id":55,"depth":469,"text":55,"children":2751},[2752,2753],{"id":62,"depth":475,"text":63},{"id":81,"depth":475,"text":82},{"id":103,"depth":469,"text":103},{"id":136,"depth":469,"text":137},{"id":143,"depth":463,"text":143,"children":2757},[2758],{"id":155,"depth":469,"text":155},{"id":173,"depth":463,"text":173},{"id":226,"depth":463,"text":227,"children":2761},[2762,2763],{"id":230,"depth":469,"text":231},{"id":269,"depth":469,"text":269,"children":2764},[2765,2766,2767,2768],{"id":272,"depth":475,"text":272},{"id":343,"depth":475,"text":343},{"id":352,"depth":475,"text":353},{"id":365,"depth":475,"text":365},{"id":374,"depth":463,"text":375,"children":2770},[2771,2772,2773],{"id":381,"depth":469,"text":382},{"id":440,"depth":469,"text":441},{"id":598,"depth":469,"text":598},{"id":615,"depth":463,"text":616,"children":2775},[2776,2777],{"id":619,"depth":469,"text":619},{"id":918,"depth":469,"text":919},{"id":1413,"depth":463,"text":1413},{"id":1534,"depth":463,"text":1534},{"id":1668,"depth":463,"text":1669,"children":2781},[2782,2783],{"id":1678,"depth":469,"text":1679},{"id":1937,"depth":469,"text":1938,"children":2784},[2785,2786,2787],{"id":2319,"depth":475,"text":2319},{"id":2403,"depth":475,"text":2404},{"id":2493,"depth":475,"text":2494},{"id":2657,"depth":463,"text":2657},{"id":2723,"depth":463,"text":2723,"children":2790},[2791],{"id":2737,"depth":469,"text":2738},[2793],"devstack","2025-08-14","md",null,{},"\u002Farticles\u002Fzoom-api-laravel",{"title":8,"description":8},"articles\u002Fzoom-api-laravel",[2802,2803],"laravel","zoom","_mix\u002Ftop-768x463.jpeg","jNr1TCK61sAidVSCnsx9vzTvrRaOg6Bur8MSs3PYh8I",{"id":2807,"title":2808,"body":2809,"category":3687,"createdAt":3688,"description":3689,"extension":2795,"index":2796,"meta":3690,"navigation":690,"path":3691,"publish":690,"seo":3692,"series":2796,"seriesTitle":2796,"stem":3693,"tag":3694,"thumbnail":3696,"updatedAt":3688,"__hash__":3697},"articles\u002Farticles\u002Fs3-clioudfront-website.md","AWS S3を使ってSSL&独自ドメインの静的ブログをホストする",{"type":10,"value":2810,"toc":3669},[2811,2814,2831,2834,2839,2842,2850,2857,2861,2864,2867,2874,2877,2880,2883,2886,2905,2908,2911,2914,2917,2920,2923,3111,3127,3141,3147,3150,3153,3157,3160,3177,3180,3184,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3238,3242,3245,3248,3251,3254,3257,3260,3263,3266,3269,3272,3276,3285,3292,3295,3303,3306,3309,3312,3320,3323,3333,3340,3343,3346,3349,3352,3356,3362,3387,3390,3403,3412,3416,3423,3427,3430,3436,3439,3442,3445,3448,3451,3454,3457,3460,3463,3466,3487,3490,3494,3511,3539,3561,3567,3575,3578,3581,3595,3653,3656,3659,3663,3666],[13,2812,2813],{},"こんにちはjunです。会社で静的書き出ししたコンテンツをs3でホストし、cloudfrontを用いてwebサーバーの様にブログ内容をリクエストできる様な構成を作成しました。せっかくなので自分のブログも同じ様にやってみようと思い、復習兼ねて記事にしようと思います。今回は以下のことを説明します。",[192,2815,2816,2819,2822,2825,2828],{},[119,2817,2818],{},"S3の設定",[119,2820,2821],{},"cloudfrontの設定",[119,2823,2824],{},"独自ドメインの設定（DNSはお名前ドットコム）",[119,2826,2827],{},"IAMでのデプロイユーザーの作成",[119,2829,2830],{},"nuxt generate で転送する方法",[13,2832,2833],{},"私のこのブログはnuxt content を用いてnuxt generate をして静的書き出したものをサーバに転送しています。今まではレンタルサーバーの一部ディレクトリを使用していましたが、AWSの勉強ややってみたい機能をつけやすくするためにS3へ引っ越しました。それでは早速やっていきましょう。",[864,2835,2838],{"className":2836},[867,2837],"alert-danger","\nこの記事が説明している内容は2021年10月時点の情報です。\n",[36,2840,2841],{"id":2841},"参考資料",[13,2843,2844,2845],{},"\b",[17,2846,2849],{"href":2847,"rel":2848},"https:\u002F\u002Faws.amazon.com\u002Fjp\u002Fpremiumsupport\u002Fknowledge-center\u002Fcloudfront-serve-static-website#Using_a_website_endpoint_as_the_origin.2C_with_anonymous_.28public.29_access_allowed",[21],"CloudFront を使用して、Amazon S3 でホストされた静的ウェブサイトを公開するにはどうすればよいですか?",[13,2851,2852],{},[17,2853,2856],{"href":2854,"rel":2855},"https:\u002F\u002Fdocs.aws.amazon.com\u002Fja_jp\u002FRoute53\u002Flatest\u002FDeveloperGuide\u002Frouting-to-cloudfront-distribution.html",[21],"ドメイン名を使用したトラフィックの Amazon CloudFront ディストリビューションへのルーティング",[36,2858,2860],{"id":2859},"s3でデプロイ用のバケットを作成する","S3でデプロイ用のバケットを作成する。",[13,2862,2863],{},"では最初にまずホスティング元であるバケットを作成します。",[108,2865],{":src":2866,":width":111},"'s3-clioudfront-website\u002Fsch-iam-4.png'",[13,2868,2869,2870,2873],{},"バケットの名前は ",[289,2871,2872],{},"ホストするドメインの名前と同じ"," になるようにします。私の場合はバケット名が「jun-app.com」です。",[108,2875],{":src":2876,":width":111},"'s3-clioudfront-website\u002Fsch-s3-1.png'",[13,2878,2879],{},"設定した後にバケット一覧から選択し、プロパティを選びます。プロパティの一番下までスクロールすると「静的ウェブサイトホスティング」とあるのでここを編集します。",[108,2881],{":src":2882,":width":111},"'s3-clioudfront-website\u002Fsch-s3-2.png'",[108,2884],{":src":2885,":width":111},"'s3-clioudfront-website\u002Fsch-s3-3.png'",[13,2887,2888,2889,2892,2893,2896,2897,2900,2901,2904],{},"無効から有効に変更し、インデックスドキュメントに",[294,2890,2891],{},"index.html","を入力します。こうすると",[294,2894,2895],{},"https:\u002F\u002Fjun-app.com\u002Fdir\u002F","みたいにパスだけであっても",[294,2898,2899],{},"https:\u002F\u002Fjun-app.com\u002Fdir\u002Findex.html","に接続してくれます。また、エラードキュメントを指定しておくことで404リクエストの際のフォールバックとして利用できます。私の場合はルート直下の ",[294,2902,2903],{},"404.html","を指定しています。",[108,2906],{":src":2907,":width":111},"'s3-clioudfront-website\u002Fsch-s3-4.png'",[13,2909,2910],{},"編集後もういちど静的ウェブサイトホスティングの設定までいくと、一応URLが提供されますがアクセスしても「403 Forbidden」となります。これはバケットに対してパブリックアクセスがないからです。バケット作成時、デフォルトでは全てのパブリックアクセスが制限されていたので、その制限を外してあげます。バケットの画面から「アクセス許可」を選択し、「ブロックパブリックアクセス (バケット設定)」の編集をします。（ここを見ると「パブリックアクセスをすべて ブロック」が有効になっている）",[108,2912],{":src":2913,":width":111},"'s3-clioudfront-website\u002Fsch-s3-5.png'",[13,2915,2916],{},"編集の画面で「パブリックアクセスをすべて ブロック」のチェックを外して、全てのブロックを外します。",[108,2918],{":src":2919,":width":111},"'s3-clioudfront-website\u002Fsch-s3-6.png'",[13,2921,2922],{},"まだ終わりではありません。先ほどのはバケット単位の設定であり、アップロードされるオブジェクト（静的ファイル）には公開される設定がされません。毎回オブジェクトに対してパブリックアクセスを与えるのは面倒なので、「アクセス許可」の画面にあるバケットポリシーを以下のように編集します。",[402,2924,2928],{"className":2925,"code":2926,"language":2927,"meta":408,"style":408},"language-JSON shiki shiki-themes material-theme-ocean","{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"PublicReadGetObject\",\n            \"Effect\": \"Allow\",\n            \"Principal\": \"*\",\n            \"Action\": [\n                \"s3:GetObject\"\n            ],\n            \"Resource\": [\n                \"arn:aws:s3:::your-buget-name\u002F*\"\n            ]\n        }\n    ]\n}\n","JSON",[294,2929,2930,2935,2962,2976,2981,3003,3023,3043,3056,3067,3072,3085,3094,3098,3103,3107],{"__ignoreMap":408},[454,2931,2932],{"class":456,"line":457},[454,2933,466],{"class":2934},"sAklC",[454,2936,2937,2940,2944,2947,2950,2953,2957,2959],{"class":456,"line":463},[454,2938,2939],{"class":2934},"    \"",[454,2941,2943],{"class":2942},"sJ14y","Version",[454,2945,2946],{"class":2934},"\"",[454,2948,2949],{"class":2934},":",[454,2951,2952],{"class":2934}," \"",[454,2954,2956],{"class":2955},"sfyAc","2012-10-17",[454,2958,2946],{"class":2934},[454,2960,2961],{"class":2934},",\n",[454,2963,2964,2966,2969,2971,2973],{"class":456,"line":469},[454,2965,2939],{"class":2934},[454,2967,2968],{"class":2942},"Statement",[454,2970,2946],{"class":2934},[454,2972,2949],{"class":2934},[454,2974,2975],{"class":2934}," [\n",[454,2977,2978],{"class":456,"line":475},[454,2979,2980],{"class":2934},"        {\n",[454,2982,2983,2986,2990,2992,2994,2996,2999,3001],{"class":456,"line":481},[454,2984,2985],{"class":2934},"            \"",[454,2987,2989],{"class":2988},"s5Dmg","Sid",[454,2991,2946],{"class":2934},[454,2993,2949],{"class":2934},[454,2995,2952],{"class":2934},[454,2997,2998],{"class":2955},"PublicReadGetObject",[454,3000,2946],{"class":2934},[454,3002,2961],{"class":2934},[454,3004,3005,3007,3010,3012,3014,3016,3019,3021],{"class":456,"line":487},[454,3006,2985],{"class":2934},[454,3008,3009],{"class":2988},"Effect",[454,3011,2946],{"class":2934},[454,3013,2949],{"class":2934},[454,3015,2952],{"class":2934},[454,3017,3018],{"class":2955},"Allow",[454,3020,2946],{"class":2934},[454,3022,2961],{"class":2934},[454,3024,3025,3027,3030,3032,3034,3036,3039,3041],{"class":456,"line":493},[454,3026,2985],{"class":2934},[454,3028,3029],{"class":2988},"Principal",[454,3031,2946],{"class":2934},[454,3033,2949],{"class":2934},[454,3035,2952],{"class":2934},[454,3037,3038],{"class":2955},"*",[454,3040,2946],{"class":2934},[454,3042,2961],{"class":2934},[454,3044,3045,3047,3050,3052,3054],{"class":456,"line":499},[454,3046,2985],{"class":2934},[454,3048,3049],{"class":2988},"Action",[454,3051,2946],{"class":2934},[454,3053,2949],{"class":2934},[454,3055,2975],{"class":2934},[454,3057,3058,3061,3064],{"class":456,"line":505},[454,3059,3060],{"class":2934},"                \"",[454,3062,3063],{"class":2955},"s3:GetObject",[454,3065,3066],{"class":2934},"\"\n",[454,3068,3069],{"class":456,"line":511},[454,3070,3071],{"class":2934},"            ],\n",[454,3073,3074,3076,3079,3081,3083],{"class":456,"line":517},[454,3075,2985],{"class":2934},[454,3077,3078],{"class":2988},"Resource",[454,3080,2946],{"class":2934},[454,3082,2949],{"class":2934},[454,3084,2975],{"class":2934},[454,3086,3087,3089,3092],{"class":456,"line":523},[454,3088,3060],{"class":2934},[454,3090,3091],{"class":2955},"arn:aws:s3:::your-buget-name\u002F*",[454,3093,3066],{"class":2934},[454,3095,3096],{"class":456,"line":529},[454,3097,1031],{"class":2934},[454,3099,3100],{"class":456,"line":535},[454,3101,3102],{"class":2934},"        }\n",[454,3104,3105],{"class":456,"line":541},[454,3106,1184],{"class":2934},[454,3108,3109],{"class":456,"line":547},[454,3110,562],{"class":2934},[13,3112,3113,3114,3116,3117,3119,3120,3122,3123,3126],{},"上記のJSONは",[294,3115,3029],{},"はこのポリシーを付与するユーザーです。",[294,3118,3038],{},"としてゲストを含む全てのユーザーに",[294,3121,3063],{},"のアクション、つまりファイルの読み取りを",[294,3124,3125],{},"\"arn:aws:s3:::your-buget-name\u002F*\"","のバケットオブジェクトに許可するという意味です。",[13,3128,3129,3132,3133,3136,3137,3140],{},[294,3130,3131],{},"your-buget-name","にはあなたが設定したバケット名を入力します。そしてそのバケット配下の全オブジェクトに適用するため",[294,3134,3135],{},"your-buget-name\u002F*","とディレクトリのように指定します。",[294,3138,3139],{},"\"arn:aws:s3:::your-buget-name","という記述（Amazon リソースネーム）はバケットのプロパティで取得できます。",[13,3142,3143,3144,3146],{},"こうすればこのバケットないのファイルは公開されます。試しに",[294,3145,2891],{},"をアップロードして、提供されたオブジェクト URLにアクセスすると確かに見れます。",[108,3148],{":src":3149,":width":111},"'s3-clioudfront-website\u002Fsch-s3-7.png'",[13,3151,3152],{},"これでS3側の設定が完了しました。ただしドメインはS3の初期状態のままです。独自ドメインを割り当てとSSL化を行います。",[36,3154,3156],{"id":3155},"sslと独自ドメインを割り当てる","SSLと独自ドメインを割り当てる",[13,3158,3159],{},"SSL化した独自ドメインをS3に接続させるためにはcloudfrontの力が必要です。また今回使用するドメイン「jun-app.com」はお名前ドットコムで取得しているので、CNAMEを加えるなどの設定が必要です。クライアントがコンテンツを取得する流れは以下のようになります。",[116,3161,3162,3165,3168,3171,3174],{},[119,3163,3164],{},"クライアントがURLにアクセス",[119,3166,3167],{},"お名前ドットコムへDNS問い合わせ",[119,3169,3170],{},"お名前ドットコムはRoute53にNSを移譲しているので、問い合わせはRoute53へ",[119,3172,3173],{},"Route53 はcloudfrontのIPを返す",[119,3175,3176],{},"cloudfrontからS3に接続",[13,3178,3179],{},"といった流れです。",[53,3181,3183],{"id":3182},"route53と外部dnsの設定","Route53と外部DNSの設定",[13,3185,3186,3187,3192],{},"まずはRoute53でホストゾーンというものを作成して、お名前.comからNSサーバーをRoute53へ委任できるようにします。",[17,3188,3191],{"href":3189,"rel":3190},"https:\u002F\u002Fconsole.aws.amazon.com\u002Froute53\u002Fv2\u002Fhome#Dashboard",[21],"Route53","に移動して「ホストゾーンの作成」をします。",[108,3194],{":src":3195,":width":111},"'s3-clioudfront-website\u002Fsch-r53-1.png'",[13,3197,3198],{},"委任したいドメインを入力します。「パブリックホストゾーン」で大丈夫です。タグは管理上のものなので空欄で大丈夫です。問題なければ「ホストゾーンの作成」をクリックします。",[108,3200],{":src":3201,":width":111},"'s3-clioudfront-website\u002Fsch-r53-2.png'",[13,3203,3204],{},"作成後にはこのようにNSレコードとSOAレコードが作成されます。ドメイン自体が外部DNS（お名前.com）にある場合この「NSレコード」を指定することでRoute53に委任します。（ぼやけていますが４つ作成されます。）",[108,3206],{":src":3207,":width":111},"'s3-clioudfront-website\u002Fsch-r53-3.png'",[13,3209,3210],{},"このNSレコードをお名前.comの場合は「ネームサーバーの設定」を選択します。",[108,3212],{":src":3213,":width":111},"'s3-clioudfront-website\u002Fsch-r53-4.png'",[13,3215,3216],{},"そして「2.ネームサーバーの選択」＞「その他」＞「その他のネームサーバーを使う」にてRoute53で表示された４つのNSレコードをいれて「確認」クリックします。",[108,3218],{":src":3219,":width":111},"'s3-clioudfront-website\u002Fsch-r53-5.png'",[13,3221,3222],{},"「確認」の後にはDNSのNSが切り替わり、「jun-app.com」はまずお名前.comへ問い合わせられますが、結局Route53へたらい回しされます。これで「jun-app.com」のAレコードなどをRoute53で制御できるようになりました。",[13,3224,3225],{},"次はS3とcloudfrontと連携させてSSLを利用できるようにします。",[53,3227,3228],{"id":3228},"独自ドメインの証明書を取得する",[13,3230,3231,3232,3237],{},"まずcloudfrontの設定の前に独自ドメイン（jun-app.com）のSSL証明書をAWSで取得しておきます。AWSでは",[17,3233,3236],{"href":3234,"rel":3235},"https:\u002F\u002Fap-northeast-1.console.aws.amazon.com\u002Facm\u002Fhome",[21],"AWS Certificate Manager"," で取得できます。ただしここで注意点があります。",[864,3239,3241],{"className":3240},[867,2837],"\n以下の画像のようにAWS Certificate Managerでのカスタム証明書(独自ドメインの証明書)をcloudfrontに割り当てる場合、AWS Certificate Managerのリージョンが「米国東部 (バージニア北部) リージョン (us-east-1) 」でないといけません。私は間違って東京リージョンで作成してしまい、証明書がサジェストで出てこずはまりました。\n",[108,3243],{":src":3244,":width":111},"'s3-clioudfront-website\u002Fsch-cfm-1.png'",[13,3246,3247],{},"リージョンを「us-east-1」に変更したのち「証明書をリクエスト」をクリックして作成します。",[108,3249],{":src":3250,":width":111},"'s3-clioudfront-website\u002Fsch-cfm-2.png'",[13,3252,3253],{},"証明書のタイプは「パブリック証明書をリクエスト」を選択し、ドメイン名などを入力します。そして検証では「DNS検証」を行います。Route53で委任してあれば「DNS検証」ですぐに発行できます。DNS検証はドメインを管理しているDNSにAWS Certificate Managerで発行されたCNAMEを設定することで、ドメインの所有権を確認します。全て入力して「リクエスト」となります。",[108,3255],{":src":3256,":width":111},"'s3-clioudfront-website\u002Fsch-cfm-3.png'",[13,3258,3259],{},"リクエスト後には一覧に入力したドメインが現れ、対応するCNAMEが現れます。Route53で管理している場合は「Route53でレコードを作成」のボタンを押すだけで作ってくれます。手動で設定する場合は「CNAMEレコード」を作成して、その値を設定するだけです。",[108,3261],{":src":3262,":width":111},"'s3-clioudfront-website\u002Fsch-cfm-4.png'",[13,3264,3265],{},"このようにDNS検証に必要なCNAMEが追加されています。",[108,3267],{":src":3268,":width":111},"'s3-clioudfront-website\u002Fsch-cfm-5.png'",[13,3270,3271],{},"数分経つと証明書の検証が完了となりますので、これで「jun-app.com」の証明書の準備ができました。次はcloudfrontとS3を独自ドメインを用いて連携します。",[53,3273,3275],{"id":3274},"cloudfrontとs3を連携","cloudfrontとS3を連携",[13,3277,3278,3279,3284],{},"それではcloudfrontとS3を独自ドメインを連携します。",[17,3280,3283],{"href":3281,"rel":3282},"https:\u002F\u002Fconsole.aws.amazon.com\u002Fcloudfront\u002Fv3\u002Fhome?#\u002F",[21],"cloudfront","のコンソールに移動して「ディスりビーションの作成」をクリックします。",[13,3286,3287,3288,3291],{},"そしてcloudfrontと連携するAWSリソースやオリジンなどを設定します。「オリジンドメイン」でS3リソースを選択するのですがS3をwebサーバーとして利用する場合 ",[289,3289,3290],{},"サジェストに出てくる 「~~~~.s3.ap-northeast-1.amazonaws.com」 を選択してはいけません。"," これは単純に「S3オブジェクトURL」でありwebサーバーのような動きをしません。",[108,3293],{":src":3294,":width":111},"'s3-clioudfront-website\u002Fsch-clf-1.png'",[13,3296,3297,3298],{},"今回の構成ではS3の「静的ウェブサイトホスティング」を有効にした際に出現した（下図のボケているとこ）のバケットウェブサイトエンドポイントをいれます。このバケットウェブサイトエンドポイントは「S3をwebサーバーとして使用するときのドメイン」です。ここを忘れて「S3オブジェクトURL」に設定しても一応ルートはs3に接続はできるのですが、「",[17,3299,3302],{"href":3300,"rel":3301},"https:\u002F\u002Fjun-app.com\u002Ftest\u002F%E3%80%8D%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B%E3%81%A8%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%97%E3%81%9F%E3%82%8A%E3%80%81%E6%80%9D%E3%81%86%E3%82%88%E3%81%86%E3%81%AB%E5%8B%95%E3%81%8D%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82%E7%B5%90%E6%A7%8B%E3%83%8F%E3%83%9E%E3%82%8A%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%A7%E3%81%99%E3%80%82",[21],"https:\u002F\u002Fjun-app.com\u002Ftest\u002F」のように接続するとエラーが発生したり、思うように動きません。結構ハマりポイントです。",[108,3304],{":src":3305,":width":111},"'s3-clioudfront-website\u002Fsch-clf-2.png'",[13,3307,3308],{},"オリジンの設定をしたら他の項目は基本的にデフォルトのままでOKです。下に進んで独自ドメインと証明書を設定する箇所があります。「代替ドメイン名 (CNAME)」に連携したいドメイン名を入力し、そのドメインに対応する証明書（先ほど取得した証明書）を選択します。",[108,3310],{":src":3311,":width":111},"'s3-clioudfront-website\u002Fsch-clf-3.png'",[13,3313,3314,3315],{},"最後にデフォルトルートオブジェクトに「index.html」を設定します。こうすると「",[17,3316,3319],{"href":3317,"rel":3318},"https:\u002F\u002Fjun-app.com\u002Ftest\u002F%E3%80%8D%E3%81%A8%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E9%9A%9B%E3%80%8Chttps:\u002F\u002Fjun-app.com\u002Ftest\u002Findex.html%E3%80%8D%E3%81%8C%E8%87%AA%E5%8B%95%E7%9A%84%E3%81%AB%E8%BF%94%E3%81%95%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82",[21],"https:\u002F\u002Fjun-app.com\u002Ftest\u002F」とリクエストした際「https:\u002F\u002Fjun-app.com\u002Ftest\u002Findex.html」が自動的に返されます。",[108,3321],{":src":3322,":width":111},"'s3-clioudfront-website\u002Fsch-clf-4.png'",[13,3324,3325,3326,3329,3330,3332],{},"これで一通りcloudfrontとS3の連携が完了しました。「ディストリビーションを作成」を行いますと一覧にて ",[294,3327,3328],{},"~~~~~~.cloudfront.net"," というS3オリジンに対するcloudfrontのドメインが作成されます。このドメインにアクセスしてみますと、SSL付きでS3上にアップロードした ",[294,3331,2891],{}," が表示されます。",[13,3334,3335,3336,3339],{},"つぎは cloudfrontのドメインが ",[294,3337,3338],{},"jun-app.com"," に向くようにRoute53を設定します。",[53,3341,3342],{"id":3342},"cloudfrontのドメインを独自ドメインに向ける",[13,3344,3345],{},"ではRoute53に戻ってcloudfrontのドメインが独自ドメインに向くようにします。ドメインのホストゾーンにて「レコードの作成」を選択します。そして以下のような画面が開きます。「レコード名」はサブドメインを作るわけでないので、空欄で大丈夫です。そしてレコードタイプは「A - ipvアドレスと..」を選択します。「トラフィックのルーティング先」のエイリアスをONにします。（トグルになっています）",[13,3347,3348],{},"Aレコードは普通、IPアドレスを入力しますがcloudfrontなどのAWSリソースでしているする場合「エイリアス」を使用します。エイリアスでは「CloudFront ディストリビューションへのエイリアス」を選択します。そしてその下にcloudfrontのディストリビューションの入力欄が現れますので、そこに先ほどのcloudfrontのドメインを入力します。",[108,3350],{":src":3351,":width":111},"'s3-clioudfront-website\u002Fsch-clf-r53-1.png'",[864,3353,3355],{"className":3354},[867,868],"\n本来であれば登録したcloudfrontのディストリビューションがサジェストに表示されますが、登録したてだと出てこないことがあります。ただディストリビューションがあればとりあえず登録できます。\n",[13,3357,3358,3359,3361],{},"そして最後に「レコードの作成」をクリックします。すると ",[294,3360,3338],{}," に対するAレコードがcloudfrontのディストリビューションに向くようになります。ここまでドメインの流れを追うと以下の通りになります。",[116,3363,3364,3369,3382],{},[119,3365,3366,3368],{},[294,3367,3338],{}," にリクエストしたとき、まずお名前.comに問い合わせるが「Route53」に委任されているのでRoute53へ問い合わせ",[119,3370,3371,3372,3374,3375,3378,3379,3381],{},"Route53では ",[294,3373,3338],{}," のAレコードが ",[294,3376,3377],{},"xxxx.cloudfront.net"," に向いているので、",[294,3380,3377],{},"へ接続。",[119,3383,3384,3386],{},[294,3385,3377],{}," は S3のwebホスティングURLに向いているので、そこに接続",[13,3388,3389],{},"となります。（結構ざっくりとしてます）\n以上でバックエンド側の準備ができました。すでにドメインを使用している場合はDNSキャッシュが効いていることがあります。別の端末からアクセスしたり、DNSキャッシュをクリアするコマンドを打ってみましょう。",[864,3391,3394,3395,3398,3399,3402],{"className":3392},[867,3393],"alert-success","\nDNSの設定がうまく、対象のサーバーに向いているかを調べるときは",[294,3396,3397],{},"nslookup","や",[294,3400,3401],{},"dig","コマンドを使って対象ドメインに紐づけられているサーバーを確かめてみましょう。\n",[13,3404,3405,3406,3408,3409,3411],{},"現在のところS3に",[294,3407,2891],{},"が置いてあれば、独自ドメイン＋SSLでその",[294,3410,2891],{},"が表示されていればOKです。",[36,3413,3415],{"id":3414},"aws-cliを用いたs3へのファイルのデプロイ","AWS CLIを用いたS3へのファイルのデプロイ",[13,3417,3418,3419,3422],{},"では最後にブログファイル群をS3にアップロードします。私の場合はNuxt.jsであらかじめ書き出しを行い、",[294,3420,3421],{},"dist","ファイル配下を送信します。手動は毎回大変ですのでAWS CLIを用いて自動化と差分アップロード できるようにします。その手順を解説します。最初にAWS CLIをもちいて対象バケットにアクセスできるIAMユーザーを作成します。",[53,3424,3426],{"id":3425},"iamでデプロイユーザーを作成する","IAMでデプロイユーザーを作成する",[13,3428,3429],{},"AWSの運用ベストプラクティスでは",[3431,3432,3433],"blockquote",{},[13,3434,3435],{},"AWS アカウントのルートユーザー のアクセスキーを使用しないでください。\nIAM により、複数のユーザーに AWS リソースへの安全なアクセスを簡単に提供できます。IAM により以下が可能となります。",[13,3437,3438],{},"とある様に基本的にIAMというものを用いてリソースにアクセスする様にします。例えば今回の様にAWS CLIを用いて自分のAWSリソースにアクセスする時にシークレットキーとアクセスキーを使用します。AWSを始めた時に作ったRootアカウントでも可能ですが、rootはCLIを通じで本当になんでもできてしまうので使うべきではありません。であればアクションとアクセスを制限されたユーザー（IAM）を使用すれば、もしキーが漏れたとしても被害は小さめです。なので今回のブログデプロイ用のIAMを作成してしまいます。",[13,3440,3441],{},"AWSにログインしてIAMに移動し、「ユーザーを追加」をクリックします。",[108,3443],{":src":3444,":width":111},"'s3-clioudfront-website\u002Fsch-iam-0.png'",[13,3446,3447],{},"ユーザー名を設定し、「プログラムによるアクセス」にチェックをし、次のアクセス権限の設定を行います。",[108,3449],{":src":3450,":width":111},"'s3-clioudfront-website\u002Fsch-iam-1.png'",[13,3452,3453],{},"ここでこのユーザーがアクセスできるアクションを設定できます。一応JSONを用いて細かい設定もできますが、今回はチュートリアル的な内容なので「AmazonS3FullAccess」を与えておきます。",[108,3455],{":src":3456,":width":111},"'s3-clioudfront-website\u002Fsch-iam-2.png'",[13,3458,3459],{},"次にタグを設定しますが、組織でなければ特に設定しません。最後に全体を確認して「ユーザーを作成」をします。これでS3だけにアクセスできるIAMができます。",[108,3461],{":src":3462,":width":111},"'s3-clioudfront-website\u002Fsch-iam-3.png'",[13,3464,3465],{},"このユーザー用のアクセスキーとシークレットキーがダウンロードできますので、控えておきます。まずデプロイ用のIAMユーザーができました。AWS CLIのインストールはここでは割愛しまが、ここで取得したIAMのキーをプロファイルに設定します。",[402,3467,3471],{"className":3468,"code":3469,"language":3470,"meta":408,"style":408},"language-bash shiki shiki-themes material-theme-ocean","aws configure --profile junapp-s3\n","bash",[294,3472,3473],{"__ignoreMap":408},[454,3474,3475,3478,3481,3484],{"class":456,"line":457},[454,3476,3477],{"class":2988},"aws",[454,3479,3480],{"class":2955}," configure",[454,3482,3483],{"class":2955}," --profile",[454,3485,3486],{"class":2955}," junapp-s3\n",[13,3488,3489],{},"プロファイル名は分かりやすい名前にしておくといいです。これでIAMと転送のセットアップは完了です。",[53,3491,3493],{"id":3492},"ファイルを生成してs3へアップロード","ファイルを生成してS3へアップロード",[13,3495,3496,3497,3500,3501,3503,3504,3506,3507,3510],{},"私の環境では ",[294,3498,3499],{},"npm run generate"," で ",[294,3502,3421],{}," ファイルが生成され、必要なHTMLファイルとアセットが出力されます。その ",[294,3505,3421],{},"ファイル配下を全て",[294,3508,3509],{},"s3:\u002F\u002Fjun-app.com\u002F","へ転送します。そのときはAWS CLIで以下のコマンドを使用します。",[402,3512,3514],{"className":3468,"code":3513,"language":3470,"meta":408,"style":408},"aws s3 sync .\u002Fdist\u002F s3:\u002F\u002Fjun-app.com\u002F --delete --profile junapp-s3\n",[294,3515,3516],{"__ignoreMap":408},[454,3517,3518,3520,3523,3526,3529,3532,3535,3537],{"class":456,"line":457},[454,3519,3477],{"class":2988},[454,3521,3522],{"class":2955}," s3",[454,3524,3525],{"class":2955}," sync",[454,3527,3528],{"class":2955}," .\u002Fdist\u002F",[454,3530,3531],{"class":2955}," s3:\u002F\u002Fjun-app.com\u002F",[454,3533,3534],{"class":2955}," --delete",[454,3536,3483],{"class":2955},[454,3538,3486],{"class":2955},[13,3540,3541,3544,3545,3548,3549,3552,3553,3556,3557,3560],{},[294,3542,3543],{},"s3 sync","は2回目以降、差分をみてあれば同期してくれます。二回目以降は転送量を節約できます。",[294,3546,3547],{},"--delete"," は同期先（S3）にて同期元（ローカル）にないファイルを消してくれます。例えばnuxt.jsではキャッシュ対策のため、",[294,3550,3551],{},"_nuxt","ディレクトリ配下に",[294,3554,3555],{},"34234324.js","みたいなハッシュ化したjsファイルが書き出しごとに生成されます。単純に転送していくと",[294,3558,3559],{},"__nuxt","配下にjsファイルが溜まっていくので差分を見て古いファイルを削除します。他にも「メニューなどにはないけれど、削除した記事がファイルとしては残ったままになっている」みたいな事故も防げます。",[13,3562,3563,3566],{},[294,3564,3565],{},"--profile"," にて先ほど設定したIAMのプロファイルを指定します。",[864,3568,3570,3571,3574],{"className":3569},[867,868],"\nこのコマンドを実行する前に ",[294,3572,3573],{},"--dryrun","のオプションをいれてシミュレートしてから本番を実行しましょう。\n",[13,3576,3577],{},"準備ができたらコマンドを打ちます。終わったらバケットを確認して終了です。",[60,3579,3580],{"id":3580},"スクリプトに入れておくと便利",[13,3582,3583,3584,3586,3587,3590,3591,3594],{},"Nuxt.jsで",[294,3585,3499],{},"した際に",[294,3588,3589],{},"sync","されるように",[294,3592,3593],{},"package.json","を以下のように編集しておきました。",[402,3596,3601],{"className":3597,"code":3598,"filename":3599,"language":3600,"meta":408,"style":408},"language-json shiki shiki-themes material-theme-ocean","{\n\"scripts\": {\n    ...\n    \"generate-production\": \"nuxt generate && aws s3 sync .\u002Fdist\u002F s3:\u002F\u002Fjun-app.com\u002F --delete --profile junapp-s3\"\n}\n}\n","pakcage.json","json",[294,3602,3603,3607,3621,3627,3645,3649],{"__ignoreMap":408},[454,3604,3605],{"class":456,"line":457},[454,3606,466],{"class":2934},[454,3608,3609,3611,3614,3616,3618],{"class":456,"line":463},[454,3610,2946],{"class":2934},[454,3612,3613],{"class":2942},"scripts",[454,3615,2946],{"class":2934},[454,3617,2949],{"class":2934},[454,3619,3620],{"class":2934}," {\n",[454,3622,3623],{"class":456,"line":469},[454,3624,3626],{"class":3625},"s0W1g","    ...\n",[454,3628,3629,3631,3634,3636,3638,3640,3643],{"class":456,"line":475},[454,3630,2939],{"class":2934},[454,3632,3633],{"class":2988},"generate-production",[454,3635,2946],{"class":2934},[454,3637,2949],{"class":2934},[454,3639,2952],{"class":2934},[454,3641,3642],{"class":2955},"nuxt generate && aws s3 sync .\u002Fdist\u002F s3:\u002F\u002Fjun-app.com\u002F --delete --profile junapp-s3",[454,3644,3066],{"class":2934},[454,3646,3647],{"class":456,"line":481},[454,3648,562],{"class":2934},[454,3650,3651],{"class":456,"line":487},[454,3652,562],{"class":2934},[60,3654,3655],{"id":3655},"cloudfrontのキャッシュに注意",[13,3657,3658],{},"cloudfrontはCDNであり、キャッシュが強いです。更新したのに本番が変わらないときはキャッシュのせいかもしれません。cloudfrontでは明示的にキャッシュをクリアすることもできますし、キャッシュの期間を変更することもできます。閲覧数などに合わせて設定しましょう。",[36,3660,3662],{"id":3661},"以上","以上！",[13,3664,3665],{},"以上で S3 x SSL x 独自ドメインな静的ブログを構築できました。利用量によっては無料枠でも収まりそうです。しばらく料金の方も見てみてレンサバよりお得かを確認してみます。",[2743,3667,3668],{},"html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}",{"title":408,"searchDepth":469,"depth":469,"links":3670},[3671,3672,3673,3679,3686],{"id":2841,"depth":463,"text":2841},{"id":2859,"depth":463,"text":2860},{"id":3155,"depth":463,"text":3156,"children":3674},[3675,3676,3677,3678],{"id":3182,"depth":469,"text":3183},{"id":3228,"depth":469,"text":3228},{"id":3274,"depth":469,"text":3275},{"id":3342,"depth":469,"text":3342},{"id":3414,"depth":463,"text":3415,"children":3680},[3681,3682],{"id":3425,"depth":469,"text":3426},{"id":3492,"depth":469,"text":3493,"children":3683},[3684,3685],{"id":3580,"depth":475,"text":3580},{"id":3655,"depth":475,"text":3655},{"id":3661,"depth":463,"text":3662},[2793],"2025-07-18","AWS S3とcloudfrontを合わせることでwebサーバの様に扱う方法",{},"\u002Farticles\u002Fs3-clioudfront-website",{"title":2808,"description":3689},"articles\u002Fs3-clioudfront-website",[3695,3477],"infrastructure","s3-clioudfront-website\u002Fthumbnail.png","TS4Cse0E-KmfWg0S6g6WJdYqcKJXhparmDnjSsmNt20",{"id":3699,"title":3700,"body":3701,"category":5091,"createdAt":5092,"description":5093,"extension":2795,"index":2796,"meta":5094,"navigation":690,"path":5095,"publish":690,"seo":5096,"series":2796,"seriesTitle":2796,"stem":5097,"tag":5098,"thumbnail":5100,"updatedAt":2796,"__hash__":5101},"articles\u002Farticles\u002F2024_09_22_line_great_circle_antimeridian_cutting.md","turf.jsを用いたメルカトル図法地図上の線の大圏航路補正・逆子午線分割",{"type":10,"value":3702,"toc":5084},[3703,3706,3709,3713,3716,3720,3723,3726,3729,3732,3735,3738,3741,3744,3747,3750,3753,3756,3760,3763,3767,3770,4484,4487,5081],[13,3704,3705],{},"私がメルカトル図法を使用した地図上で、mapboxに2点以上の線を引く処理を実装していたとき、線が直線的に描画されるという問題が発生しました。さらに、経度180度の逆子午線を跨ぐ場合、mapboxでは大回りの線が描かれてしまい、その修正も必要でした。例えば、東京→ロサンゼルスの２点を結ぶと、東経180度を越えるのでなく、西回りでぐるっと線が引かれてしまいました。",[13,3707,3708],{},"本記事では、JavaScriptライブラリであるturf.jsを用いて、補正と分割を行った表示用のパスを算出手法について紹介します。",[36,3710,3712],{"id":3711},"大圏航路とはなぜ補正が必要","大圏航路とは？なぜ補正が必要？",[13,3714,3715],{},"大圏航路（Great Circle Route）は、地球上の2点を最短距離で結ぶ経路です。なんとなく２点を結んだ直線が最短経路と思ってしましますが、実際はそうではありません。よく見る平面の地図はメルカトル図法という方法で表現されており、引いた線が実際の地球上の経路、距離、面積と一致しません。これは地球が球面であり、球面上の線を平面上の線の描画と一致しないからです。例えば、東京→デリーの最短ルートは実際以下の通りで、直線的に結んだものではありません。（左が補正なしの直線）",[108,3717],{":src":3718,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig2.png'","'200px'",[108,3721],{":src":3722,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig1.png'",[13,3724,3725],{},"メルカトル図法は、緯度が高くなるほど距離の比率が歪むため、大圏航路を描くと地図上で曲線として表示されるのが特徴です。しかし、通常のGeoJSONなどを使用して単純に線を引くと、2点間を直線で結んでしまい、大圏航路としての正確さが失れます。",[13,3727,3728],{},"これは2点の距離が離れるほど、平面上に引いた直線と実際の線と乖離します。短い距離、少なくとも日本列島ぐらいであれば問題ありませんが、大陸レベルだったり海路・空路を表現するときはその乖離が顕著になります。",[13,3730,3731],{},"海路・空路を記載するような地図はこの補正を考慮しないといけません。わかりやすくすると以下の通りです。",[108,3733],{":src":3734,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig5.png'",[108,3736],{":src":3737,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig6.png'",[36,3739,3740],{"id":3740},"逆子午線を越えるときには分割が必要",[13,3742,3743],{},"逆子午線とは、経度180度の線を指します。mapboxで経度180度を跨ぐ線を描こうとすると、通常の経路補正では大回りで描かれてしまい、直感的に不自然になります。",[108,3745],{":src":3746,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig3.png'",[108,3748],{":src":3749,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig4.png'",[13,3751,3752],{},"そのため、逆子午線を越える線を正しく描画するには、線を分割して複数のLineStringやMultiLineStringに分ける処理を行いました。図としてこのような感じです。",[108,3754],{":src":3755,":width":3719},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig7.png'",[36,3757,3759],{"id":3758},"基本的にturfjsを使えば解決できる","基本的にturf.jsを使えば解決できる",[13,3761,3762],{},"JavaScriptのturf.jsライブラリを用いることで、これらの問題を解決できます。turf.jsはGeoJSONデータの操作に強力な機能を提供しており、大圏航路の計算を行うとき、180度を越えるとき自動的に分割したパスを渡してくれます。",[53,3764,3766],{"id":3765},"_2点間に補正点を算出","2点間に補正点を算出",[13,3768,3769],{},"まず、2点間の線を大圏航路で補正するために、turf.jsのturf.greatCircle関数を使用しました。この関数を使うことで2点間を大圏航路で補正し、曲線的な追加のパスを生成できます。\n180度を超えない2点の場合、補正した緯度経度の配列が戻ります。180度を越える場合、180度で分割した2つのlineの緯度経度配列が戻ります。",[402,3771,3775],{"className":3772,"code":3773,"language":3774,"meta":408,"style":408},"language-ts shiki shiki-themes material-theme-ocean","import * as turf from '@turf\u002Fturf';\nimport { Feature } from \"geojson\";\n\nconst path = [lat:number,lng:number][]\nconst displayPath: [number, number][][] = [];\n\n\u002F\u002F 各ラインのパスごとに大円航路を計算したパスを追加\nfor (let i = 0; i \u003C path.length - 1; i++) {\n    const startPoint = path[i];\n    const endPoint = path[i + 1];\n\n    const currentPoint = turf.point([startPoint[1], startPoint[0]]); \u002F\u002F turf.js は経度・緯度と入力したり戻ってくるので注意。\n    const nextPoint = turf.point([endPoint[1], endPoint[0]]);\n\n    const greatCircle = turf.greatCircle(currentPoint, nextPoint, { npoints: 30 }); \u002F\u002F npoints が追加する補正点。多いほど滑らか\n\n    const coordinates = greatCircle.geometry.coordinates;\n\n    \u002F\u002F LineString の場合\n    if (Array.isArray(coordinates[0]) && !Array.isArray(coordinates[0][0])) {\n        displayPath.push(coordinates as [number, number][]);\n    } \n    \u002F\u002F MultiLineString の場合\n    else if (Array.isArray(coordinates[0][0])) {\n        coordinates.forEach((segment) => {\n            displayPath.push(segment as [number, number][]);\n        });\n    }\n}\n\nconst geojson = {\n    type: \"Feature\",\n    properties: {},\n    geometry: {\n        type: \"MultiLineString\",\n        coordinates: displayPath,\n    },\n} as Feature;\n\nreturn geojson;\n","ts",[294,3776,3777,3807,3832,3836,3856,3886,3890,3896,3952,3977,4001,4005,4053,4091,4095,4143,4147,4170,4174,4179,4235,4264,4272,4277,4309,4334,4361,4370,4374,4378,4382,4393,4409,4419,4428,4444,4454,4459,4470,4474],{"__ignoreMap":408},[454,3778,3779,3783,3786,3789,3792,3795,3798,3801,3804],{"class":456,"line":457},[454,3780,3782],{"class":3781},"s6cf3","import",[454,3784,3785],{"class":2934}," *",[454,3787,3788],{"class":3781}," as",[454,3790,3791],{"class":3625}," turf ",[454,3793,3794],{"class":3781},"from",[454,3796,3797],{"class":2934}," '",[454,3799,3800],{"class":2955},"@turf\u002Fturf",[454,3802,3803],{"class":2934},"'",[454,3805,3806],{"class":2934},";\n",[454,3808,3809,3811,3814,3817,3820,3823,3825,3828,3830],{"class":456,"line":463},[454,3810,3782],{"class":3781},[454,3812,3813],{"class":2934}," {",[454,3815,3816],{"class":3625}," Feature",[454,3818,3819],{"class":2934}," }",[454,3821,3822],{"class":3781}," from",[454,3824,2952],{"class":2934},[454,3826,3827],{"class":2955},"geojson",[454,3829,2946],{"class":2934},[454,3831,3806],{"class":2934},[454,3833,3834],{"class":456,"line":469},[454,3835,691],{"emptyLinePlaceholder":690},[454,3837,3838,3841,3844,3847,3850,3853],{"class":456,"line":475},[454,3839,3840],{"class":2942},"const",[454,3842,3843],{"class":3625}," path ",[454,3845,3846],{"class":2934},"=",[454,3848,3849],{"class":3625}," [lat:number",[454,3851,3852],{"class":2934},",",[454,3854,3855],{"class":3625},"lng:number][]\n",[454,3857,3858,3860,3863,3865,3868,3871,3873,3876,3879,3881,3884],{"class":456,"line":481},[454,3859,3840],{"class":2942},[454,3861,3862],{"class":3625}," displayPath",[454,3864,2949],{"class":2934},[454,3866,3867],{"class":3625}," [",[454,3869,3870],{"class":2988},"number",[454,3872,3852],{"class":2934},[454,3874,3875],{"class":2988}," number",[454,3877,3878],{"class":3625},"][][] ",[454,3880,3846],{"class":2934},[454,3882,3883],{"class":3625}," []",[454,3885,3806],{"class":2934},[454,3887,3888],{"class":456,"line":487},[454,3889,691],{"emptyLinePlaceholder":690},[454,3891,3892],{"class":456,"line":493},[454,3893,3895],{"class":3894},"sC9rS","\u002F\u002F 各ラインのパスごとに大円航路を計算したパスを追加\n",[454,3897,3898,3901,3904,3907,3910,3912,3916,3919,3921,3924,3927,3930,3933,3936,3939,3941,3944,3947,3950],{"class":456,"line":499},[454,3899,3900],{"class":3781},"for",[454,3902,3903],{"class":3625}," (",[454,3905,3906],{"class":2942},"let",[454,3908,3909],{"class":3625}," i ",[454,3911,3846],{"class":2934},[454,3913,3915],{"class":3914},"sx098"," 0",[454,3917,3918],{"class":2934},";",[454,3920,3909],{"class":3625},[454,3922,3923],{"class":2934},"\u003C",[454,3925,3926],{"class":3625}," path",[454,3928,3929],{"class":2934},".",[454,3931,3932],{"class":3625},"length ",[454,3934,3935],{"class":2934},"-",[454,3937,3938],{"class":3914}," 1",[454,3940,3918],{"class":2934},[454,3942,3943],{"class":3625}," i",[454,3945,3946],{"class":2934},"++",[454,3948,3949],{"class":3625},") ",[454,3951,466],{"class":2934},[454,3953,3954,3957,3960,3963,3965,3969,3972,3975],{"class":456,"line":505},[454,3955,3956],{"class":2942},"    const",[454,3958,3959],{"class":3625}," startPoint",[454,3961,3962],{"class":2934}," =",[454,3964,3926],{"class":3625},[454,3966,3968],{"class":3967},"s-wAU","[",[454,3970,3971],{"class":3625},"i",[454,3973,3974],{"class":3967},"]",[454,3976,3806],{"class":2934},[454,3978,3979,3981,3984,3986,3988,3990,3992,3995,3997,3999],{"class":456,"line":511},[454,3980,3956],{"class":2942},[454,3982,3983],{"class":3625}," endPoint",[454,3985,3962],{"class":2934},[454,3987,3926],{"class":3625},[454,3989,3968],{"class":3967},[454,3991,3971],{"class":3625},[454,3993,3994],{"class":2934}," +",[454,3996,3938],{"class":3914},[454,3998,3974],{"class":3967},[454,4000,3806],{"class":2934},[454,4002,4003],{"class":456,"line":517},[454,4004,691],{"emptyLinePlaceholder":690},[454,4006,4007,4009,4012,4014,4017,4019,4023,4026,4029,4031,4034,4036,4038,4040,4042,4045,4048,4050],{"class":456,"line":523},[454,4008,3956],{"class":2942},[454,4010,4011],{"class":3625}," currentPoint",[454,4013,3962],{"class":2934},[454,4015,4016],{"class":3625}," turf",[454,4018,3929],{"class":2934},[454,4020,4022],{"class":4021},"sdLwU","point",[454,4024,4025],{"class":3967},"([",[454,4027,4028],{"class":3625},"startPoint",[454,4030,3968],{"class":3967},[454,4032,4033],{"class":3914},"1",[454,4035,3974],{"class":3967},[454,4037,3852],{"class":2934},[454,4039,3959],{"class":3625},[454,4041,3968],{"class":3967},[454,4043,4044],{"class":3914},"0",[454,4046,4047],{"class":3967},"]])",[454,4049,3918],{"class":2934},[454,4051,4052],{"class":3894}," \u002F\u002F turf.js は経度・緯度と入力したり戻ってくるので注意。\n",[454,4054,4055,4057,4060,4062,4064,4066,4068,4070,4073,4075,4077,4079,4081,4083,4085,4087,4089],{"class":456,"line":529},[454,4056,3956],{"class":2942},[454,4058,4059],{"class":3625}," nextPoint",[454,4061,3962],{"class":2934},[454,4063,4016],{"class":3625},[454,4065,3929],{"class":2934},[454,4067,4022],{"class":4021},[454,4069,4025],{"class":3967},[454,4071,4072],{"class":3625},"endPoint",[454,4074,3968],{"class":3967},[454,4076,4033],{"class":3914},[454,4078,3974],{"class":3967},[454,4080,3852],{"class":2934},[454,4082,3983],{"class":3625},[454,4084,3968],{"class":3967},[454,4086,4044],{"class":3914},[454,4088,4047],{"class":3967},[454,4090,3806],{"class":2934},[454,4092,4093],{"class":456,"line":535},[454,4094,691],{"emptyLinePlaceholder":690},[454,4096,4097,4099,4102,4104,4106,4108,4111,4114,4117,4119,4121,4123,4125,4128,4130,4133,4135,4138,4140],{"class":456,"line":541},[454,4098,3956],{"class":2942},[454,4100,4101],{"class":3625}," greatCircle",[454,4103,3962],{"class":2934},[454,4105,4016],{"class":3625},[454,4107,3929],{"class":2934},[454,4109,4110],{"class":4021},"greatCircle",[454,4112,4113],{"class":3967},"(",[454,4115,4116],{"class":3625},"currentPoint",[454,4118,3852],{"class":2934},[454,4120,4059],{"class":3625},[454,4122,3852],{"class":2934},[454,4124,3813],{"class":2934},[454,4126,4127],{"class":3967}," npoints",[454,4129,2949],{"class":2934},[454,4131,4132],{"class":3914}," 30",[454,4134,3819],{"class":2934},[454,4136,4137],{"class":3967},")",[454,4139,3918],{"class":2934},[454,4141,4142],{"class":3894}," \u002F\u002F npoints が追加する補正点。多いほど滑らか\n",[454,4144,4145],{"class":456,"line":547},[454,4146,691],{"emptyLinePlaceholder":690},[454,4148,4149,4151,4154,4156,4158,4160,4163,4165,4168],{"class":456,"line":553},[454,4150,3956],{"class":2942},[454,4152,4153],{"class":3625}," coordinates",[454,4155,3962],{"class":2934},[454,4157,4101],{"class":3625},[454,4159,3929],{"class":2934},[454,4161,4162],{"class":3625},"geometry",[454,4164,3929],{"class":2934},[454,4166,4167],{"class":3625},"coordinates",[454,4169,3806],{"class":2934},[454,4171,4172],{"class":456,"line":559},[454,4173,691],{"emptyLinePlaceholder":690},[454,4175,4176],{"class":456,"line":1028},[454,4177,4178],{"class":3894},"    \u002F\u002F LineString の場合\n",[454,4180,4181,4184,4186,4189,4191,4194,4196,4198,4200,4202,4205,4208,4211,4213,4215,4217,4219,4221,4223,4225,4228,4230,4233],{"class":456,"line":1034},[454,4182,4183],{"class":3781},"    if",[454,4185,3903],{"class":3967},[454,4187,4188],{"class":3625},"Array",[454,4190,3929],{"class":2934},[454,4192,4193],{"class":4021},"isArray",[454,4195,4113],{"class":3967},[454,4197,4167],{"class":3625},[454,4199,3968],{"class":3967},[454,4201,4044],{"class":3914},[454,4203,4204],{"class":3967},"]) ",[454,4206,4207],{"class":2934},"&&",[454,4209,4210],{"class":2934}," !",[454,4212,4188],{"class":3625},[454,4214,3929],{"class":2934},[454,4216,4193],{"class":4021},[454,4218,4113],{"class":3967},[454,4220,4167],{"class":3625},[454,4222,3968],{"class":3967},[454,4224,4044],{"class":3914},[454,4226,4227],{"class":3967},"][",[454,4229,4044],{"class":3914},[454,4231,4232],{"class":3967},"])) ",[454,4234,466],{"class":2934},[454,4236,4237,4240,4242,4245,4247,4249,4251,4253,4255,4257,4259,4262],{"class":456,"line":1039},[454,4238,4239],{"class":3625},"        displayPath",[454,4241,3929],{"class":2934},[454,4243,4244],{"class":4021},"push",[454,4246,4113],{"class":3967},[454,4248,4167],{"class":3625},[454,4250,3788],{"class":3781},[454,4252,3867],{"class":3967},[454,4254,3870],{"class":2988},[454,4256,3852],{"class":2934},[454,4258,3875],{"class":2988},[454,4260,4261],{"class":3967},"][])",[454,4263,3806],{"class":2934},[454,4265,4266,4269],{"class":456,"line":1045},[454,4267,4268],{"class":2934},"    }",[454,4270,4271],{"class":3967}," \n",[454,4273,4274],{"class":456,"line":1050},[454,4275,4276],{"class":3894},"    \u002F\u002F MultiLineString の場合\n",[454,4278,4279,4282,4285,4287,4289,4291,4293,4295,4297,4299,4301,4303,4305,4307],{"class":456,"line":1056},[454,4280,4281],{"class":3781},"    else",[454,4283,4284],{"class":3781}," if",[454,4286,3903],{"class":3967},[454,4288,4188],{"class":3625},[454,4290,3929],{"class":2934},[454,4292,4193],{"class":4021},[454,4294,4113],{"class":3967},[454,4296,4167],{"class":3625},[454,4298,3968],{"class":3967},[454,4300,4044],{"class":3914},[454,4302,4227],{"class":3967},[454,4304,4044],{"class":3914},[454,4306,4232],{"class":3967},[454,4308,466],{"class":2934},[454,4310,4311,4314,4316,4319,4321,4323,4327,4329,4332],{"class":456,"line":1062},[454,4312,4313],{"class":3625},"        coordinates",[454,4315,3929],{"class":2934},[454,4317,4318],{"class":4021},"forEach",[454,4320,4113],{"class":3967},[454,4322,4113],{"class":2934},[454,4324,4326],{"class":4325},"s7ZW3","segment",[454,4328,4137],{"class":2934},[454,4330,4331],{"class":2942}," =>",[454,4333,3620],{"class":2934},[454,4335,4336,4339,4341,4343,4345,4347,4349,4351,4353,4355,4357,4359],{"class":456,"line":1068},[454,4337,4338],{"class":3625},"            displayPath",[454,4340,3929],{"class":2934},[454,4342,4244],{"class":4021},[454,4344,4113],{"class":3967},[454,4346,4326],{"class":3625},[454,4348,3788],{"class":3781},[454,4350,3867],{"class":3967},[454,4352,3870],{"class":2988},[454,4354,3852],{"class":2934},[454,4356,3875],{"class":2988},[454,4358,4261],{"class":3967},[454,4360,3806],{"class":2934},[454,4362,4363,4366,4368],{"class":456,"line":1074},[454,4364,4365],{"class":2934},"        }",[454,4367,4137],{"class":3967},[454,4369,3806],{"class":2934},[454,4371,4372],{"class":456,"line":1079},[454,4373,1110],{"class":2934},[454,4375,4376],{"class":456,"line":1084},[454,4377,562],{"class":2934},[454,4379,4380],{"class":456,"line":1090},[454,4381,691],{"emptyLinePlaceholder":690},[454,4383,4384,4386,4389,4391],{"class":456,"line":1096},[454,4385,3840],{"class":2942},[454,4387,4388],{"class":3625}," geojson ",[454,4390,3846],{"class":2934},[454,4392,3620],{"class":2934},[454,4394,4395,4398,4400,4402,4405,4407],{"class":456,"line":1102},[454,4396,4397],{"class":3967},"    type",[454,4399,2949],{"class":2934},[454,4401,2952],{"class":2934},[454,4403,4404],{"class":2955},"Feature",[454,4406,2946],{"class":2934},[454,4408,2961],{"class":2934},[454,4410,4411,4414,4416],{"class":456,"line":1107},[454,4412,4413],{"class":3967},"    properties",[454,4415,2949],{"class":2934},[454,4417,4418],{"class":2934}," {},\n",[454,4420,4421,4424,4426],{"class":456,"line":1113},[454,4422,4423],{"class":3967},"    geometry",[454,4425,2949],{"class":2934},[454,4427,3620],{"class":2934},[454,4429,4430,4433,4435,4437,4440,4442],{"class":456,"line":1852},[454,4431,4432],{"class":3967},"        type",[454,4434,2949],{"class":2934},[454,4436,2952],{"class":2934},[454,4438,4439],{"class":2955},"MultiLineString",[454,4441,2946],{"class":2934},[454,4443,2961],{"class":2934},[454,4445,4446,4448,4450,4452],{"class":456,"line":1857},[454,4447,4313],{"class":3967},[454,4449,2949],{"class":2934},[454,4451,3862],{"class":3625},[454,4453,2961],{"class":2934},[454,4455,4456],{"class":456,"line":1862},[454,4457,4458],{"class":2934},"    },\n",[454,4460,4461,4464,4466,4468],{"class":456,"line":1868},[454,4462,4463],{"class":2934},"}",[454,4465,3788],{"class":3781},[454,4467,3816],{"class":2988},[454,4469,3806],{"class":2934},[454,4471,4472],{"class":456,"line":1873},[454,4473,691],{"emptyLinePlaceholder":690},[454,4475,4476,4479,4482],{"class":456,"line":1878},[454,4477,4478],{"class":3781},"return",[454,4480,4481],{"class":3625}," geojson",[454,4483,3806],{"class":2934},[13,4485,4486],{},"わかりやすいように、180度を越える・超えないパターンでの大圏航路の補正です。",[402,4488,4492],{"className":4489,"code":4490,"language":4491,"meta":408,"style":408},"language-js shiki shiki-themes material-theme-ocean","import * as turf from '@turf\u002Fturf';\n\nconst nonOver = turf.greatCircle(\n    [139.6503, 35.6762],  \u002F\u002F 東京の座標\n    [77.2090, 28.6139], \u002F\u002F デリーの座標\n    { npoints: 30 }\n);\n\nconsole.log(nonOver)\n\u002F**\n[\n  [ 139.6503, 35.67620000000001 ],\n  [ 137.45530635075792, 36.00345099838847 ],\n  [ 135.24315241142824, 36.29039534292487 ],\n  [ 133.01584313143206, 36.53630839532294 ],\n  [ 130.77553308674652, 36.74055732953035 ],\n  [ 128.52450866279412, 36.90260805727012 ],\n  [ 126.26516729530344, 37.022031252284606 ],\n  [ 123.99999414238111, 37.09850731283235 ],\n  [ 121.73153667001746, 37.131830125727525 ],\n  [ 119.46237772511748, 37.121909525547366 ],\n  [ 117.19510773852923, 37.0687723782786 ],\n  [ 114.93229674053903, 36.972562257930214 ],\n  [ 112.67646687998554, 36.83353772552399 ],\n  [ 110.43006611487587, 36.65206926027127 ],\n  [ 108.19544368887564, 36.42863493057574 ],\n  [ 105.97482792817907, 36.163814925900674 ],\n  [ 103.7703067926894, 35.858285097980165 ],\n  [ 101.58381150098748, 35.51280968026889 ],\n  [ 99.41710342759748, 35.12823336735064 ],\n  [ 97.27176435078754, 34.705472941209955 ],\n  [ 95.14919001606306, 34.245508629231686 ],\n  [ 93.05058687993017, 33.749375370334334 ],\n  [ 90.97697181428099, 33.21815415185492 ],\n  [ 88.92917448617246, 32.6529635619425 ],\n  [ 86.90784208162192, 32.05495168160128 ],\n  [ 84.91344601479491, 31.42528841842546 ],\n  [ 82.94629025402638, 30.7651583616406 ],\n  [ 81.00652090116452, 30.075754216293156 ],\n  [ 79.09413667798896, 29.358270854084918 ],\n  [ 77.209, 28.613900000000005 ]\n]\n*\u002F\n\nconst over = turf.greatCircle(\n    [139.6503, 35.6762],  \u002F\u002F 東京の座標\n    [-147.7164, 64.8378], \u002F\u002F アラスカの座標\n    { npoints: 30 }\n);\n\nconsole.log(over)\n\u002F*\n  [\n    [ 139.6503, 35.67620000000001 ],\n    [ 140.80178756980726, 37.16607636013253 ],\n    [ 141.99941083018345, 38.64436492965305 ],\n    [ 143.24727313996488, 40.10993078543407 ],\n    [ 144.54983478842868, 41.56151748104379 ],\n    [ 145.91194328157903, 42.9977313513525 ],\n    [ 147.33886324769875, 44.41702385324905 ],\n    [ 148.83630454782175, 45.81767177569259 ],\n    [ 150.41044655311683, 47.19775518537249 ],\n    [ 152.0679557258351, 48.55513303641521 ],\n    [ 153.8159925691613, 49.88741647693477 ],\n    [ 155.66220265313686, 51.19194004906208 ],\n    [ 157.6146847535422, 52.465731224424154 ],\n    [ 159.68192717003024, 53.705479070518045 ],\n    [ 161.87270110244128, 54.90750333482882 ],\n    [ 164.19589776721827, 56.06772589186521 ],\n    [ 166.66029413034695, 57.18164734362052 ],\n    [ 169.27423139783608, 58.24433259326387 ],\n    [ 172.0451917704491, 59.25041037671838 ],\n    [ 174.9792638386216, 60.19409291290832 ],\n    [ 178.08049701800238, 61.069222785803966 ],\n    [ 180, 61.53895140096846 ]\n  ],\n  [\n    [ -180, 61.53895140096846 ],\n    [ -178.64983787529644, 61.86935452669037 ],\n    [ -175.2140408098754, 62.587877615485425 ],\n    [ -171.618756489847, 63.21818519285117 ],\n    [ -167.87562800253605, 63.753888204781546 ],\n    [ -164.0017045661598, 64.18906791167207 ],\n    [ -160.0194735821535, 64.51855132598804 ],\n    [ -155.9564136218279, 64.73818576863974 ],\n    [ -151.84402691822368, 64.84508272020929 ],\n    [ -147.7164, 64.8378 ]\n  ]\n]\n*\u002F\n","js",[294,4493,4494,4514,4518,4536,4556,4575,4589,4595,4599,4612,4617,4622,4627,4632,4637,4642,4647,4652,4657,4662,4667,4672,4677,4682,4687,4692,4697,4702,4707,4712,4717,4722,4727,4732,4737,4742,4747,4752,4757,4762,4767,4772,4777,4781,4785,4802,4818,4839,4851,4857,4861,4872,4876,4881,4886,4891,4896,4901,4906,4911,4916,4921,4926,4931,4936,4941,4946,4951,4956,4961,4966,4971,4976,4982,4988,4994,5000,5005,5011,5017,5023,5029,5035,5041,5047,5053,5059,5065,5071,5076],{"__ignoreMap":408},[454,4495,4496,4498,4500,4502,4504,4506,4508,4510,4512],{"class":456,"line":457},[454,4497,3782],{"class":3781},[454,4499,3785],{"class":2934},[454,4501,3788],{"class":3781},[454,4503,3791],{"class":3625},[454,4505,3794],{"class":3781},[454,4507,3797],{"class":2934},[454,4509,3800],{"class":2955},[454,4511,3803],{"class":2934},[454,4513,3806],{"class":2934},[454,4515,4516],{"class":456,"line":463},[454,4517,691],{"emptyLinePlaceholder":690},[454,4519,4520,4522,4525,4527,4529,4531,4533],{"class":456,"line":469},[454,4521,3840],{"class":2942},[454,4523,4524],{"class":3625}," nonOver ",[454,4526,3846],{"class":2934},[454,4528,4016],{"class":3625},[454,4530,3929],{"class":2934},[454,4532,4110],{"class":4021},[454,4534,4535],{"class":3625},"(\n",[454,4537,4538,4541,4544,4546,4549,4551,4553],{"class":456,"line":475},[454,4539,4540],{"class":3625},"    [",[454,4542,4543],{"class":3914},"139.6503",[454,4545,3852],{"class":2934},[454,4547,4548],{"class":3914}," 35.6762",[454,4550,3974],{"class":3625},[454,4552,3852],{"class":2934},[454,4554,4555],{"class":3894},"  \u002F\u002F 東京の座標\n",[454,4557,4558,4560,4563,4565,4568,4570,4572],{"class":456,"line":481},[454,4559,4540],{"class":3625},[454,4561,4562],{"class":3914},"77.2090",[454,4564,3852],{"class":2934},[454,4566,4567],{"class":3914}," 28.6139",[454,4569,3974],{"class":3625},[454,4571,3852],{"class":2934},[454,4573,4574],{"class":3894}," \u002F\u002F デリーの座標\n",[454,4576,4577,4580,4582,4584,4586],{"class":456,"line":487},[454,4578,4579],{"class":2934},"    {",[454,4581,4127],{"class":3967},[454,4583,2949],{"class":2934},[454,4585,4132],{"class":3914},[454,4587,4588],{"class":2934}," }\n",[454,4590,4591,4593],{"class":456,"line":493},[454,4592,4137],{"class":3625},[454,4594,3806],{"class":2934},[454,4596,4597],{"class":456,"line":499},[454,4598,691],{"emptyLinePlaceholder":690},[454,4600,4601,4604,4606,4609],{"class":456,"line":505},[454,4602,4603],{"class":3625},"console",[454,4605,3929],{"class":2934},[454,4607,4608],{"class":4021},"log",[454,4610,4611],{"class":3625},"(nonOver)\n",[454,4613,4614],{"class":456,"line":511},[454,4615,4616],{"class":3894},"\u002F**\n",[454,4618,4619],{"class":456,"line":517},[454,4620,4621],{"class":3894},"[\n",[454,4623,4624],{"class":456,"line":523},[454,4625,4626],{"class":3894},"  [ 139.6503, 35.67620000000001 ],\n",[454,4628,4629],{"class":456,"line":529},[454,4630,4631],{"class":3894},"  [ 137.45530635075792, 36.00345099838847 ],\n",[454,4633,4634],{"class":456,"line":535},[454,4635,4636],{"class":3894},"  [ 135.24315241142824, 36.29039534292487 ],\n",[454,4638,4639],{"class":456,"line":541},[454,4640,4641],{"class":3894},"  [ 133.01584313143206, 36.53630839532294 ],\n",[454,4643,4644],{"class":456,"line":547},[454,4645,4646],{"class":3894},"  [ 130.77553308674652, 36.74055732953035 ],\n",[454,4648,4649],{"class":456,"line":553},[454,4650,4651],{"class":3894},"  [ 128.52450866279412, 36.90260805727012 ],\n",[454,4653,4654],{"class":456,"line":559},[454,4655,4656],{"class":3894},"  [ 126.26516729530344, 37.022031252284606 ],\n",[454,4658,4659],{"class":456,"line":1028},[454,4660,4661],{"class":3894},"  [ 123.99999414238111, 37.09850731283235 ],\n",[454,4663,4664],{"class":456,"line":1034},[454,4665,4666],{"class":3894},"  [ 121.73153667001746, 37.131830125727525 ],\n",[454,4668,4669],{"class":456,"line":1039},[454,4670,4671],{"class":3894},"  [ 119.46237772511748, 37.121909525547366 ],\n",[454,4673,4674],{"class":456,"line":1045},[454,4675,4676],{"class":3894},"  [ 117.19510773852923, 37.0687723782786 ],\n",[454,4678,4679],{"class":456,"line":1050},[454,4680,4681],{"class":3894},"  [ 114.93229674053903, 36.972562257930214 ],\n",[454,4683,4684],{"class":456,"line":1056},[454,4685,4686],{"class":3894},"  [ 112.67646687998554, 36.83353772552399 ],\n",[454,4688,4689],{"class":456,"line":1062},[454,4690,4691],{"class":3894},"  [ 110.43006611487587, 36.65206926027127 ],\n",[454,4693,4694],{"class":456,"line":1068},[454,4695,4696],{"class":3894},"  [ 108.19544368887564, 36.42863493057574 ],\n",[454,4698,4699],{"class":456,"line":1074},[454,4700,4701],{"class":3894},"  [ 105.97482792817907, 36.163814925900674 ],\n",[454,4703,4704],{"class":456,"line":1079},[454,4705,4706],{"class":3894},"  [ 103.7703067926894, 35.858285097980165 ],\n",[454,4708,4709],{"class":456,"line":1084},[454,4710,4711],{"class":3894},"  [ 101.58381150098748, 35.51280968026889 ],\n",[454,4713,4714],{"class":456,"line":1090},[454,4715,4716],{"class":3894},"  [ 99.41710342759748, 35.12823336735064 ],\n",[454,4718,4719],{"class":456,"line":1096},[454,4720,4721],{"class":3894},"  [ 97.27176435078754, 34.705472941209955 ],\n",[454,4723,4724],{"class":456,"line":1102},[454,4725,4726],{"class":3894},"  [ 95.14919001606306, 34.245508629231686 ],\n",[454,4728,4729],{"class":456,"line":1107},[454,4730,4731],{"class":3894},"  [ 93.05058687993017, 33.749375370334334 ],\n",[454,4733,4734],{"class":456,"line":1113},[454,4735,4736],{"class":3894},"  [ 90.97697181428099, 33.21815415185492 ],\n",[454,4738,4739],{"class":456,"line":1852},[454,4740,4741],{"class":3894},"  [ 88.92917448617246, 32.6529635619425 ],\n",[454,4743,4744],{"class":456,"line":1857},[454,4745,4746],{"class":3894},"  [ 86.90784208162192, 32.05495168160128 ],\n",[454,4748,4749],{"class":456,"line":1862},[454,4750,4751],{"class":3894},"  [ 84.91344601479491, 31.42528841842546 ],\n",[454,4753,4754],{"class":456,"line":1868},[454,4755,4756],{"class":3894},"  [ 82.94629025402638, 30.7651583616406 ],\n",[454,4758,4759],{"class":456,"line":1873},[454,4760,4761],{"class":3894},"  [ 81.00652090116452, 30.075754216293156 ],\n",[454,4763,4764],{"class":456,"line":1878},[454,4765,4766],{"class":3894},"  [ 79.09413667798896, 29.358270854084918 ],\n",[454,4768,4769],{"class":456,"line":1883},[454,4770,4771],{"class":3894},"  [ 77.209, 28.613900000000005 ]\n",[454,4773,4774],{"class":456,"line":1889},[454,4775,4776],{"class":3894},"]\n",[454,4778,4779],{"class":456,"line":1894},[454,4780,1340],{"class":3894},[454,4782,4783],{"class":456,"line":1900},[454,4784,691],{"emptyLinePlaceholder":690},[454,4786,4787,4789,4792,4794,4796,4798,4800],{"class":456,"line":1905},[454,4788,3840],{"class":2942},[454,4790,4791],{"class":3625}," over ",[454,4793,3846],{"class":2934},[454,4795,4016],{"class":3625},[454,4797,3929],{"class":2934},[454,4799,4110],{"class":4021},[454,4801,4535],{"class":3625},[454,4803,4804,4806,4808,4810,4812,4814,4816],{"class":456,"line":2162},[454,4805,4540],{"class":3625},[454,4807,4543],{"class":3914},[454,4809,3852],{"class":2934},[454,4811,4548],{"class":3914},[454,4813,3974],{"class":3625},[454,4815,3852],{"class":2934},[454,4817,4555],{"class":3894},[454,4819,4820,4822,4824,4827,4829,4832,4834,4836],{"class":456,"line":2168},[454,4821,4540],{"class":3625},[454,4823,3935],{"class":2934},[454,4825,4826],{"class":3914},"147.7164",[454,4828,3852],{"class":2934},[454,4830,4831],{"class":3914}," 64.8378",[454,4833,3974],{"class":3625},[454,4835,3852],{"class":2934},[454,4837,4838],{"class":3894}," \u002F\u002F アラスカの座標\n",[454,4840,4841,4843,4845,4847,4849],{"class":456,"line":2173},[454,4842,4579],{"class":2934},[454,4844,4127],{"class":3967},[454,4846,2949],{"class":2934},[454,4848,4132],{"class":3914},[454,4850,4588],{"class":2934},[454,4852,4853,4855],{"class":456,"line":2179},[454,4854,4137],{"class":3625},[454,4856,3806],{"class":2934},[454,4858,4859],{"class":456,"line":2185},[454,4860,691],{"emptyLinePlaceholder":690},[454,4862,4863,4865,4867,4869],{"class":456,"line":2191},[454,4864,4603],{"class":3625},[454,4866,3929],{"class":2934},[454,4868,4608],{"class":4021},[454,4870,4871],{"class":3625},"(over)\n",[454,4873,4874],{"class":456,"line":2197},[454,4875,1297],{"class":3894},[454,4877,4878],{"class":456,"line":2202},[454,4879,4880],{"class":3894},"  [\n",[454,4882,4883],{"class":456,"line":2208},[454,4884,4885],{"class":3894},"    [ 139.6503, 35.67620000000001 ],\n",[454,4887,4888],{"class":456,"line":2214},[454,4889,4890],{"class":3894},"    [ 140.80178756980726, 37.16607636013253 ],\n",[454,4892,4893],{"class":456,"line":2220},[454,4894,4895],{"class":3894},"    [ 141.99941083018345, 38.64436492965305 ],\n",[454,4897,4898],{"class":456,"line":2226},[454,4899,4900],{"class":3894},"    [ 143.24727313996488, 40.10993078543407 ],\n",[454,4902,4903],{"class":456,"line":2232},[454,4904,4905],{"class":3894},"    [ 144.54983478842868, 41.56151748104379 ],\n",[454,4907,4908],{"class":456,"line":2237},[454,4909,4910],{"class":3894},"    [ 145.91194328157903, 42.9977313513525 ],\n",[454,4912,4913],{"class":456,"line":2243},[454,4914,4915],{"class":3894},"    [ 147.33886324769875, 44.41702385324905 ],\n",[454,4917,4918],{"class":456,"line":2249},[454,4919,4920],{"class":3894},"    [ 148.83630454782175, 45.81767177569259 ],\n",[454,4922,4923],{"class":456,"line":2255},[454,4924,4925],{"class":3894},"    [ 150.41044655311683, 47.19775518537249 ],\n",[454,4927,4928],{"class":456,"line":4},[454,4929,4930],{"class":3894},"    [ 152.0679557258351, 48.55513303641521 ],\n",[454,4932,4933],{"class":456,"line":2265},[454,4934,4935],{"class":3894},"    [ 153.8159925691613, 49.88741647693477 ],\n",[454,4937,4938],{"class":456,"line":2270},[454,4939,4940],{"class":3894},"    [ 155.66220265313686, 51.19194004906208 ],\n",[454,4942,4943],{"class":456,"line":2276},[454,4944,4945],{"class":3894},"    [ 157.6146847535422, 52.465731224424154 ],\n",[454,4947,4948],{"class":456,"line":2282},[454,4949,4950],{"class":3894},"    [ 159.68192717003024, 53.705479070518045 ],\n",[454,4952,4953],{"class":456,"line":2288},[454,4954,4955],{"class":3894},"    [ 161.87270110244128, 54.90750333482882 ],\n",[454,4957,4958],{"class":456,"line":2294},[454,4959,4960],{"class":3894},"    [ 164.19589776721827, 56.06772589186521 ],\n",[454,4962,4963],{"class":456,"line":2300},[454,4964,4965],{"class":3894},"    [ 166.66029413034695, 57.18164734362052 ],\n",[454,4967,4968],{"class":456,"line":2306},[454,4969,4970],{"class":3894},"    [ 169.27423139783608, 58.24433259326387 ],\n",[454,4972,4973],{"class":456,"line":2311},[454,4974,4975],{"class":3894},"    [ 172.0451917704491, 59.25041037671838 ],\n",[454,4977,4979],{"class":456,"line":4978},73,[454,4980,4981],{"class":3894},"    [ 174.9792638386216, 60.19409291290832 ],\n",[454,4983,4985],{"class":456,"line":4984},74,[454,4986,4987],{"class":3894},"    [ 178.08049701800238, 61.069222785803966 ],\n",[454,4989,4991],{"class":456,"line":4990},75,[454,4992,4993],{"class":3894},"    [ 180, 61.53895140096846 ]\n",[454,4995,4997],{"class":456,"line":4996},76,[454,4998,4999],{"class":3894},"  ],\n",[454,5001,5003],{"class":456,"line":5002},77,[454,5004,4880],{"class":3894},[454,5006,5008],{"class":456,"line":5007},78,[454,5009,5010],{"class":3894},"    [ -180, 61.53895140096846 ],\n",[454,5012,5014],{"class":456,"line":5013},79,[454,5015,5016],{"class":3894},"    [ -178.64983787529644, 61.86935452669037 ],\n",[454,5018,5020],{"class":456,"line":5019},80,[454,5021,5022],{"class":3894},"    [ -175.2140408098754, 62.587877615485425 ],\n",[454,5024,5026],{"class":456,"line":5025},81,[454,5027,5028],{"class":3894},"    [ -171.618756489847, 63.21818519285117 ],\n",[454,5030,5032],{"class":456,"line":5031},82,[454,5033,5034],{"class":3894},"    [ -167.87562800253605, 63.753888204781546 ],\n",[454,5036,5038],{"class":456,"line":5037},83,[454,5039,5040],{"class":3894},"    [ -164.0017045661598, 64.18906791167207 ],\n",[454,5042,5044],{"class":456,"line":5043},84,[454,5045,5046],{"class":3894},"    [ -160.0194735821535, 64.51855132598804 ],\n",[454,5048,5050],{"class":456,"line":5049},85,[454,5051,5052],{"class":3894},"    [ -155.9564136218279, 64.73818576863974 ],\n",[454,5054,5056],{"class":456,"line":5055},86,[454,5057,5058],{"class":3894},"    [ -151.84402691822368, 64.84508272020929 ],\n",[454,5060,5062],{"class":456,"line":5061},87,[454,5063,5064],{"class":3894},"    [ -147.7164, 64.8378 ]\n",[454,5066,5068],{"class":456,"line":5067},88,[454,5069,5070],{"class":3894},"  ]\n",[454,5072,5074],{"class":456,"line":5073},89,[454,5075,4776],{"class":3894},[454,5077,5079],{"class":456,"line":5078},90,[454,5080,1340],{"class":3894},[2743,5082,5083],{},"html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":408,"searchDepth":469,"depth":469,"links":5085},[5086,5087,5088],{"id":3711,"depth":463,"text":3712},{"id":3740,"depth":463,"text":3740},{"id":3758,"depth":463,"text":3759,"children":5089},[5090],{"id":3765,"depth":469,"text":3766},[2793],"2024-09-22","平面の地図にまっすぐひいた線は最短の航路ではありません。",{},"\u002Farticles\u002F2024_09_22_line_great_circle_antimeridian_cutting",{"title":3700,"description":5093},"articles\u002F2024_09_22_line_great_circle_antimeridian_cutting",[4491,5099],"gis","2024_09_22_line_great_circle_antimeridian_cutting\u002Fthumbnail.webp","rjpze4IQqIg5qQZw4X3RE4NP18BGM-IbRT3T9c54h4c",{"id":5103,"title":5104,"body":5105,"category":5319,"createdAt":5321,"description":5322,"extension":2795,"index":2796,"meta":5323,"navigation":690,"path":5324,"publish":690,"seo":5325,"series":2796,"seriesTitle":2796,"stem":5326,"tag":5327,"thumbnail":5330,"updatedAt":2796,"__hash__":5331},"articles\u002Farticles\u002F2024_09_12_engineer_needs_math.md","エンジニアという職業に就くために数学は必要なのか？",{"type":10,"value":5106,"toc":5297},[5107,5110,5113,5116,5119,5123,5126,5130,5133,5136,5139,5143,5146,5149,5152,5155,5158,5161,5164,5167,5171,5174,5177,5181,5184,5187,5191,5194,5197,5200,5203,5206,5209,5212,5215,5218,5221,5224,5227,5230,5233,5239,5242,5248,5251,5254,5257,5260,5263,5266,5269,5273,5276,5279,5282,5285,5288,5291],[13,5108,5109],{},"エンジニアにとって数学が必要かどうかという議論は昔からあります。私の考えでは、「数学はまったく不要」とは言い切れないものの、大学で専攻するほどの知識は必須ではないと思っています。高校数学の基礎を理解していれば、ほとんどのエンジニアリング業務には対応できるでしょう。",[13,5111,5112],{},"実際のところ、数学がどれほど必要かは、どの分野のエンジニアを目指すかによって大きく異なります。例えば、アルゴリズム開発やAIの分野では高度な数学が不可欠です。一方で、フロントエンド開発などでは、それほど多くの数学知識は要求されません。このように、分野ごとに求められるスキルが異なるため、それに応じた準備が必要です。",[13,5114,5115],{},"また、最近では「数学は絶対に必要だ」とか「全くいらない」といった極端な主張をするYouTuberやサイトが目立ちますが、これらは前提となる「どの分野のエンジニア」を対象にしているかが曖昧なことが多いです。ただ不安を煽っているだけなので、あまり気にしない方が良いでしょう。",[13,5117,5118],{},"この記事では、どの分野で数学が必要か、逆に必要でない場合には何が重要とされるのかについて詳しく説明していきます。これからエンジニアにジョブチェンジしたい、またはエンジニアとして就職を考えているけれど、数学に自信がないという方々に役立ててもらえれば幸いです。",[53,5120,5122],{"id":5121},"必要な分野不要な分野","必要な分野、不要な分野",[13,5124,5125],{},"エンジニアリングの分野によって、数学が必要な度合いは大きく変わります。特に、アルゴリズム開発やAIの研究などの分野では、数学は避けて通れない要素です。逆に、フロントエンド開発やライブラリを用いた実装が中心の業務では、高度な数学の知識はほとんど必要ありません。以下でそれぞれの分野について、数学の重要性を説明していきます。",[53,5127,5129],{"id":5128},"アルゴリズム開発やaiなど","アルゴリズム開発やAIなど",[13,5131,5132],{},"アルゴリズム開発やAI（人工知能）の分野では、大学レベルの数学知識が必要不可欠です。特に、統計学、線形代数、微積分、確率論などは、これらの分野において基礎となるスキルです。AIのモデルを作る際には、データの処理や学習アルゴリズムを理解するために、数学的な知識が重要になります。数学が好きで得意な人にとっては、エンジニアリングの中でもこの分野は非常にやりがいのある選択肢と言えます。",[53,5134,5135],{"id":5135},"フロントエンドやライブラリを用いての実装中心",[13,5137,5138],{},"私が主に活動しているフロントエンドエンジニアリングの分野では、数学はそこまで重要ではありません。もちろん、組み合わせ論や論理的な思考は多少求められるものの、日常的な業務では、どちらかというとソフトウェア設計の考え方が重視されます。DB設計、クラス設計、UI設計など、ソフトウェア全体の構成をどうするかが重要なスキルです。",[53,5140,5142],{"id":5141},"アニメーション3dゲーム開発","アニメーション、3D、ゲーム開発",[13,5144,5145],{},"アニメーションや3D、ゲーム開発の分野では、ベクトルや三角関数といった数学の知識が役立つことがあります。例えば、アニメーションを描画する際には、フレームレートの調整や、物体の移動をスムーズに見せるために三角関数を使うことがあります。私も以前、Web上で水玉が特定の方向にふわふわと移動するアニメーションを実装した際、ベクトルと三角関数の知識が役に立ちました。",[13,5147,5148],{},"ゲーム開発においては、アニメーションの他に物理演算や処理負荷の最適化など、さらに複雑な数学の概念が求められることがあります。ただし、ノベルゲームのように動きが少ないゲームの場合は、そこまで高度な数学の知識は不要です。",[36,5150,5151],{"id":5151},"エンジニアの多くであろう実装中心あたりについて",[13,5153,5154],{},"エンジニアとしてのキャリアを歩む上で、多くの人が「実装中心」の分野に携わることになるでしょう。ここで言う「実装中心」とは、クライアントやプロダクトの要件に基づいてシステムやアプリケーションを構築する業務です。特にバックエンドやフロントエンド、またはその両方に携わるエンジニアが多く、この領域では数学的な知識よりも別のスキルセットが求められます。研究という要素が少なく、要件に従った実装やそのヒアリング、ライブラリやフレームワークを利用するための調査や構築を行うことが多いです。",[13,5156,5157],{},"といっても流石に数学が全く不要だったかというと設計や効率的なコードを記述するにあたり学んでおいてよかった数学の知識もあります。",[13,5159,5160],{},"具体的に必要なスキルセットや、知っておいてよかった数学の知識は以下の通りです。",[53,5162,5163],{"id":5163},"実務上のスキルセット",[13,5165,5166],{},"エンジニアとしてサービスを開発する際に、実際に必要だと感じたスキルは数多くあります。これらのスキルは、数学が得意でなくても習得できるものであり、私自身も日々の業務を通じて身につけてきました。数学的な知識が役立つ場面もあるかもしれませんが、実務においては、それ以上に重要な要素があります。",[60,5168,5170],{"id":5169},"db設計","DB設計",[13,5172,5173],{},"データベース設計は、エンジニアにとって基本的なスキルの一つです。データを効率的に取り扱い、冗長なデータをなくすために「正規化」の概念が必要になります。さらに、カラムの構成や役割を理解し、データベースから必要な情報を適切に取得するためのSQLコマンドも必須です。",[13,5175,5176],{},"リレーショナルデータベースの考え方を把握し、エンティティ関係図（ER図）を描けるようになると、データ同士の関係を視覚的に整理でき、設計作業がよりスムーズになります。最終的には、プロジェクトの要件に従い、データベースの構造をどう最適化するかが重要な課題となります。",[60,5178,5180],{"id":5179},"クラス設計オブジェクト指向","クラス設計（オブジェクト指向）",[13,5182,5183],{},"保守しやすいアプリケーション開発を行う上で、オブジェクト指向の概念は欠かせません。特に大規模なシステムや長期運用するサービスでは、コードの再利用性や拡張性を意識した設計が求められます。オブジェクト指向は学習が難しいと感じることもありますが、概念を理解しておくと、後の業務での作業が格段に楽になります。",[13,5185,5186],{},"実践的に学ぶためには、実際に自分でオブジェクト指向を用いたプロダクトを作成してみるのが効果的です。理論を覚えるよりも、プロジェクトを通じて実践的なスキルを養うことで、オブジェクト指向の本質に触れることができます。",[60,5188,5190],{"id":5189},"コミュニケーション力ヒアリング力","コミュニケーション力（ヒアリング力）",[13,5192,5193],{},"技術力以上に重要なのが、顧客やチームメンバーとのコミュニケーションです。特に、顧客が専門家でない場合、要望が非常に曖昧なことがあります。そのため、顧客の意図を汲み取り、具体的な要件として定義する能力が求められます。技術的な要件だけでなく、ビジネスのニーズを理解し、それをシステムに落とし込むことができるエンジニアは、プロジェクトを成功に導けるでしょう。",[13,5195,5196],{},"さらに、顧客が気づいていない問題やリスクを指摘し、改善点や便利な機能を提案できると、信頼関係が深まり、仕事の質も高まります。エンジニアにとって、ヒアリング力と提案力は非常に重要なスキルです。",[60,5198,5199],{"id":5199},"ドキュメントの読解と理解力",[13,5201,5202],{},"開発現場では、ゼロからコードを書き始めることは少なく、ほとんどのケースでフレームワークやライブラリを活用します。そのため、これらのツールのドキュメントをしっかりと読解し、理解する能力が重要です。英語のドキュメントが多いため、英語を抵抗なく読むことが求められますが、翻訳ツールを使いながらでも問題ありません。",[13,5204,5205],{},"ドキュメントを適切に理解できるかどうかで、開発の効率は大きく変わります。必要な情報を素早く引き出し、それを実装に反映する能力が、スムーズなプロジェクト進行に直結します。",[60,5207,5208],{"id":5208},"バグの調査や原因の探究",[13,5210,5211],{},"バグの解決は、開発業務において避けては通れない要素です。原因を素早く特定し、解決に導くスキルは、エンジニアにとって非常に重要です。特に、ライブラリやフレームワークを多用する現代の開発では、バグの原因が外部要因なのか、自分の実装ミスなのかを切り分ける力が必要です。",[13,5213,5214],{},"具体的には、ライブラリのリポジトリにあるIssueを参照したり、StackOverflowなどのコミュニティを活用しながら、同じ問題に遭遇した他のエンジニアの知見を活用することが求められます。問題の解決には、粘り強く調査し、正しい情報を集める力が欠かせません。",[53,5216,5217],{"id":5217},"実装中心でもあった方がいい数学知識",[13,5219,5220],{},"実装中心の業務において、数学知識が必須というわけではありませんが、知っておくと役立つ場面もあります。ただし、これらの知識を専門家レベルで理解する必要はありません。「どんなものかを説明できるか」が重要です。実際の計算処理はコンピュータが行ってくれるため、エンジニアとしては理論や式を構築する場面が多くなります。",[13,5222,5223],{},"また、実装では処理の流れや効率を重視することが多く、数学的な論理やパターンの考え方が役に立つことがあります。特に論理系の知識は、条件分岐やアルゴリズムの構築において重要です。",[60,5225,5226],{"id":5226},"集合と論理",[13,5228,5229],{},"集合や論理の知識は、条件分岐やデータの取り扱いに直結します。特に、条件式を効率的に記述する能力が求められます。知らないと、冗長な条件式を書いてしまうことがありますが、論理的な考え方を理解していれば、シンプルかつ効果的な条件分岐を作成できます。",[13,5231,5232],{},"例えば、以下のような冗長な条件式があります。",[402,5234,5237],{"className":5235,"code":5236,"language":407},[405],"if (x == 1 && (y == 2 || y == 3) && !(z == 0)) { ... }\n",[294,5238,5236],{"__ignoreMap":408},[13,5240,5241],{},"これを論理的に整理すると、以下のように簡潔な式にできます。",[402,5243,5246],{"className":5244,"code":5245,"language":407},[405],"if (x == 1 && y > 1 && y \u003C 4 && z != 0) { ... }\n",[294,5247,5245],{"__ignoreMap":408},[13,5249,5250],{},"論理を整理することで、コードが読みやすくなり、バグの発生も減らすことができます。",[60,5252,5253],{"id":5253},"組み合わせ",[13,5255,5256],{},"組み合わせの考え方は、要件の中で複数の要素が組み合わさる場面に役立ちます。例えば、ユーザーの入力によって異なる結果を表示するシステムでは、どのようなパターンがあり得るのかを予測し、それに対応した処理を実装することが必要です。特に、脆弱性を防ぐためにも、可能な限りパターンを見つけ出し、考慮に入れることが重要です。",[13,5258,5259],{},"具体的な数学的な組み合わせの計算を行うというよりも、要件に対してどんな組み合わせがあり得るかを考え、それをシステム設計に反映する能力が求められます。",[60,5261,5262],{"id":5262},"統計学",[13,5264,5265],{},"統計学は、データを扱う際に特に役立ちます。ただし、データ解析に携わらないエンジニアであれば、それほど深い理解は不要です。しかし、統計的な概念を理解していると、正確なデータ分析や結果の解釈ができるため、役立つ場面が出てくることもあります。",[13,5267,5268],{},"例えば、A\u002FBテストなどのデータ分析を行う際には、統計の基本的な知識があった方が、より精度の高い判断ができます。",[60,5270,5272],{"id":5271},"数列指数関数","数列、指数関数",[13,5274,5275],{},"数列や指数関数は、エンジニアリングの現場では、特に処理の効率やデータの増加に関わる場面で役立ちます。例えば、アルゴリズムの計算量を評価する際には、数列や指数関数の考え方が重要です。データが増加するにつれて処理時間やメモリ使用量がどのように変化するかを予測するためには、これらの概念が不可欠です。",[13,5277,5278],{},"また、物理シミュレーションやグラフィカルな処理においても、数列や指数関数を使って計算を行うことがあります。例えば、一定間隔で増減する値や、特定の割合で増加・減少するデータを処理する際には、数列や指数関数の知識が役立ちます。",[13,5280,5281],{},"さらに、リソース管理の観点からも、指数的に増加する処理負荷を見積もる際に使われます。大量のデータを処理するシステムにおいて、時間やリソースの効率的な利用を計画するために、指数関数の理解は重要です。",[36,5283,5284],{"id":5284},"でも数学は学んでおいて損はないと思う",[13,5286,5287],{},"エンジニアリング業務において、数学が直接的に必要ない場合も多いですが、学んでおいて損はありません。特に、実装中心のエンジニアであれば、まずは使用するプログラミング言語の仕様や設計の知識を優先的に習得すべきです。しかし、アニメーション系の開発、ゲーム開発、そして研究職になるほど、数学の重要性が高まっていきます。",[13,5289,5290],{},"数学は抽象的な概念を扱うため、最初は理解しにくく、何に役立つのかがわからないことが多いです。だからこそ、まずは実際にコードを書きながら実践し、必要性を感じたタイミングで学んでいくのも一つの方法です。例えば、私自身もGIS（地理情報システム）を用いたアプリケーションを開発する際に、数学の知識があればさらに深く学べたと思ったことがあります。",[13,5292,5293,5296],{},[289,5294,5295],{},"補足）GISとは？","\nGIS（Geographic Information System、地理情報システム）は、地理的なデータを収集・解析・可視化するためのシステムです。地図をベースにデータを管理・操作する技術で、都市計画、交通分析、環境モニタリングなど、さまざまな分野で利用されています。これらのアプリケーション開発においては、緯度や経度などの座標データを扱うため、三角関数やベクトルといった数学的な知識が役立ちます。",{"title":408,"searchDepth":469,"depth":469,"links":5298},[5299,5300,5301,5302,5303,5318],{"id":5121,"depth":469,"text":5122},{"id":5128,"depth":469,"text":5129},{"id":5135,"depth":469,"text":5135},{"id":5141,"depth":469,"text":5142},{"id":5151,"depth":463,"text":5151,"children":5304},[5305,5312],{"id":5163,"depth":469,"text":5163,"children":5306},[5307,5308,5309,5310,5311],{"id":5169,"depth":475,"text":5170},{"id":5179,"depth":475,"text":5180},{"id":5189,"depth":475,"text":5190},{"id":5199,"depth":475,"text":5199},{"id":5208,"depth":475,"text":5208},{"id":5217,"depth":469,"text":5217,"children":5313},[5314,5315,5316,5317],{"id":5226,"depth":475,"text":5226},{"id":5253,"depth":475,"text":5253},{"id":5262,"depth":475,"text":5262},{"id":5271,"depth":475,"text":5272},{"id":5284,"depth":463,"text":5284},[5320],"myopinion","2024-09-12","よく言われるエンジニア数学必要論について",{},"\u002Farticles\u002F2024_09_12_engineer_needs_math",{"title":5104,"description":5322},"articles\u002F2024_09_12_engineer_needs_math",[5328,5329],"dev_exp","carrier","2024_09_12_engineer_needs_math\u002Fthumbnail.webp","D_weARn9rztnF82t4tSpUG5nn-OAzcNSgt0p8tjz33E",{"id":5333,"title":5334,"body":5335,"category":6142,"createdAt":6143,"description":6144,"extension":2795,"index":2796,"meta":6145,"navigation":690,"path":6146,"publish":690,"seo":6147,"series":2796,"seriesTitle":2796,"stem":6148,"tag":6149,"thumbnail":6151,"updatedAt":2796,"__hash__":6152},"articles\u002Farticles\u002F2024_08_31_map_lib_price_guide.md","Webやネイティブアプリで地図表示・操作をしたいときのライブラリ選定、使用で考えたいこと",{"type":10,"value":5336,"toc":6108},[5337,5346,5349,5363,5366,5369,5372,5375,5378,5381,5384,5387,5391,5394,5397,5400,5403,5406,5409,5412,5415,5418,5421,5424,5427,5430,5433,5436,5440,5443,5446,5450,5453,5456,5459,5462,5466,5469,5472,5475,5478,5482,5485,5488,5491,5494,5497,5501,5504,5507,5510,5513,5517,5520,5523,5526,5529,5533,5536,5539,5542,5545,5549,5552,5555,5558,5561,5564,5567,5571,5574,5946,5950,5953,5972,5976,5979,5982,5985,5989,5992,6038,6042,6045,6049,6052,6077,6080,6083,6086,6089,6105],[13,5338,5339,5340,5345],{},"こんにちはjunです。私は個人開発で",[17,5341,5344],{"href":5342,"rel":5343},"https:\u002F\u002Froute-share.net",[21],"RouteShare","とよばれる地図を用いた地理情報・アクティビティ共有型webサービスを運営・開発しています。そのサービスではGoogle Map APIを使用して地図を表示したり、ユーザーが任意にピンをおいたりラインを引いたり、地図上から検索できるといった機能を持たせています。このように地図を表示したり、何かしら地図上で編集する機能などをサービスに加えたい時にGoogle Map APIやMapbxのSDKを使用することが多いと思います。しかし課金の体系がわからなかったり、意外と価格がすることもあります。",[13,5347,5348],{},"今回の記事では",[192,5350,5351,5354,5357,5360],{},[119,5352,5353],{},"地図ライブラリに必要なこと",[119,5355,5356],{},"いかにしてwebなどで地図を表示するのか",[119,5358,5359],{},"最低限の構成で低コストで運用する方法",[119,5361,5362],{},"メジャーライブラリの課金体系",[13,5364,5365],{},"という４点について詳細な内容をお伝えしたいとおもいます。主にこれから地図を利用したアプリケーションを開発、運営したいと思う技術者向けの内容です。",[36,5367,5368],{"id":5368},"地図を表示して操作するということ",[13,5370,5371],{},"Google MapやMapboxなどの地図ライブラリを使用して地図を表示し、操作できることは当たり前のように感じるかもしれませんが、その裏側には複雑な仕組みが存在します。ここでは、その仕組みをわかりやすく解説します。",[53,5373,5374],{"id":5374},"様々な操作に応じて指定した地図を表示する",[13,5376,5377],{},"地図アプリを使う際、ユーザーはズームイン・ズームアウト、パン（スワイプ）などの操作を行います。この時、地図ライブラリはその操作に応じて、表示する地図データを動的に更新します。例えば、ズームインすると、現在の中央緯度経度を基点に、より詳細な地図データを読み込み、表示範囲を狭めることで、より詳細な情報を表示します。",[53,5379,5380],{"id":5380},"地図を表示するためにはタイル画像を読み込んでいる",[13,5382,5383],{},"地図データは大きな一枚の画像ではなく、「タイル」と呼ばれる小さな画像に分割されています。これにより、地図を表示する際には、ユーザーの画面に表示される部分だけを効率的に読み込むことができます。例えば、ズームインしたり、パンしたりするたびに、必要な範囲のタイル画像が新たに読み込まれ、表示されます。Googleのネットワークを見てみるとこのようにタイルごとの画像をリクエストされているのがわかります。",[108,5385],{":src":5386,":width":111},"'2024_08_31_map_lib_price_guide\u002Fmaptitle_req.png'",[53,5388,5390],{"id":5389},"どこを表示しているのかをjsなどで制御し必要なタイルをリクエストする","どこを表示しているのかをJSなどで制御し、必要なタイルをリクエストする",[13,5392,5393],{},"地図ライブラリはJavaScriptなどを使って、現在の表示範囲やズームレベルを管理し、それに基づいて必要なタイル画像をサーバーにリクエストします。これにより、無駄なデータの読み込みを避け、効率的に地図を表示することができます。例えば、ユーザーが画面を移動させた際、表示範囲が変わったことを検知し、その範囲に対応するタイル画像を新たに取得して表示します。",[53,5395,5396],{"id":5396},"球体の地球を平面で問題なく表示できるようにするための工夫",[13,5398,5399],{},"地球は球体ですが、地図は平面に表示されます。この変換には「メルカトル図法」などの投影法が使われています。メルカトル図法は、経緯度を一定の間隔で平面に投影することで、球体の地球を平面の地図として表示することができます。ただし、この方法では緯度が高いほど形が歪むため、特に高緯度地方では地図のスケールが大きくなります。",[53,5401,5402],{"id":5402},"地図をスクラッチで開発する場合の考慮点",[13,5404,5405],{},"もし、地図表示機能をスクラッチで開発しようと考えると、いくつか非常に重要な技術や知識が必要となります。まず、地球は球体であるため、それを平面の地図として正確に表現するためには、地理計算技術や地学に関する深い知識が不可欠です。例えば、メルカトル図法などの投影法を用いて、経緯度を正確に平面に変換する必要があります。",[13,5407,5408],{},"さらに、ユーザーがズームイン・ズームアウト、パンなどの操作を行うたびに、その表示位置を正確に計算し、適切な地図データを表示するためのアルゴリズムが求められます。このような技術を実装するには、数学的な計算だけでなく、地理情報システム（GIS）に関する知識も必要です。",[13,5410,5411],{},"また、実際に地図を表示するためには、膨大な数のタイル画像を効率的に管理し、提供するためのタイルサーバも不可欠です。これを自前で構築するとなると、莫大なデータ容量を持つサーバと、それを高速に処理・配信するためのインフラが必要となります。例えば、地球全体をカバーするタイル画像をすべて用意した場合、その総データサイズは数百GBから数TBに達することがあります。",[13,5413,5414],{},"タイルサーバの必要枚数の計算\n地球全体をカバーするためのタイル画像の総枚数は、ズームレベルに依存します。タイルの総枚数は、ズームレベルz に対して次の式で計算されます：",[13,5416,5417],{},"タイル枚数 = 2^(2z)",[13,5419,5420],{},"例えば、ズームレベル0では1枚、ズームレベル1では4枚、ズームレベル2では16枚のタイルが必要です。ズームレベルが上がるごとに、必要なタイル枚数は指数関数的に増加します。ズームレベル10の場合、約1,048,576枚、ズームレベル15では約1,073,741,824枚ものタイルが必要となります。まして言語ごとに表示を変えるとならにさらに必要となります。",[13,5422,5423],{},"これらを保存し、ユーザーに迅速に提供するためには、高性能かつ大容量のストレージが必要となり、その運用コストも無視できません。静的でないサーバーサイドレンダリングでもサーバーパワーとキャッシュを工夫する知識が必要です。",[13,5425,5426],{},"特に、Web上で地図を描画する際には、CanvasやWebGLなどの描画技術が使われます。これにより、地図データをピクセル単位で描画し、ユーザーの操作に応じてリアルタイムで更新することが可能です。しかし、このような技術を一から実装することは非常に手間がかかり、パフォーマンスの最適化やブラウザ間の互換性を考慮する必要があります。",[13,5428,5429],{},"さらに、Google Mapを例にとると、短く・狭い範囲での２点は直線ですが広い範囲では曲線になります。実際は球面に沿ったまっすぐな線を引いていますが、それを平面に表した結果です。",[108,5431],{":src":5432,":width":111},"'2024_08_31_map_lib_price_guide\u002Fdistorted_map_example.png'",[13,5434,5435],{},"このように、広範囲での地図表示には特有の課題があり、それらを解決するための技術が必要となります。",[36,5437,5439],{"id":5438},"メジャーライブラリタイルサーバの仕組みと課金体系","メジャーライブラリ・タイルサーバの仕組みと課金体系",[13,5441,5442],{},"地図表示ライブラリやタイルサーバは、地図アプリケーションの開発において重要な要素です。ここでは、代表的なライブラリとタイルサーバの紹介とともに、それぞれのメリット、デメリット、制限事項、そして課金体系について解説します。なお2024年8月現在の内容です。",[53,5444,5445],{"id":5445},"地図表示ライブラリ",[60,5447,5449],{"id":5448},"google-map-sdk","Google Map SDK",[13,5451,5452],{},"メリット:世界中で広く使用され、信頼性が高い。多機能で詳細な地図データやストリートビューなど豊富なAPIを提供。ドキュメントが充実し、コミュニティサポートも豊富。SDK以外にも地理計算ライブラリが含まれており、高度なカスタマイズが可能。",[13,5454,5455],{},"デメリット:課金体系が比較的高額で、使用量が増えるとコストが急増する可能性がある。特定のデータや機能に制約がある場合がある。",[13,5457,5458],{},"制限事項:無料使用枠があるが、一定のリクエスト数を超えると課金が発生する。Googleの著作表示が必要。",[13,5460,5461],{},"課金体系:web・ネイティブ共に月間のSDKリクエスト数に基づく従量課金制。SDKのインスタンス作成時に課金され、料金はAPIごとに異なり、使用する機能によって変動する。",[60,5463,5465],{"id":5464},"mapbox-sdk","Mapbox SDK",[13,5467,5468],{},"メリット:新興のプラットフォームで、Google Mapよりオープンかつデベロッパーフレンドリー。高度にカスタマイズ可能で、地図のデザインやデータ表示を柔軟にコントロールできる。料金体系がGoogle Mapより安価で、小規模なプロジェクトやスタートアップに適している。",[13,5470,5471],{},"デメリット:Google Mapに比べてコミュニティ規模が小さく、サポートリソースが限られる場合がある。人気が出た場合、Google Mapに近い価格になる可能性がある。",[13,5473,5474],{},"制限事項:商用利用時に無料枠を超えると従量課金が発生する。Mapboxの著作表示が必要。",[13,5476,5477],{},"課金体系:web版はSDKのインスタンス作成時をSDKリクエストとして課金、ネイティブ版はアクセストークンから計測されるアクティブユーザー数に基づく。",[60,5479,5481],{"id":5480},"leafletjs","Leaflet.js",[13,5483,5484],{},"メリット:軽量でシンプルなオープンソースライブラリでカスタマイズ性が高い。大規模な地図アプリケーションだけでなく、軽量な地図表示にも適しており、無料で使用可能。",[13,5486,5487],{},"デメリット:標準機能は少なく、複雑な機能や高度なカスタマイズには他のプラグインやライブラリが必要。自前でタイルサーバを用意する必要がある場合がある。",[13,5489,5490],{},"制限事項:オープンソースのため、サポートはコミュニティに依存する部分が大きい。",[13,5492,5493],{},"課金体系:Leaflet.js自体は無料。ただし、タイルサーバ利用には別途コストが発生する場合がある。",[53,5495,5496],{"id":5496},"タイルサーバ",[60,5498,5500],{"id":5499},"maptiler","MapTiler",[13,5502,5503],{},"メリット:高品質な地図タイルを提供し、世界中の地図データにアクセス可能。カスタマイズが容易で商用利用もサポート。",[13,5505,5506],{},"デメリット:高度な機能や大規模な使用にはそれ相応のコストがかかる。",[13,5508,5509],{},"制限事項:商用利用には有料プランが必要。",[13,5511,5512],{},"課金体系:利用量に応じた従量課金制で、使用するタイル数やアクセス頻度に基づいて料金が決まる。",[60,5514,5516],{"id":5515},"openstreetmap","OpenStreetMap",[13,5518,5519],{},"メリット:オープンソースプロジェクトであり、無料で利用可能。世界中のユーザーがデータを提供し、地図データが豊富。主にテスト時や地図を印刷する際に使用される。",[13,5521,5522],{},"デメリット:商用利用は基本的に不可能で使用には制限がある。データの正確性や一貫性にバラつきがある場合がある。アクセスしすぎるとブロックされる。",[13,5524,5525],{},"制限事項:大規模な商用プロジェクトでの利用は推奨されない。",[13,5527,5528],{},"課金体系:無料で使用可能だが、商用利用時には著作表示が必要。",[60,5530,5532],{"id":5531},"mapbox-tile-server","Mapbox Tile Server",[13,5534,5535],{},"メリット:高品質な地図タイルを提供し、Mapbox SDKとの統合が容易。高度にカスタマイズ可能で、デザインや機能を自由に変更できる。",[13,5537,5538],{},"デメリット:使用量に応じてコストがかかるため、大規模なプロジェクトでは費用が高くなる可能性がある。",[13,5540,5541],{},"制限事項:無料使用枠があるが、商用利用には有料プランが必要。",[13,5543,5544],{},"課金体系:使用するタイル数やユーザー数に応じた従量課金制。",[60,5546,5548],{"id":5547},"google-map-tile","Google Map Tile",[13,5550,5551],{},"メリット:信頼性が高く、Googleの地図データを利用可能。多機能で、他のGoogleサービスとの連携が容易。",[13,5553,5554],{},"デメリット:料金が高めで、使用量が増えるとコストが急増する可能性がある。",[13,5556,5557],{},"制限事項:一定の無料枠を超えると課金が発生する。",[13,5559,5560],{},"課金体系:タイルのリクエスト数に基づいた従量課金制。",[36,5562,5563],{"id":5563},"webでの最小の構成",[13,5565,5566],{},"Webアプリケーションで地図を表示する際、コストを抑えつつシンプルに構成するためには、軽量な地図ライブラリと信頼性の高いタイルサーバを選ぶことが重要です。ここでは、Leaflet.jsとMapboxのタイルサーバを使用した最小構成について説明します。",[53,5568,5570],{"id":5569},"_1-leafletjsの導入","1. Leaflet.jsの導入",[13,5572,5573],{},"Leaflet.jsは、軽量でシンプルなオープンソースのJavaScriptライブラリです。基本的な地図表示機能を備えており、軽量でありながら高いカスタマイズ性を持っています。まずは、Leaflet.jsをプロジェクトに導入します。",[402,5575,5579],{"className":5576,"code":5577,"language":5578,"meta":408,"style":408},"language-html shiki shiki-themes material-theme-ocean","\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n    \u003Ctitle>Leaflet Map\u003C\u002Ftitle>\n    \u003Clink rel=\"stylesheet\" href=\"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.css\" \u002F>\n    \u003Cscript src=\"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.js\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cdiv id=\"map\" style=\"width: 600px; height: 400px;\">\u003C\u002Fdiv>\n    \u003Cscript>\n        var map = L.map('map').setView([51.505, -0.09], 13);\n\n        L.tileLayer('https:\u002F\u002Fapi.mapbox.com\u002Fstyles\u002Fv1\u002F{id}\u002Ftiles\u002F{z}\u002F{x}\u002F{y}?access_token=YOUR_MAPBOX_ACCESS_TOKEN', {\n            maxZoom: 18,\n            id: 'mapbox\u002Fstreets-v11',\n            tileSize: 512,\n            zoomOffset: -1,\n            accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN'\n        }).addTo(map);\n    \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html",[294,5580,5581,5595,5603,5612,5633,5667,5693,5701,5710,5746,5754,5810,5814,5837,5849,5865,5877,5890,5905,5921,5930,5938],{"__ignoreMap":408},[454,5582,5583,5586,5589,5592],{"class":456,"line":457},[454,5584,5585],{"class":2934},"\u003C!",[454,5587,5588],{"class":3967},"DOCTYPE",[454,5590,5591],{"class":2942}," html",[454,5593,5594],{"class":2934},">\n",[454,5596,5597,5599,5601],{"class":456,"line":463},[454,5598,3923],{"class":2934},[454,5600,5578],{"class":3967},[454,5602,5594],{"class":2934},[454,5604,5605,5607,5610],{"class":456,"line":469},[454,5606,3923],{"class":2934},[454,5608,5609],{"class":3967},"head",[454,5611,5594],{"class":2934},[454,5613,5614,5617,5620,5623,5626,5629,5631],{"class":456,"line":475},[454,5615,5616],{"class":2934},"    \u003C",[454,5618,5619],{"class":3967},"title",[454,5621,5622],{"class":2934},">",[454,5624,5625],{"class":3625},"Leaflet Map",[454,5627,5628],{"class":2934},"\u003C\u002F",[454,5630,5619],{"class":3967},[454,5632,5594],{"class":2934},[454,5634,5635,5637,5640,5643,5645,5647,5650,5652,5655,5657,5659,5662,5664],{"class":456,"line":481},[454,5636,5616],{"class":2934},[454,5638,5639],{"class":3967},"link",[454,5641,5642],{"class":2942}," rel",[454,5644,3846],{"class":2934},[454,5646,2946],{"class":2934},[454,5648,5649],{"class":2955},"stylesheet",[454,5651,2946],{"class":2934},[454,5653,5654],{"class":2942}," href",[454,5656,3846],{"class":2934},[454,5658,2946],{"class":2934},[454,5660,5661],{"class":2955},"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.css",[454,5663,2946],{"class":2934},[454,5665,5666],{"class":2934}," \u002F>\n",[454,5668,5669,5671,5674,5677,5679,5681,5684,5686,5689,5691],{"class":456,"line":487},[454,5670,5616],{"class":2934},[454,5672,5673],{"class":3967},"script",[454,5675,5676],{"class":2942}," src",[454,5678,3846],{"class":2934},[454,5680,2946],{"class":2934},[454,5682,5683],{"class":2955},"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.js",[454,5685,2946],{"class":2934},[454,5687,5688],{"class":2934},">\u003C\u002F",[454,5690,5673],{"class":3967},[454,5692,5594],{"class":2934},[454,5694,5695,5697,5699],{"class":456,"line":493},[454,5696,5628],{"class":2934},[454,5698,5609],{"class":3967},[454,5700,5594],{"class":2934},[454,5702,5703,5705,5708],{"class":456,"line":499},[454,5704,3923],{"class":2934},[454,5706,5707],{"class":3967},"body",[454,5709,5594],{"class":2934},[454,5711,5712,5714,5716,5719,5721,5723,5726,5728,5731,5733,5735,5738,5740,5742,5744],{"class":456,"line":505},[454,5713,5616],{"class":2934},[454,5715,864],{"class":3967},[454,5717,5718],{"class":2942}," id",[454,5720,3846],{"class":2934},[454,5722,2946],{"class":2934},[454,5724,5725],{"class":2955},"map",[454,5727,2946],{"class":2934},[454,5729,5730],{"class":2942}," style",[454,5732,3846],{"class":2934},[454,5734,2946],{"class":2934},[454,5736,5737],{"class":2955},"width: 600px; height: 400px;",[454,5739,2946],{"class":2934},[454,5741,5688],{"class":2934},[454,5743,864],{"class":3967},[454,5745,5594],{"class":2934},[454,5747,5748,5750,5752],{"class":456,"line":511},[454,5749,5616],{"class":2934},[454,5751,5673],{"class":3967},[454,5753,5594],{"class":2934},[454,5755,5756,5759,5762,5764,5767,5769,5771,5773,5775,5777,5779,5781,5783,5786,5788,5791,5793,5796,5799,5801,5803,5806,5808],{"class":456,"line":517},[454,5757,5758],{"class":2942},"        var",[454,5760,5761],{"class":3625}," map ",[454,5763,3846],{"class":2934},[454,5765,5766],{"class":3625}," L",[454,5768,3929],{"class":2934},[454,5770,5725],{"class":4021},[454,5772,4113],{"class":3625},[454,5774,3803],{"class":2934},[454,5776,5725],{"class":2955},[454,5778,3803],{"class":2934},[454,5780,4137],{"class":3625},[454,5782,3929],{"class":2934},[454,5784,5785],{"class":4021},"setView",[454,5787,4025],{"class":3625},[454,5789,5790],{"class":3914},"51.505",[454,5792,3852],{"class":2934},[454,5794,5795],{"class":2934}," -",[454,5797,5798],{"class":3914},"0.09",[454,5800,3974],{"class":3625},[454,5802,3852],{"class":2934},[454,5804,5805],{"class":3914}," 13",[454,5807,4137],{"class":3625},[454,5809,3806],{"class":2934},[454,5811,5812],{"class":456,"line":523},[454,5813,691],{"emptyLinePlaceholder":690},[454,5815,5816,5819,5821,5824,5826,5828,5831,5833,5835],{"class":456,"line":529},[454,5817,5818],{"class":3625},"        L",[454,5820,3929],{"class":2934},[454,5822,5823],{"class":4021},"tileLayer",[454,5825,4113],{"class":3625},[454,5827,3803],{"class":2934},[454,5829,5830],{"class":2955},"https:\u002F\u002Fapi.mapbox.com\u002Fstyles\u002Fv1\u002F{id}\u002Ftiles\u002F{z}\u002F{x}\u002F{y}?access_token=YOUR_MAPBOX_ACCESS_TOKEN",[454,5832,3803],{"class":2934},[454,5834,3852],{"class":2934},[454,5836,3620],{"class":2934},[454,5838,5839,5842,5844,5847],{"class":456,"line":535},[454,5840,5841],{"class":3967},"            maxZoom",[454,5843,2949],{"class":2934},[454,5845,5846],{"class":3914}," 18",[454,5848,2961],{"class":2934},[454,5850,5851,5854,5856,5858,5861,5863],{"class":456,"line":541},[454,5852,5853],{"class":3967},"            id",[454,5855,2949],{"class":2934},[454,5857,3797],{"class":2934},[454,5859,5860],{"class":2955},"mapbox\u002Fstreets-v11",[454,5862,3803],{"class":2934},[454,5864,2961],{"class":2934},[454,5866,5867,5870,5872,5875],{"class":456,"line":547},[454,5868,5869],{"class":3967},"            tileSize",[454,5871,2949],{"class":2934},[454,5873,5874],{"class":3914}," 512",[454,5876,2961],{"class":2934},[454,5878,5879,5882,5884,5886,5888],{"class":456,"line":553},[454,5880,5881],{"class":3967},"            zoomOffset",[454,5883,2949],{"class":2934},[454,5885,5795],{"class":2934},[454,5887,4033],{"class":3914},[454,5889,2961],{"class":2934},[454,5891,5892,5895,5897,5899,5902],{"class":456,"line":559},[454,5893,5894],{"class":3967},"            accessToken",[454,5896,2949],{"class":2934},[454,5898,3797],{"class":2934},[454,5900,5901],{"class":2955},"YOUR_MAPBOX_ACCESS_TOKEN",[454,5903,5904],{"class":2934},"'\n",[454,5906,5907,5909,5911,5913,5916,5919],{"class":456,"line":1028},[454,5908,4365],{"class":2934},[454,5910,4137],{"class":3625},[454,5912,3929],{"class":2934},[454,5914,5915],{"class":4021},"addTo",[454,5917,5918],{"class":3625},"(map)",[454,5920,3806],{"class":2934},[454,5922,5923,5926,5928],{"class":456,"line":1034},[454,5924,5925],{"class":2934},"    \u003C\u002F",[454,5927,5673],{"class":3967},[454,5929,5594],{"class":2934},[454,5931,5932,5934,5936],{"class":456,"line":1039},[454,5933,5628],{"class":2934},[454,5935,5707],{"class":3967},[454,5937,5594],{"class":2934},[454,5939,5940,5942,5944],{"class":456,"line":1045},[454,5941,5628],{"class":2934},[454,5943,5578],{"class":3967},[454,5945,5594],{"class":2934},[53,5947,5949],{"id":5948},"_2-mapboxタイルサーバの契約と設定","2. Mapboxタイルサーバの契約と設定",[13,5951,5952],{},"Leaflet.js自体は無料で利用できますが、タイル画像を提供するサーバが必要です。ここでは、Mapboxのタイルサーバを契約します。Mapboxは信頼性が高く、カスタマイズ性に優れたタイルサーバを提供しています。また無料枠内であればタイルも無料で利用できます。",[116,5954,5955,5963,5969],{},[119,5956,5957,5962],{},[17,5958,5961],{"href":5959,"rel":5960},"https:\u002F\u002Fwww.mapbox.com\u002F",[21],"Mapboxの公式サイト","でアカウントを作成します。",[119,5964,5965,5966,5968],{},"タイルを作成し、アクセストークンを取得し、上記のLeaflet.jsのコードにある ",[294,5967,5901],{}," の部分に取得したアクセストークンを入力します。",[119,5970,5971],{},"必要に応じて、地図のスタイルやズームレベルをカスタマイズできます。",[53,5973,5975],{"id":5974},"_3-最小構成のメリットと注意点","3. 最小構成のメリットと注意点",[13,5977,5978],{},"この最小構成では、Leaflet.jsの軽量なコードとMapboxの高品質なタイルサーバを組み合わせて、低コストでシンプルな地図表示を実現できます。ただし、商用利用の場合は、Mapboxの料金体系に注意し、利用量に応じたプランを選択する必要があります。\nまた、シンプルな構成であるため、複雑なカスタマイズや高負荷のユースケースには向いていない場合があります。プロジェクトの規模や要件に応じて、適切なライブラリやサーバ構成を検討することが重要です。",[36,5980,5981],{"id":5981},"部分的な仕様の場合",[13,5983,5984],{},"特定の地域や用途に限定した地図表示を行う場合、全世界をカバーする必要がないため、タイルアクセスを最適化することが可能です。ここでは、部分的な仕様における地図表示の方法について説明します。",[53,5986,5988],{"id":5987},"_1-エリア制限によるタイルアクセスの節約","1. エリア制限によるタイルアクセスの節約",[13,5990,5991],{},"特定の地域のみを対象とする場合、表示できるエリアを緯度経度範囲とズームレベルで制限することで、無駄なタイルアクセスを減らし、コストを節約できます。例えば、都市単位での地図表示や、特定の観光地を中心にしたアプリケーションの場合、必要最小限のエリアだけを表示するよう設定することで、効率的なタイルアクセスが可能です。",[402,5993,5997],{"className":5994,"code":5995,"language":5996,"meta":408,"style":408},"language-javascript shiki shiki-themes material-theme-ocean","var map = L.map('map', {\n    center: [35.6895, 139.6917], \u002F\u002F 東京の緯度経度\n    zoom: 12,\n    maxBounds: [\n        [35.0, 138.0], \u002F\u002F 南西端\n        [36.0, 140.0]  \u002F\u002F 北東端\n    ]\n});\n","javascript",[294,5998,5999,6004,6009,6014,6019,6024,6029,6033],{"__ignoreMap":408},[454,6000,6001],{"class":456,"line":457},[454,6002,6003],{},"var map = L.map('map', {\n",[454,6005,6006],{"class":456,"line":463},[454,6007,6008],{},"    center: [35.6895, 139.6917], \u002F\u002F 東京の緯度経度\n",[454,6010,6011],{"class":456,"line":469},[454,6012,6013],{},"    zoom: 12,\n",[454,6015,6016],{"class":456,"line":475},[454,6017,6018],{},"    maxBounds: [\n",[454,6020,6021],{"class":456,"line":481},[454,6022,6023],{},"        [35.0, 138.0], \u002F\u002F 南西端\n",[454,6025,6026],{"class":456,"line":487},[454,6027,6028],{},"        [36.0, 140.0]  \u002F\u002F 北東端\n",[454,6030,6031],{"class":456,"line":493},[454,6032,1184],{},[454,6034,6035],{"class":456,"line":499},[454,6036,6037],{},"});\n",[53,6039,6041],{"id":6040},"_2-webでの利用leafletjsの活用","2. Webでの利用：Leaflet.jsの活用",[13,6043,6044],{},"Webアプリケーションにおいて、軽量でカスタマイズ性の高いLeaflet.jsを利用することは有効です。Leaflet.jsは、シンプルな実装で特定エリアの地図表示を行うことができ、タイルサーバへのアクセスを最小限に抑えることができます。",[53,6046,6048],{"id":6047},"_3-日本国内のみの利用国土地理院のタイルサーバ","3. 日本国内のみの利用：国土地理院のタイルサーバ",[13,6050,6051],{},"日本国内のみを対象とする場合、国土地理院が提供するタイルサーバを利用することも一つの選択肢です。国土地理院のタイルは無料で利用可能で、日本の詳細な地図データを提供しています。Leaflet.jsと組み合わせることで、日本国内での特定エリアを効率的に表示することが可能です。",[402,6053,6055],{"className":5994,"code":6054,"language":5996,"meta":408,"style":408},"L.tileLayer('https:\u002F\u002Fcyberjapandata.gsi.go.jp\u002Fxyz\u002Fstd\u002F{z}\u002F{x}\u002F{y}.png', {\n    maxZoom: 18,\n    attribution: '© 国土地理院'\n}).addTo(map);\n",[294,6056,6057,6062,6067,6072],{"__ignoreMap":408},[454,6058,6059],{"class":456,"line":457},[454,6060,6061],{},"L.tileLayer('https:\u002F\u002Fcyberjapandata.gsi.go.jp\u002Fxyz\u002Fstd\u002F{z}\u002F{x}\u002F{y}.png', {\n",[454,6063,6064],{"class":456,"line":463},[454,6065,6066],{},"    maxZoom: 18,\n",[454,6068,6069],{"class":456,"line":469},[454,6070,6071],{},"    attribution: '© 国土地理院'\n",[454,6073,6074],{"class":456,"line":475},[454,6075,6076],{},"}).addTo(map);\n",[13,6078,6079],{},"このように、部分的な仕様の場合には、地図表示エリアを限定することで、コストを抑えつつ必要な情報を提供することが可能です。利用する地域や用途に応じて、最適な構成を選択してください。",[36,6081,6082],{"id":6082},"結論",[13,6084,6085],{},"とりあえず内容はこの通りです。記述した通り、地図表示というのはそれなりのサーバコストやライブラリの開発コストが高いため、ある意味一部のサービスの寡占状態になっています。しかし地図表示は非常にわかりやすく需要が多いためgoogle mapが高くても使わざる得ないような状態になっています。ですが、google map SDK以外にも上記の最小構成などである程度コストを節約することができるので、小さいサービスや自治体が地図を使用したいときなどに利用できます。ちなみにネイティブの場合はSDKに当たるものにleaflet.jsのようなものがないので難しいです。私もreact nativeで探したのですが見当たらない様子です。この辺は調べてまた見つかったら更新しようと思います。",[36,6087,6088],{"id":6088},"参考",[192,6090,6091,6098],{},[119,6092,6093],{},[17,6094,6097],{"href":6095,"rel":6096},"https:\u002F\u002Fspeakerdeck.com\u002Fsmellman\u002Fguo-nei-xiang-ketairusabafalsegou-zhu-toyun-yong-nituite?slide=22",[21],"国内向けタイルサーバの構築と運用について",[119,6099,6100],{},[17,6101,6104],{"href":6102,"rel":6103},"https:\u002F\u002Fwww.gsi.go.jp\u002FGIS\u002Fwhatisgis.html",[21],"GISとは",[2743,6106,6107],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}",{"title":408,"searchDepth":469,"depth":469,"links":6109},[6110,6117,6130,6135,6140,6141],{"id":5368,"depth":463,"text":5368,"children":6111},[6112,6113,6114,6115,6116],{"id":5374,"depth":469,"text":5374},{"id":5380,"depth":469,"text":5380},{"id":5389,"depth":469,"text":5390},{"id":5396,"depth":469,"text":5396},{"id":5402,"depth":469,"text":5402},{"id":5438,"depth":463,"text":5439,"children":6118},[6119,6124],{"id":5445,"depth":469,"text":5445,"children":6120},[6121,6122,6123],{"id":5448,"depth":475,"text":5449},{"id":5464,"depth":475,"text":5465},{"id":5480,"depth":475,"text":5481},{"id":5496,"depth":469,"text":5496,"children":6125},[6126,6127,6128,6129],{"id":5499,"depth":475,"text":5500},{"id":5515,"depth":475,"text":5516},{"id":5531,"depth":475,"text":5532},{"id":5547,"depth":475,"text":5548},{"id":5563,"depth":463,"text":5563,"children":6131},[6132,6133,6134],{"id":5569,"depth":469,"text":5570},{"id":5948,"depth":469,"text":5949},{"id":5974,"depth":469,"text":5975},{"id":5981,"depth":463,"text":5981,"children":6136},[6137,6138,6139],{"id":5987,"depth":469,"text":5988},{"id":6040,"depth":469,"text":6041},{"id":6047,"depth":469,"text":6048},{"id":6082,"depth":463,"text":6082},{"id":6088,"depth":463,"text":6088},[2793],"2024-08-31","地図アプリで考えておくこと",{},"\u002Farticles\u002F2024_08_31_map_lib_price_guide",{"title":5334,"description":6144},"articles\u002F2024_08_31_map_lib_price_guide",[4491,6150],"native","2024_08_31_map_lib_price_guide\u002Fthumbnail.webp","UCWkKbRmKWP-9IKYE6ttGwipqTvfmV2bZ1WTCC2d2BA",{"id":6154,"title":6155,"body":6156,"category":6555,"createdAt":6557,"description":6558,"extension":2795,"index":2796,"meta":6559,"navigation":690,"path":6560,"publish":690,"seo":6561,"series":2796,"seriesTitle":2796,"stem":6562,"tag":6563,"thumbnail":2796,"updatedAt":2796,"__hash__":6564},"articles\u002Farticles\u002F2024_06_28_laravel-guard-igonore-on-seeder.md","LaravelのSeederではModelのguardが無効になることで場合によって存在しないカラムへの挿入でエラーとなる",{"type":10,"value":6157,"toc":6550},[6158,6169,6176,6179,6182,6185,6267,6270,6273,6382,6385,6413,6416,6478,6492,6509,6512,6518,6537,6540,6542,6548],[13,6159,6160,6161,6164,6165,6168],{},"こんにちはjunです。Laravelのseederでfactoryの",[294,6162,6163],{},"create()","を使用せず、",[294,6166,6167],{},"new Model()","からなるモデルメソッドからデータ挿入を行った時に詰まったことがあり、その忘備録です。",[13,6170,6171,6172,6175],{},"結論としてSeederではモデルのguardが無効になり、",[294,6173,6174],{},"fill()","を使用した挿入で存在しないカラムへのinsertが走ってエラーになることが原因でした。背景から話すので、もしさっさと解決策を知りたい方は「解決方法」のとこまで移動してください。",[36,6177,6178],{"id":6178},"モデルベースで挿入処理をしたい",[13,6180,6181],{},"Laravelにはダミーデータを挿入するためにseederと呼ばれる挿入スクリプトファイル、factoryというfakerを利用して指定モデルへのダミーデータの定義を作成できます。とりあえずデータを作成してページングや表示の様子を確かめる時に非常に便利です。",[13,6183,6184],{},"ただしこのfacotryで挿入するときは「DBのinsert処理を走らせる」方法です。factoryと連結したモデルのキャストなどは考慮してくれますが、基本的にはDBのinsert文を作成するような感じです。",[402,6186,6188],{"className":447,"code":6187,"language":450,"meta":408,"style":408},"\u002F\u002F ※ Laravel6での記述方法です！\n\n\u002F** @var \\Illuminate\\Database\\Eloquent\\Factory $factory *\u002F\n\nuse App\\Models\\Article;\nuse Faker\\Generator as Faker;\n\n$factory->define(Article::class, function (Faker $faker) {\n    return [\n        'title'=>$faker->text(),\n        'content'=>$faker->realText(),\n        'is_active'=>true, \u002F\u002F キャストしてくれる。true => 1\n        'author'=> User::inRandomOrder()->first()->id \u002F\u002F ID番号を指定しないとダメ。\n    ];\n});\n\n",[294,6189,6190,6195,6199,6204,6208,6213,6218,6222,6227,6232,6237,6242,6250,6258,6263],{"__ignoreMap":408},[454,6191,6192],{"class":456,"line":457},[454,6193,6194],{},"\u002F\u002F ※ Laravel6での記述方法です！\n",[454,6196,6197],{"class":456,"line":463},[454,6198,691],{"emptyLinePlaceholder":690},[454,6200,6201],{"class":456,"line":469},[454,6202,6203],{},"\u002F** @var \\Illuminate\\Database\\Eloquent\\Factory $factory *\u002F\n",[454,6205,6206],{"class":456,"line":475},[454,6207,691],{"emptyLinePlaceholder":690},[454,6209,6210],{"class":456,"line":481},[454,6211,6212],{},"use App\\Models\\Article;\n",[454,6214,6215],{"class":456,"line":487},[454,6216,6217],{},"use Faker\\Generator as Faker;\n",[454,6219,6220],{"class":456,"line":493},[454,6221,691],{"emptyLinePlaceholder":690},[454,6223,6224],{"class":456,"line":499},[454,6225,6226],{},"$factory->define(Article::class, function (Faker $faker) {\n",[454,6228,6229],{"class":456,"line":505},[454,6230,6231],{},"    return [\n",[454,6233,6234],{"class":456,"line":511},[454,6235,6236],{},"        'title'=>$faker->text(),\n",[454,6238,6239],{"class":456,"line":517},[454,6240,6241],{},"        'content'=>$faker->realText(),\n",[454,6243,6244,6247],{"class":456,"line":523},[454,6245,6246],{},"        'is_active'=>true,",[454,6248,6249],{}," \u002F\u002F キャストしてくれる。true => 1\n",[454,6251,6252,6255],{"class":456,"line":529},[454,6253,6254],{},"        'author'=> User::inRandomOrder()->first()->id",[454,6256,6257],{}," \u002F\u002F ID番号を指定しないとダメ。\n",[454,6259,6260],{"class":456,"line":535},[454,6261,6262],{},"    ];\n",[454,6264,6265],{"class":456,"line":541},[454,6266,6037],{},[13,6268,6269],{},"複雑なリレーションがあったり、モデル内でなんやかんやして他のモデルやデータに変更を与える場合は上記の方法では面倒なことがあります。そのため、facotryで仮のhttpリクエストに相当する連想配列を作成しておき、リクエストボディから挿入処理を行うときのメソッドを経由してデータを作成したい時があります。",[13,6271,6272],{},"例ではこんな感じ..",[402,6274,6276],{"className":447,"code":6275,"language":450,"meta":408,"style":408},"\u002F\u002F Controller.php\npublic function createData(HogeRequest $request){\n    $m = new TargetModel();\n    $m->add($request->validated());\n    return $m\n}\n\n\n\u002F\u002F TargetModel.php\npublic function add(array $val){\n    $this->fill($val);\n    $this->save();\n    \u002F\u002F complex process...\n    return $this;\n}\n\u002F\u002F ↑このメソッドを使用したい！\n\n\n\u002F\u002F seeder\u002FinsertDummy.php\n$f = factory(App\\Models\\TargetModel.php)->make(); \u002F\u002F データの挿入はせず、fakerで作成した連想配列が取得できます..!\n$m = new TargetModel();\n$m->add($f);\n",[294,6277,6278,6283,6288,6293,6298,6303,6307,6311,6315,6320,6325,6330,6335,6340,6345,6349,6354,6358,6362,6367,6372,6377],{"__ignoreMap":408},[454,6279,6280],{"class":456,"line":457},[454,6281,6282],{},"\u002F\u002F Controller.php\n",[454,6284,6285],{"class":456,"line":463},[454,6286,6287],{},"public function createData(HogeRequest $request){\n",[454,6289,6290],{"class":456,"line":469},[454,6291,6292],{},"    $m = new TargetModel();\n",[454,6294,6295],{"class":456,"line":475},[454,6296,6297],{},"    $m->add($request->validated());\n",[454,6299,6300],{"class":456,"line":481},[454,6301,6302],{},"    return $m\n",[454,6304,6305],{"class":456,"line":487},[454,6306,562],{},[454,6308,6309],{"class":456,"line":493},[454,6310,691],{"emptyLinePlaceholder":690},[454,6312,6313],{"class":456,"line":499},[454,6314,691],{"emptyLinePlaceholder":690},[454,6316,6317],{"class":456,"line":505},[454,6318,6319],{},"\u002F\u002F TargetModel.php\n",[454,6321,6322],{"class":456,"line":511},[454,6323,6324],{},"public function add(array $val){\n",[454,6326,6327],{"class":456,"line":517},[454,6328,6329],{},"    $this->fill($val);\n",[454,6331,6332],{"class":456,"line":523},[454,6333,6334],{},"    $this->save();\n",[454,6336,6337],{"class":456,"line":529},[454,6338,6339],{},"    \u002F\u002F complex process...\n",[454,6341,6342],{"class":456,"line":535},[454,6343,6344],{},"    return $this;\n",[454,6346,6347],{"class":456,"line":541},[454,6348,562],{},[454,6350,6351],{"class":456,"line":547},[454,6352,6353],{},"\u002F\u002F ↑このメソッドを使用したい！\n",[454,6355,6356],{"class":456,"line":553},[454,6357,691],{"emptyLinePlaceholder":690},[454,6359,6360],{"class":456,"line":559},[454,6361,691],{"emptyLinePlaceholder":690},[454,6363,6364],{"class":456,"line":1028},[454,6365,6366],{},"\u002F\u002F seeder\u002FinsertDummy.php\n",[454,6368,6369],{"class":456,"line":1034},[454,6370,6371],{},"$f = factory(App\\Models\\TargetModel.php)->make(); \u002F\u002F データの挿入はせず、fakerで作成した連想配列が取得できます..!\n",[454,6373,6374],{"class":456,"line":1039},[454,6375,6376],{},"$m = new TargetModel();\n",[454,6378,6379],{"class":456,"line":1045},[454,6380,6381],{},"$m->add($f);\n",[13,6383,6384],{},"例として、id,title,contentのカラムを持つArticleテーブルとManyToManyのリレーションをもつTagsテーブルがあるとしましょう。作成する際のhttp bodyでは",[402,6386,6388],{"className":447,"code":6387,"language":450,"meta":408,"style":408},"[\n    'title'=>'タイトルだよ',\n    'content'=>'文章文章...'\n    'tags'=>[1,3]\n]\n",[294,6389,6390,6394,6399,6404,6409],{"__ignoreMap":408},[454,6391,6392],{"class":456,"line":457},[454,6393,4621],{},[454,6395,6396],{"class":456,"line":463},[454,6397,6398],{},"    'title'=>'タイトルだよ',\n",[454,6400,6401],{"class":456,"line":469},[454,6402,6403],{},"    'content'=>'文章文章...'\n",[454,6405,6406],{"class":456,"line":475},[454,6407,6408],{},"    'tags'=>[1,3]\n",[454,6410,6411],{"class":456,"line":481},[454,6412,4776],{},[13,6414,6415],{},"このようにフロントエンドから送信され、対象のArticleが作成されたのちに指定のTagsとのIDが連携されます。",[402,6417,6419],{"className":447,"code":6418,"language":450,"meta":408,"style":408},"\u002F\u002F Article.php\nprotected $guarded = ['id'];\n\npublic function tag(){\n    return $this->hasMany(\\App\\Models\\Tags);\n}\n\npublic function add(array $val){\n    $this->fill($val);\n    $this->save();\n    $this->sync($val['tags'])\n    return $this;\n}\n",[294,6420,6421,6426,6431,6435,6440,6445,6449,6453,6457,6461,6465,6470,6474],{"__ignoreMap":408},[454,6422,6423],{"class":456,"line":457},[454,6424,6425],{},"\u002F\u002F Article.php\n",[454,6427,6428],{"class":456,"line":463},[454,6429,6430],{},"protected $guarded = ['id'];\n",[454,6432,6433],{"class":456,"line":469},[454,6434,691],{"emptyLinePlaceholder":690},[454,6436,6437],{"class":456,"line":475},[454,6438,6439],{},"public function tag(){\n",[454,6441,6442],{"class":456,"line":481},[454,6443,6444],{},"    return $this->hasMany(\\App\\Models\\Tags);\n",[454,6446,6447],{"class":456,"line":487},[454,6448,562],{},[454,6450,6451],{"class":456,"line":493},[454,6452,691],{"emptyLinePlaceholder":690},[454,6454,6455],{"class":456,"line":499},[454,6456,6324],{},[454,6458,6459],{"class":456,"line":505},[454,6460,6329],{},[454,6462,6463],{"class":456,"line":511},[454,6464,6334],{},[454,6466,6467],{"class":456,"line":517},[454,6468,6469],{},"    $this->sync($val['tags'])\n",[454,6471,6472],{"class":456,"line":523},[454,6473,6344],{},[454,6475,6476],{"class":456,"line":529},[454,6477,562],{},[13,6479,6480,6481,6484,6485,6487,6488,6491],{},"モデルでは",[294,6482,6483],{},"guarded","を指定して、fillが使用できるようにします。リレーション部分のカラムはlaravelが対応してくれます。そのため、",[294,6486,6174],{},"では",[294,6489,6490],{},"tags","というカラムに挿入させるようなSQLは生成されなくなります。",[13,6493,6494,6495,6498,6499,6501,6502,6505,6506,6508],{},"しかしseederでこのメソッドを使用した時に ",[294,6496,6497],{},"Array to srting convertion"," というエラーが発生し詰まってしまいました。よくよくエラートレースを見ると、insert文のカラムに",[294,6500,6490],{},"が存在しているのがわかりました。「",[294,6503,6504],{},"filable","が正常に機能していないのか？」と予測をたてて調べたらどうやら「seeder中はfactory通りの内容が挿入されるように、",[294,6507,6483],{},"が無効になりfillの値が全て考慮される」とのことでした。fillableがワイルドカードのような状態となり、tagsがArticlesのカラムとして認識されたことが原因です。",[36,6510,6511],{"id":6511},"解決方法",[13,6513,6514,6515,6517],{},"ではseeder中でもモデルのfillable,",[294,6516,6483],{},"を有効にすれば解決します。その専用のメソッドがあります。",[402,6519,6521],{"className":447,"code":6520,"language":450,"meta":408,"style":408},"use Illuminate\\Database\\Eloquent\\Model;\n\nModel::reguard(); \u002F\u002F←これをfillの前に入れる\n",[294,6522,6523,6528,6532],{"__ignoreMap":408},[454,6524,6525],{"class":456,"line":457},[454,6526,6527],{},"use Illuminate\\Database\\Eloquent\\Model;\n",[454,6529,6530],{"class":456,"line":463},[454,6531,691],{"emptyLinePlaceholder":690},[454,6533,6534],{"class":456,"line":469},[454,6535,6536],{},"Model::reguard(); \u002F\u002F←これをfillの前に入れる\n",[13,6538,6539],{},"この静的メソッドを呼び出すことでguardedを再度有効にでき、指定・存在するカラムのみにfillが行われます。",[36,6541,6088],{"id":6088},[13,6543,6544],{},[17,6545,6546],{"href":6546,"rel":6547},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F67092809\u002Ffillable-doesnt-work-when-creating-model-from-seeder",[21],[2743,6549,2745],{},{"title":408,"searchDepth":469,"depth":469,"links":6551},[6552,6553,6554],{"id":6178,"depth":463,"text":6178},{"id":6511,"depth":463,"text":6511},{"id":6088,"depth":463,"text":6088},[6556],"ministack","2024-06-28","LaravelのSeederでModelメソッドを用いて挿入する場合の注意点",{},"\u002Farticles\u002F2024_06_28_laravel-guard-igonore-on-seeder",{"title":6155,"description":6558},"articles\u002F2024_06_28_laravel-guard-igonore-on-seeder",[2802],"V44O9yNCFMlfPQN4Yjg8vBnRzTGUlYiKV2q4aF6CG4U",{"id":6566,"title":6567,"body":6568,"category":7641,"createdAt":7642,"description":7643,"extension":2795,"index":2796,"meta":7644,"navigation":690,"path":7645,"publish":690,"seo":7646,"series":2796,"seriesTitle":2796,"stem":7647,"tag":7648,"thumbnail":2796,"updatedAt":2796,"__hash__":7649},"articles\u002Farticles\u002Fsearch-console-index-programmatically.md","Google Search ConsoleのインデックスリクエストをAPI通じて行う方法",{"type":10,"value":6569,"toc":7633},[6570,6578,6582,6589,6592,6595,6598,6601,6604,6612,6617,6620,6623,6629,6635,6638,6641,6644,6653,6656,6663,6674,6677,6680,6683,6686,6689,6692,6695,6698,6701,6705,6708,6713,6716,6719,6722,6725,6728,6731,6735,6744,6747,6750,7619,7622,7625,7628,7630],[13,6571,6572,6573,6577],{},"実装をさっさとみたい方は",[17,6574,6576],{"href":6575},"#api%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E7%9A%84%E3%81%AB%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B","APIを使用してプログラム的にリクエストする","へ移動してください。",[36,6579,6581],{"id":6580},"ページがgoogleに乗らない状態に","ページがGoogleに乗らない状態に",[13,6583,6584,6585,6588],{},"個人開発で作成したwebサービス ",[17,6586,5344],{"href":5342,"rel":6587},[21],"のユーザーインプレッション数を伸ばすため、Crowdワークスでライターを募集して記事をたくさん作成したが一向にOrganig Searchが伸びない現象に悩んでいました。",[13,6590,6591],{},"おもむろにSearch Consoleを調べている時に「インデックスの登録」というものを見つけました。すると",[108,6593],{":src":6594,":width":111},"'search-console-index-programmatically\u002Fsearch-console-index.png'",[13,6596,6597],{},"未登録の部分が200件となっていました。この未登録とはGoogle上で配信されていないという意味になります。つまり記事を書いたのにもかかわらず、記事が検索結果にでてこないというやばい状態です。",[13,6599,6600],{},"もう少し下に移動するとその未登録の理由がわかります。",[108,6602],{":src":6603,":width":111},"'search-console-index-programmatically\u002Findex-reason.png'",[13,6605,6606,6607,855],{},"自分の場合はこの「検出」というとこが200件ありました。この",[17,6608,6611],{"href":6609,"rel":6610},"https:\u002F\u002Fsupport.google.com\u002Fwebmasters\u002Fanswer\u002F7440203#discovered__unclear_status",[21],"検出というのは公式いわく",[3431,6613,6614],{},[13,6615,6616],{},"ページは Google により検出されましたが、まだクロールされていません。これは通常、Google が URL をクロールしようとしたものの、サイトへの過負荷が予想されたため、クロールの再スケジュールが必要となった場合です。そのため、レポート上で最終クロール日が空欄になっています。",[13,6618,6619],{},"という意味であり、サイトマップ上で新規のページが認識されているがサイト負荷が予想されたためクロール（インデックス登録）が見送られたということです。",[13,6621,6622],{},"この未登録状態を人力で解消するためには以下のようにします。",[13,6624,6625,6626],{},"1: 「検証」の行をクリックして表示される未登録のページ一覧のURLを確認\n",[108,6627],{":src":6628,":width":111},"'search-console-index-programmatically\u002Freslove1.png'",[13,6630,6631,6632],{},"2: URLを検査（一覧の虫眼鏡マーク）\n",[108,6633],{":src":6634,":width":111},"'search-console-index-programmatically\u002Freslove2.png'",[13,6636,6637],{},"3: 「インデックス登録をリクエスト」をクリック",[108,6639],{":src":6640,":width":111},"'search-console-index-programmatically\u002Freslove3.png'",[13,6642,6643],{},"リクエストを送ることでインデックス登録を再度行ってくれ、問題なければ登録されます。",[13,6645,6646,6647,6652],{},"一件ずつやってもいいのですが、検査やインデックスリクエストの時にローディングがかかるので200件あると結構時間がかかります。何かプログラム的にやる方法がないかを探したところ、",[17,6648,6651],{"href":6649,"rel":6650},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fapis\u002Findexing-api\u002Fv3\u002Fquickstart?hl=ja",[21],"Google Indexing API","というものがありました。",[36,6654,6576],{"id":6655},"apiを使用してプログラム的にリクエストする",[13,6657,6658,6662],{},[17,6659,6651],{"href":6660,"rel":6661},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fapis\u002Findexing-api\u002Fv3\u002Fquickstart",[21],"でおおまかな内容が書いてありますが、要約すると",[116,6664,6665,6668,6671],{},[119,6666,6667],{},"Google Cloud Platoform にてサービスアカウントを作成し、キーを取得する。（GCPのアカウントが必要です）",[119,6669,6670],{},"登録したアカウントのアドレスをSearch Consoleのユーザーに登録し、所有者と同じ権限を持たせる。",[119,6672,6673],{},"キーを用いたOauthでトークンを取得した後、対象のURLを更新するAPIリクエストを投げる。",[13,6675,6676],{},"上記の通りです。",[53,6678,6679],{"id":6679},"サービスアカウントの作成とキーの取得",[13,6681,6682],{},"GCPのアカウントがない場合はその作成から行ってください。（今回は省略）",[13,6684,6685],{},"「IAMと管理」から「サービスアカウント」に移動します。「サービスアカウントを作成」をクリックします。",[108,6687],{":src":6688,":width":111},"'search-console-index-programmatically\u002Fcreate-service-account.png'",[13,6690,6691],{},"アカウント名、IDを入力します。2と3は何も入力しなくて大丈夫です。",[13,6693,6694],{},"作成後に一覧に戻るので、作成したアカウントを詳細表示します。「キー」のタブに移動して「鍵を追加」から「新しい鍵を作成」をクリックします。",[108,6696],{":src":6697,":width":111},"'search-console-index-programmatically\u002Fcreate-key.png'",[13,6699,6700],{},"タイプでは「JSON」を選択し、作成をクリックします。JSONがダウンロードされますので、APIリクエストを行うディレクトリに保存しておいてください。まずはキーの取得が完了しました。",[53,6702,6704],{"id":6703},"サービスアカウントをserach-consoleに登録する","サービスアカウントをserach consoleに登録する",[13,6706,6707],{},"サービスアカウントがserach consoleを触れるようにユーザーとして登録しておきます。",[3431,6709,6710],{},[13,6711,6712],{},"公式のドキュメントが旧UIを参照した形で書かれていますので注意してください。このページでは2024年1月現在のUIで説明します。",[13,6714,6715],{},"まずは権限を付与したいサイト（プロパティー）を選択し、「設定」をクリックします。表示されたらユーザーと権限をクリックします。",[108,6717],{":src":6718,":width":111},"'search-console-index-programmatically\u002Fsearch-console-user.png'",[13,6720,6721],{},"ユーザーと権限で「ユーザーを追加」をクリックして、サービスアカウントのアドレスとコピペして権限を「オーナー（委任された所有者）」を選択します。",[108,6723],{":src":6724,":width":111},"'search-console-index-programmatically\u002Fuser-auth.png'",[108,6726],{":src":6727,":width":111},"'search-console-index-programmatically\u002Fadd-user.png'",[13,6729,6730],{},"サービスアカウントのアドレスはサービスアカウント一覧または、キーJSONのclient_emailから取得できます。権限を付与してアドレスを登録すればこのアカウントからAPIアクセスができるようになります。（アドレス、所有者の確認は不要）",[53,6732,6734],{"id":6733},"apiリクエストの実装","APIリクエストの実装",[13,6736,6737,6738,6743],{},"私はnode.jsで実装しました。google apiライブラリを使用して簡単に実装ができ、",[17,6739,6742],{"href":6740,"rel":6741},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fapis\u002Findexing-api\u002Fv3\u002Fprereqs",[21],"公式にもいくつかの言語でサンプルコード","があります。",[13,6745,6746],{},"サンプルコードは1件しかできないので、ダウンロードした未登録のURLCSVファイルを読み込んで必要分をAPIリクエストするようなコードにしました。（本来であればBatchリクエストがいいですが、難しかったので今回は1件ずつリクエストを送ります。機会があればBatchリクエストの解説もします）",[13,6748,6749],{},"またIndexing APIは200件ほど（多分）のレートリミットがあるのでリクエスト間と送信数は時間を明けた方がいいです。とりあえずコードは以下の通りです。",[402,6751,6753],{"className":5994,"code":6752,"language":5996,"meta":408,"style":408},"\nvar fs = require('fs');\nvar axios = require('axios');\nvar { google } = require('googleapis');\nvar key = require('.\u002Fkey.json');\nvar {parse} = require('csv-parse\u002Fsync');\nvar {stringify} = require('csv-stringify\u002Fsync');\n\nconst jwtClient = new google.auth.JWT(\n  key.client_email,\n  null,\n  key.private_key,\n  ['https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Findexing'],\n  null\n);\n\njwtClient.authorize(function(err,token) {\n  if (err) {\n    console.log(err);\n    return;\n  }\n\n  \u002F\u002F CSVファイルからURLを読み込む\n  var urls = parse(fs.readFileSync('.\u002Fdata.csv'), {\n    columns: false,\n    trim: true\n  });\n\n  var requestedUrls = [];\n\n  \u002F\u002F 各URLに対してリクエストを送信\n  let stop = false;\n  (async () => {\n    for (let i = 0; i \u003C urls.length; i++) {\n      if(stop) break;\n      let url = urls[i][0];\n      await new Promise(resolve => setTimeout(resolve, 1000)); \u002F\u002F 1秒の遅延\n\n      await axios.post(\"https:\u002F\u002Findexing.googleapis.com\u002Fv3\u002FurlNotifications:publish\",{\n        'url': url,\n        'type': 'URL_UPDATED'\n      },{\n        headers: {\n          'Content-Type': 'application\u002Fjson',\n          'Authorization': 'Bearer '+token.access_token\n        },\n      })\n      .then(res=>{\n        requestedUrls.push([url]);\n      })\n      .catch(err=>{\n        console.error(err)\n        stop = true;\n      })\n    }\n\n    \u002F\u002F 成功したURLを requested.csv に書き込む\n    fs.writeFileSync('requested.csv', stringify(requestedUrls));\n  })();\n});\n\n",[294,6754,6755,6759,6785,6809,6837,6861,6889,6917,6921,6947,6959,6964,6975,6991,6996,7002,7006,7035,7048,7065,7072,7077,7081,7086,7123,7136,7146,7155,7159,7172,7176,7181,7195,7210,7249,7266,7290,7327,7331,7355,7371,7389,7394,7403,7424,7452,7457,7465,7483,7501,7507,7522,7538,7550,7556,7560,7564,7569,7602,7611],{"__ignoreMap":408},[454,6756,6757],{"class":456,"line":457},[454,6758,691],{"emptyLinePlaceholder":690},[454,6760,6761,6764,6767,6769,6772,6774,6776,6779,6781,6783],{"class":456,"line":463},[454,6762,6763],{"class":2942},"var",[454,6765,6766],{"class":3625}," fs ",[454,6768,3846],{"class":2934},[454,6770,6771],{"class":4021}," require",[454,6773,4113],{"class":3625},[454,6775,3803],{"class":2934},[454,6777,6778],{"class":2955},"fs",[454,6780,3803],{"class":2934},[454,6782,4137],{"class":3625},[454,6784,3806],{"class":2934},[454,6786,6787,6789,6792,6794,6796,6798,6800,6803,6805,6807],{"class":456,"line":469},[454,6788,6763],{"class":2942},[454,6790,6791],{"class":3625}," axios ",[454,6793,3846],{"class":2934},[454,6795,6771],{"class":4021},[454,6797,4113],{"class":3625},[454,6799,3803],{"class":2934},[454,6801,6802],{"class":2955},"axios",[454,6804,3803],{"class":2934},[454,6806,4137],{"class":3625},[454,6808,3806],{"class":2934},[454,6810,6811,6813,6815,6818,6820,6822,6824,6826,6828,6831,6833,6835],{"class":456,"line":475},[454,6812,6763],{"class":2942},[454,6814,3813],{"class":2934},[454,6816,6817],{"class":3625}," google ",[454,6819,4463],{"class":2934},[454,6821,3962],{"class":2934},[454,6823,6771],{"class":4021},[454,6825,4113],{"class":3625},[454,6827,3803],{"class":2934},[454,6829,6830],{"class":2955},"googleapis",[454,6832,3803],{"class":2934},[454,6834,4137],{"class":3625},[454,6836,3806],{"class":2934},[454,6838,6839,6841,6844,6846,6848,6850,6852,6855,6857,6859],{"class":456,"line":481},[454,6840,6763],{"class":2942},[454,6842,6843],{"class":3625}," key ",[454,6845,3846],{"class":2934},[454,6847,6771],{"class":4021},[454,6849,4113],{"class":3625},[454,6851,3803],{"class":2934},[454,6853,6854],{"class":2955},".\u002Fkey.json",[454,6856,3803],{"class":2934},[454,6858,4137],{"class":3625},[454,6860,3806],{"class":2934},[454,6862,6863,6865,6867,6870,6872,6874,6876,6878,6880,6883,6885,6887],{"class":456,"line":487},[454,6864,6763],{"class":2942},[454,6866,3813],{"class":2934},[454,6868,6869],{"class":3625},"parse",[454,6871,4463],{"class":2934},[454,6873,3962],{"class":2934},[454,6875,6771],{"class":4021},[454,6877,4113],{"class":3625},[454,6879,3803],{"class":2934},[454,6881,6882],{"class":2955},"csv-parse\u002Fsync",[454,6884,3803],{"class":2934},[454,6886,4137],{"class":3625},[454,6888,3806],{"class":2934},[454,6890,6891,6893,6895,6898,6900,6902,6904,6906,6908,6911,6913,6915],{"class":456,"line":493},[454,6892,6763],{"class":2942},[454,6894,3813],{"class":2934},[454,6896,6897],{"class":3625},"stringify",[454,6899,4463],{"class":2934},[454,6901,3962],{"class":2934},[454,6903,6771],{"class":4021},[454,6905,4113],{"class":3625},[454,6907,3803],{"class":2934},[454,6909,6910],{"class":2955},"csv-stringify\u002Fsync",[454,6912,3803],{"class":2934},[454,6914,4137],{"class":3625},[454,6916,3806],{"class":2934},[454,6918,6919],{"class":456,"line":499},[454,6920,691],{"emptyLinePlaceholder":690},[454,6922,6923,6925,6928,6930,6933,6936,6938,6941,6943,6945],{"class":456,"line":505},[454,6924,3840],{"class":2942},[454,6926,6927],{"class":3625}," jwtClient ",[454,6929,3846],{"class":2934},[454,6931,6932],{"class":2934}," new",[454,6934,6935],{"class":3625}," google",[454,6937,3929],{"class":2934},[454,6939,6940],{"class":3625},"auth",[454,6942,3929],{"class":2934},[454,6944,63],{"class":4021},[454,6946,4535],{"class":3625},[454,6948,6949,6952,6954,6957],{"class":456,"line":511},[454,6950,6951],{"class":3625},"  key",[454,6953,3929],{"class":2934},[454,6955,6956],{"class":3625},"client_email",[454,6958,2961],{"class":2934},[454,6960,6961],{"class":456,"line":517},[454,6962,6963],{"class":2934},"  null,\n",[454,6965,6966,6968,6970,6973],{"class":456,"line":523},[454,6967,6951],{"class":3625},[454,6969,3929],{"class":2934},[454,6971,6972],{"class":3625},"private_key",[454,6974,2961],{"class":2934},[454,6976,6977,6980,6982,6985,6987,6989],{"class":456,"line":529},[454,6978,6979],{"class":3625},"  [",[454,6981,3803],{"class":2934},[454,6983,6984],{"class":2955},"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Findexing",[454,6986,3803],{"class":2934},[454,6988,3974],{"class":3625},[454,6990,2961],{"class":2934},[454,6992,6993],{"class":456,"line":535},[454,6994,6995],{"class":2934},"  null\n",[454,6997,6998,7000],{"class":456,"line":541},[454,6999,4137],{"class":3625},[454,7001,3806],{"class":2934},[454,7003,7004],{"class":456,"line":547},[454,7005,691],{"emptyLinePlaceholder":690},[454,7007,7008,7011,7013,7016,7018,7021,7023,7026,7028,7031,7033],{"class":456,"line":553},[454,7009,7010],{"class":3625},"jwtClient",[454,7012,3929],{"class":2934},[454,7014,7015],{"class":4021},"authorize",[454,7017,4113],{"class":3625},[454,7019,7020],{"class":2942},"function",[454,7022,4113],{"class":2934},[454,7024,7025],{"class":4325},"err",[454,7027,3852],{"class":2934},[454,7029,7030],{"class":4325},"token",[454,7032,4137],{"class":2934},[454,7034,3620],{"class":2934},[454,7036,7037,7040,7042,7044,7046],{"class":456,"line":559},[454,7038,7039],{"class":3781},"  if",[454,7041,3903],{"class":3967},[454,7043,7025],{"class":3625},[454,7045,3949],{"class":3967},[454,7047,466],{"class":2934},[454,7049,7050,7053,7055,7057,7059,7061,7063],{"class":456,"line":1028},[454,7051,7052],{"class":3625},"    console",[454,7054,3929],{"class":2934},[454,7056,4608],{"class":4021},[454,7058,4113],{"class":3967},[454,7060,7025],{"class":3625},[454,7062,4137],{"class":3967},[454,7064,3806],{"class":2934},[454,7066,7067,7070],{"class":456,"line":1034},[454,7068,7069],{"class":3781},"    return",[454,7071,3806],{"class":2934},[454,7073,7074],{"class":456,"line":1039},[454,7075,7076],{"class":2934},"  }\n",[454,7078,7079],{"class":456,"line":1045},[454,7080,691],{"emptyLinePlaceholder":690},[454,7082,7083],{"class":456,"line":1050},[454,7084,7085],{"class":3894},"  \u002F\u002F CSVファイルからURLを読み込む\n",[454,7087,7088,7091,7094,7096,7099,7101,7103,7105,7108,7110,7112,7115,7117,7119,7121],{"class":456,"line":1056},[454,7089,7090],{"class":2942},"  var",[454,7092,7093],{"class":3625}," urls",[454,7095,3962],{"class":2934},[454,7097,7098],{"class":4021}," parse",[454,7100,4113],{"class":3967},[454,7102,6778],{"class":3625},[454,7104,3929],{"class":2934},[454,7106,7107],{"class":4021},"readFileSync",[454,7109,4113],{"class":3967},[454,7111,3803],{"class":2934},[454,7113,7114],{"class":2955},".\u002Fdata.csv",[454,7116,3803],{"class":2934},[454,7118,4137],{"class":3967},[454,7120,3852],{"class":2934},[454,7122,3620],{"class":2934},[454,7124,7125,7128,7130,7134],{"class":456,"line":1062},[454,7126,7127],{"class":3967},"    columns",[454,7129,2949],{"class":2934},[454,7131,7133],{"class":7132},"sbqyR"," false",[454,7135,2961],{"class":2934},[454,7137,7138,7141,7143],{"class":456,"line":1068},[454,7139,7140],{"class":3967},"    trim",[454,7142,2949],{"class":2934},[454,7144,7145],{"class":7132}," true\n",[454,7147,7148,7151,7153],{"class":456,"line":1074},[454,7149,7150],{"class":2934},"  }",[454,7152,4137],{"class":3967},[454,7154,3806],{"class":2934},[454,7156,7157],{"class":456,"line":1079},[454,7158,691],{"emptyLinePlaceholder":690},[454,7160,7161,7163,7166,7168,7170],{"class":456,"line":1084},[454,7162,7090],{"class":2942},[454,7164,7165],{"class":3625}," requestedUrls",[454,7167,3962],{"class":2934},[454,7169,3883],{"class":3967},[454,7171,3806],{"class":2934},[454,7173,7174],{"class":456,"line":1090},[454,7175,691],{"emptyLinePlaceholder":690},[454,7177,7178],{"class":456,"line":1096},[454,7179,7180],{"class":3894},"  \u002F\u002F 各URLに対してリクエストを送信\n",[454,7182,7183,7186,7189,7191,7193],{"class":456,"line":1102},[454,7184,7185],{"class":2942},"  let",[454,7187,7188],{"class":3625}," stop",[454,7190,3962],{"class":2934},[454,7192,7133],{"class":7132},[454,7194,3806],{"class":2934},[454,7196,7197,7200,7203,7206,7208],{"class":456,"line":1107},[454,7198,7199],{"class":3967},"  (",[454,7201,7202],{"class":2942},"async",[454,7204,7205],{"class":2934}," ()",[454,7207,4331],{"class":2942},[454,7209,3620],{"class":2934},[454,7211,7212,7215,7217,7219,7221,7223,7225,7227,7229,7232,7234,7236,7239,7241,7243,7245,7247],{"class":456,"line":1113},[454,7213,7214],{"class":3781},"    for",[454,7216,3903],{"class":3967},[454,7218,3906],{"class":2942},[454,7220,3943],{"class":3625},[454,7222,3962],{"class":2934},[454,7224,3915],{"class":3914},[454,7226,3918],{"class":2934},[454,7228,3943],{"class":3625},[454,7230,7231],{"class":2934}," \u003C",[454,7233,7093],{"class":3625},[454,7235,3929],{"class":2934},[454,7237,7238],{"class":3625},"length",[454,7240,3918],{"class":2934},[454,7242,3943],{"class":3625},[454,7244,3946],{"class":2934},[454,7246,3949],{"class":3967},[454,7248,466],{"class":2934},[454,7250,7251,7254,7256,7259,7261,7264],{"class":456,"line":1852},[454,7252,7253],{"class":3781},"      if",[454,7255,4113],{"class":3967},[454,7257,7258],{"class":3625},"stop",[454,7260,3949],{"class":3967},[454,7262,7263],{"class":3781},"break",[454,7265,3806],{"class":2934},[454,7267,7268,7271,7274,7276,7278,7280,7282,7284,7286,7288],{"class":456,"line":1857},[454,7269,7270],{"class":2942},"      let",[454,7272,7273],{"class":3625}," url",[454,7275,3962],{"class":2934},[454,7277,7093],{"class":3625},[454,7279,3968],{"class":3967},[454,7281,3971],{"class":3625},[454,7283,4227],{"class":3967},[454,7285,4044],{"class":3914},[454,7287,3974],{"class":3967},[454,7289,3806],{"class":2934},[454,7291,7292,7295,7297,7300,7302,7305,7307,7310,7312,7314,7316,7319,7322,7324],{"class":456,"line":1862},[454,7293,7294],{"class":3781},"      await",[454,7296,6932],{"class":2934},[454,7298,7299],{"class":2988}," Promise",[454,7301,4113],{"class":3967},[454,7303,7304],{"class":4325},"resolve",[454,7306,4331],{"class":2942},[454,7308,7309],{"class":4021}," setTimeout",[454,7311,4113],{"class":3967},[454,7313,7304],{"class":3625},[454,7315,3852],{"class":2934},[454,7317,7318],{"class":3914}," 1000",[454,7320,7321],{"class":3967},"))",[454,7323,3918],{"class":2934},[454,7325,7326],{"class":3894}," \u002F\u002F 1秒の遅延\n",[454,7328,7329],{"class":456,"line":1868},[454,7330,691],{"emptyLinePlaceholder":690},[454,7332,7333,7335,7338,7340,7343,7345,7347,7350,7352],{"class":456,"line":1873},[454,7334,7294],{"class":3781},[454,7336,7337],{"class":3625}," axios",[454,7339,3929],{"class":2934},[454,7341,7342],{"class":4021},"post",[454,7344,4113],{"class":3967},[454,7346,2946],{"class":2934},[454,7348,7349],{"class":2955},"https:\u002F\u002Findexing.googleapis.com\u002Fv3\u002FurlNotifications:publish",[454,7351,2946],{"class":2934},[454,7353,7354],{"class":2934},",{\n",[454,7356,7357,7360,7363,7365,7367,7369],{"class":456,"line":1878},[454,7358,7359],{"class":2934},"        '",[454,7361,7362],{"class":3967},"url",[454,7364,3803],{"class":2934},[454,7366,2949],{"class":2934},[454,7368,7273],{"class":3625},[454,7370,2961],{"class":2934},[454,7372,7373,7375,7378,7380,7382,7384,7387],{"class":456,"line":1883},[454,7374,7359],{"class":2934},[454,7376,7377],{"class":3967},"type",[454,7379,3803],{"class":2934},[454,7381,2949],{"class":2934},[454,7383,3797],{"class":2934},[454,7385,7386],{"class":2955},"URL_UPDATED",[454,7388,5904],{"class":2934},[454,7390,7391],{"class":456,"line":1889},[454,7392,7393],{"class":2934},"      },{\n",[454,7395,7396,7399,7401],{"class":456,"line":1894},[454,7397,7398],{"class":3967},"        headers",[454,7400,2949],{"class":2934},[454,7402,3620],{"class":2934},[454,7404,7405,7408,7411,7413,7415,7417,7420,7422],{"class":456,"line":1900},[454,7406,7407],{"class":2934},"          '",[454,7409,7410],{"class":3967},"Content-Type",[454,7412,3803],{"class":2934},[454,7414,2949],{"class":2934},[454,7416,3797],{"class":2934},[454,7418,7419],{"class":2955},"application\u002Fjson",[454,7421,3803],{"class":2934},[454,7423,2961],{"class":2934},[454,7425,7426,7428,7431,7433,7435,7437,7440,7442,7445,7447,7449],{"class":456,"line":1905},[454,7427,7407],{"class":2934},[454,7429,7430],{"class":3967},"Authorization",[454,7432,3803],{"class":2934},[454,7434,2949],{"class":2934},[454,7436,3797],{"class":2934},[454,7438,7439],{"class":2955},"Bearer ",[454,7441,3803],{"class":2934},[454,7443,7444],{"class":2934},"+",[454,7446,7030],{"class":3625},[454,7448,3929],{"class":2934},[454,7450,7451],{"class":3625},"access_token\n",[454,7453,7454],{"class":456,"line":2162},[454,7455,7456],{"class":2934},"        },\n",[454,7458,7459,7462],{"class":456,"line":2168},[454,7460,7461],{"class":2934},"      }",[454,7463,7464],{"class":3967},")\n",[454,7466,7467,7470,7473,7475,7478,7481],{"class":456,"line":2173},[454,7468,7469],{"class":2934},"      .",[454,7471,7472],{"class":4021},"then",[454,7474,4113],{"class":3967},[454,7476,7477],{"class":4325},"res",[454,7479,7480],{"class":2942},"=>",[454,7482,466],{"class":2934},[454,7484,7485,7488,7490,7492,7494,7496,7499],{"class":456,"line":2179},[454,7486,7487],{"class":3625},"        requestedUrls",[454,7489,3929],{"class":2934},[454,7491,4244],{"class":4021},[454,7493,4025],{"class":3967},[454,7495,7362],{"class":3625},[454,7497,7498],{"class":3967},"])",[454,7500,3806],{"class":2934},[454,7502,7503,7505],{"class":456,"line":2185},[454,7504,7461],{"class":2934},[454,7506,7464],{"class":3967},[454,7508,7509,7511,7514,7516,7518,7520],{"class":456,"line":2191},[454,7510,7469],{"class":2934},[454,7512,7513],{"class":4021},"catch",[454,7515,4113],{"class":3967},[454,7517,7025],{"class":4325},[454,7519,7480],{"class":2942},[454,7521,466],{"class":2934},[454,7523,7524,7527,7529,7532,7534,7536],{"class":456,"line":2197},[454,7525,7526],{"class":3625},"        console",[454,7528,3929],{"class":2934},[454,7530,7531],{"class":4021},"error",[454,7533,4113],{"class":3967},[454,7535,7025],{"class":3625},[454,7537,7464],{"class":3967},[454,7539,7540,7543,7545,7548],{"class":456,"line":2202},[454,7541,7542],{"class":3625},"        stop",[454,7544,3962],{"class":2934},[454,7546,7547],{"class":7132}," true",[454,7549,3806],{"class":2934},[454,7551,7552,7554],{"class":456,"line":2208},[454,7553,7461],{"class":2934},[454,7555,7464],{"class":3967},[454,7557,7558],{"class":456,"line":2214},[454,7559,1110],{"class":2934},[454,7561,7562],{"class":456,"line":2220},[454,7563,691],{"emptyLinePlaceholder":690},[454,7565,7566],{"class":456,"line":2226},[454,7567,7568],{"class":3894},"    \u002F\u002F 成功したURLを requested.csv に書き込む\n",[454,7570,7571,7574,7576,7579,7581,7583,7586,7588,7590,7593,7595,7598,7600],{"class":456,"line":2232},[454,7572,7573],{"class":3625},"    fs",[454,7575,3929],{"class":2934},[454,7577,7578],{"class":4021},"writeFileSync",[454,7580,4113],{"class":3967},[454,7582,3803],{"class":2934},[454,7584,7585],{"class":2955},"requested.csv",[454,7587,3803],{"class":2934},[454,7589,3852],{"class":2934},[454,7591,7592],{"class":4021}," stringify",[454,7594,4113],{"class":3967},[454,7596,7597],{"class":3625},"requestedUrls",[454,7599,7321],{"class":3967},[454,7601,3806],{"class":2934},[454,7603,7604,7606,7609],{"class":456,"line":2237},[454,7605,7150],{"class":2934},[454,7607,7608],{"class":3967},")()",[454,7610,3806],{"class":2934},[454,7612,7613,7615,7617],{"class":456,"line":2243},[454,7614,4463],{"class":2934},[454,7616,4137],{"class":3625},[454,7618,3806],{"class":2934},[13,7620,7621],{},"更新が行われたかを確認したいときは、対象のURLをserach consoleで検査したときに「ページのインデックス登録」のタブでクロールされたか、インデックス登録を許可が「はい」になっているかで確認できます。",[13,7623,7624],{},"もし上記の方法でインデックスされない場合はページの品質が悪いか、URLで変なリダイレクトが起きていたり、正常に表示できていない可能性がありますのでサーバの設定などを見直してみてください。",[13,7626,7627],{},"私の方でもなんとか200件のインデックスが行われました。（右側の緑が急激に増えている）",[108,7629],{":src":6594,":width":111},[2743,7631,7632],{},"html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sbqyR, html code.shiki .sbqyR{--shiki-default:#FF9CAC}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":408,"searchDepth":469,"depth":469,"links":7634},[7635,7636],{"id":6580,"depth":463,"text":6581},{"id":6655,"depth":463,"text":6576,"children":7637},[7638,7639,7640],{"id":6679,"depth":469,"text":6679},{"id":6703,"depth":469,"text":6704},{"id":6733,"depth":469,"text":6734},[2793],"2023-12-31","Google Search ConsoleのインデックスリクエストをAPI通じて行う",{},"\u002Farticles\u002Fsearch-console-index-programmatically",{"title":6567,"description":7643},"articles\u002Fsearch-console-index-programmatically",[],"KKGIUM7ALrbkGgHWiRqk1CA0kYCz6TJfovN44yVxLL0",{"id":7651,"title":7652,"body":7653,"category":7953,"createdAt":7954,"description":7955,"extension":2795,"index":2796,"meta":7956,"navigation":690,"path":7957,"publish":690,"seo":7958,"series":2796,"seriesTitle":2796,"stem":7959,"tag":7960,"thumbnail":2796,"updatedAt":2796,"__hash__":7961},"articles\u002Farticles\u002Fremote-work-tips.md","リモート生活３年目その備品や生活習慣など",{"type":10,"value":7654,"toc":7933},[7655,7658,7661,7664,7667,7670,7687,7690,7704,7707,7710,7713,7716,7719,7722,7725,7736,7739,7742,7759,7762,7765,7768,7771,7774,7777,7780,7783,7786,7790,7793,7796,7799,7802,7805,7809,7812,7815,7818,7821,7824,7827,7850,7853,7856,7859,7863,7866,7869,7872,7875,7878,7881,7884,7887,7904,7907,7910,7913,7916,7919,7927,7930],[13,7656,7657],{},"こんにちはjunです。2023年になり国外では規制が緩和されたり、前年に比べると次第に落ち着きを取り戻しつつありました。しかし何やかんやリモート生活3年目となり、大きなプロジェクトもリモートで済ますことができたりなど、十分にリモートでの仕事ができる様になってきました。今回の記事では技術的な内容はなく、3年のリモート生活であった方がいいもの、買ってよかったものなどを共有したいと思います。",[13,7659,7660],{},"職種はエンジニアを想定していますが、基本的にPCのデスクワーク系であれば変わらないです。また備品の紹介とともに生活習慣や感じたことについても書いていこうかなと思います。",[36,7662,7663],{"id":7663},"備品編",[13,7665,7666],{},"ざっとまとめると以下の通り",[13,7668,7669],{},"仕事では",[192,7671,7672,7675,7678,7681,7684],{},[119,7673,7674],{},"セカンドディスプレイ",[119,7676,7677],{},"USB・端子ハブ",[119,7679,7680],{},"コンセントハブ",[119,7682,7683],{},"すごい良い椅子",[119,7685,7686],{},"webカメラ",[13,7688,7689],{},"生活習慣などでは",[192,7691,7692,7695,7698,7701],{},[119,7693,7694],{},"分厚い敷布団",[119,7696,7697],{},"3本ローラー",[119,7699,7700],{},"コーヒーや飲み物セット",[119,7702,7703],{},"でかい冷凍庫",[53,7705,7706],{"id":7706},"腰と姿勢に関わるものはケチらない",[13,7708,7709],{},"リモートワークで仕事する時はオフィスと同じ様に机にPCを置くのは変わりませんが、費用的な問題で現在家にあるものでやりがちです。しかし机や椅子といった姿勢に関わるものは絶対に妥協すべきではありません。",[13,7711,7712],{},"私は一時期コタツと座椅子の上でデスクワークしましたが、猫背気味になっていまい腰を結構痛めました。また視線が下気味になると、首が痛くなります。",[13,7714,7715],{},"オフィスでも腰や首の凝りに悩む人は多いですが、デスクワークでは家でやるという緊張感からの解放や費用的な問題で、机や椅子への投資を軽視しがちです。健康を損なうこともあり、作業効率のUPにもつながるのでまず正しく、やりやすい姿勢で作業できる机と椅子を新調することは結構重要です。また、仕事を進める上での道具であるPCやキーボード、ディスプレイについても適切な位置に配置することが重要です。",[60,7717,7718],{"id":7718},"椅子と机",[13,7720,7721],{},"まず椅子と机ですが、コタツはやめた方がいいです。20代半ばの青二才でも3ヶ月ほどで腰痛と首痛がひどくなり、整体に行き、サロンパスにお世話になる羽目になりました。",[13,7723,7724],{},"コタツでなくとも",[192,7726,7727,7730,7733],{},[119,7728,7729],{},"モニターを見るのに自然な首の位置にならない、意識的に首を曲げる必要がある。",[119,7731,7732],{},"前傾姿勢になっていまう。",[119,7734,7735],{},"肘が90度にならない",[13,7737,7738],{},"そんな机と椅子の場合も新調した方がいいです。（ただし下になんか敷いたり、後述の備品で何とかなる場合もあります。）",[13,7740,7741],{},"椅子に関してはニトリで購入したリクライニングチェアを使用しています。椅子の要件としては",[192,7743,7744,7747,7750,7753,7756],{},[119,7745,7746],{},"高さの調整が可能",[119,7748,7749],{},"肘掛けの調整が可能",[119,7751,7752],{},"リクライニングができる",[119,7754,7755],{},"腰の部分が膨らんでいて、腰が浮かないようになっていること（重要）",[119,7757,7758],{},"足置きがあること",[13,7760,7761],{},"以上の機能があると仕事の質が変わります笑",[53,7763,7764],{"id":7764},"サブディスプレイは必須",[13,7766,7767],{},"エンジニアであればサブディスプレイは必須です。前述の視点を整えて、首の痛みをなくすことにもなりますが単純に作業スピードが上がります。",[13,7769,7770],{},"ソースコードを変更してその結果を確認したり、メールの確認など仕事では多くのウィンドウを使用します。その時サブディスプレイがないとかなり作業効率が落ちます。",[13,7772,7773],{},"私の場合は27インチのそんなに高くないディスプレイをヨドバシで買いました。2万ちょっとでそれほどしません。",[13,7775,7776],{},"アニメーター、デザイナーなど表示されるものが高画質で色の表現がシビアなものであれば別のディスプレイがいいですが、プログラムとその確認であれば広くて安価なディスプレイで十分です。",[108,7778],{":src":7779,":width":111},"'remote-work-tips\u002F20230827_192107.jpg'",[13,7781,7782],{},"こんな感じで左にPC、真ん中にディスプレイをおいています。メインの作業はサブディスプレイで行い、メールや確認作業はPCで行っています。",[13,7784,7785],{},"一番作業で使用して見続けるものを正面に配置し、残りのディスプレイを左右に置くといった感じです。",[53,7787,7789],{"id":7788},"ないと意外と困るものあるといいもの","ないと意外と困るもの・あるといいもの",[13,7791,7792],{},"買ってよかったものを紹介します。",[60,7794,7795],{"id":7795},"トラックパッド",[13,7797,7798],{},"Macを使用しているため純正のトラックパッドを使用しています。トラックパッドの良い点はマウス操作によって肩が凝りにくくなること、Macの場合は指を使ったウィンドウの操作がすぐに行えることです。",[13,7800,7801],{},"マウスは移動をする際に腕全体を使うため、腕と肩が疲れます。しかしトラックパッドは手首から先を動かすので、腕・肩が疲れません。Macであれば３本指でそれぞれのディスプレイを遷移したり、ズームができたりと便利です。",[13,7803,7804],{},"トラックパッドが苦手な方は、ホイールボール付きマウスを使うのもいいかもしれません。ホイールでカーソルを移動することができ、トラックパッドに似た操作が可能です。",[60,7806,7808],{"id":7807},"pcスタンド","PCスタンド",[13,7810,7811],{},"私はMacbookProを使用しています。もし既存のキーボードを使用してサブディスプレイを使いたい場合、PCを前に置いてサブディスプレイを奥、または左右に置く必要があります。",[13,7813,7814],{},"しかしそれでは姿勢が崩れやすく、視点も下がってしまうため好きではありません。ラップトップは特に視点が下に下がりやすいため、PCスタンドを用いて適切な高さに配置できる様にした方がいいです。",[13,7816,7817],{},"単純にサブディスプレイで高さを揃えるだけなら適当な箱でもいけます。しかし、サブディスプレイを買うほどでもない方は、スタンドがあると斜めに配置できます。斜めになることでキーボードが打ちやすく、視線も自然な位置になります。箱を使うと高さと肘の角度が微妙になってしまいます。",[60,7819,7677],{"id":7820},"usb端子ハブ",[13,7822,7823],{},"ないと意外と困ったものは端子ハブです。サブディスプレイ、webカメラ、そのほか諸々のデバイスとPCを繋げると端子が対応していなかったり、足りないことがあります。ディスプレイの場合HDMIで接続することが多く、既存のPCにはその端子がないことが多いです。単純にPCのUSBハブ⇆HDMI端子に変換するアダプターもありますが、1対1で繋いでいると次第に端子が少なくなったり配線が多くなりごちゃごちゃします。",[13,7825,7826],{},"その場合は端子ハブを購入して、1つのPCの端子から複数の端子に接続できるものがあるとすごい便利です。私はTUNEWEARのALMIGHTY DOCK CS1を使用しています。USB typeCでmacに接続し以下の端子が利用可能です。",[192,7828,7829,7832,7835,7838,7841,7844,7847],{},[119,7830,7831],{},"USB 3.0 typeA",[119,7833,7834],{},"HDMI 4 k",[119,7836,7837],{},"SD",[119,7839,7840],{},"microSD",[119,7842,7843],{},"USB typeC Thunderbolt",[119,7845,7846],{},"イヤホンジャック",[119,7848,7849],{},"インサーネット",[13,7851,7852],{},"ちょっとお高めですが1年半は問題なく稼働しているので信頼できます。",[36,7854,7855],{"id":7855},"生活習慣編",[13,7857,7858],{},"次は生活習慣編です。",[53,7860,7862],{"id":7861},"さびしい","さびしい...",[13,7864,7865],{},"リモートでは基本的に家に一人（独身の場合）になることが多いです。SlackやZoomなどで仕事のコミュニケーションはとれますが、本当に仕事に関する報連相だけになり雑談などはほとんどないです。また人と会わないので次第に孤独感が強くなります。特に孤独感は厄介でチーム内で不仲やうまくプロジェクトが回らないと他人に対して大きな不信感を増長させることがあります。そのためできる限り人とコミュニケーションは取れる工夫をしておいた方がいいです。",[13,7867,7868],{},"あと全く出会いもないのでスポーツクラブとか積極的に何かしらの活動に参加していくといいです。",[53,7870,7871],{"id":7871},"運動不足は危険だよ",[13,7873,7874],{},"運動不足は結構危険です。通勤がないのはとてもいいのですがある意味最低限の運動という側面もありました。日によっては一歩も外に出ない日もあり、体重も増えがちでした。体力の落ちようは特に感じており、リモートで楽になったのにもかかわらず19時あたりにかなり疲れと眠気を感じてしまうこともありました。",[13,7876,7877],{},"運動不足の割には食事を取りすぎてしまったりするので、散歩したり定期的な運動をした方がいいです。私の場合は3本ローラーという家で自転車を回せるガジェットを買いました。（サイクリングが趣味なため）",[53,7879,7880],{"id":7880},"メリハリつけよう",[13,7882,7883],{},"仕事場=家になるので仕事とプライベートの境界が非常に曖昧になります。なんとなく業務時間外にプロジェクトファイルを開いて作業してしまったり、逆に仕事の時間に遊んでしまいそうになることもあります。そのため難しいですが両者の境界をうまく分ける工夫が入ります。",[13,7885,7886],{},"例えば",[192,7888,7889,7892,7895,7898,7901],{},[119,7890,7891],{},"PCに関しては仕事用と私用で端末やプロファイルを用意して分ける。",[119,7893,7894],{},"寝室には仕事用のPCは持ち込まない。",[119,7896,7897],{},"オフの時はSlackなど仕事の通知はオフにする。",[119,7899,7900],{},"オンの時はゲームや使用の通知はオフにする。",[119,7902,7903],{},"誘惑は目のつかないとこにしまう",[13,7905,7906],{},"と物理的に分けることがおすすめです。ただし費用、部屋の構造上難しいとこもあるので極力分けるようにすることをお勧めします。",[13,7908,7909],{},"特に私の場合 nintendo switchとスプラトゥーンを買ってからものすごいハマってしまい、始業15分前までやっていたりしたこともありました。流石に他のやりたいこともあるのでKitchen Safeと呼ばれる時間まで開かない封印箱を買って自制しました笑",[53,7911,7912],{"id":7912},"せっかく時間ができるので",[13,7914,7915],{},"リモートの大きな利点としては通勤、出社準備（着替えや化粧など）がなくなりその浮いた時間ができることです。通勤していた時は準備を含めて1日のうち90分を使用していました。この通勤と準備による消費時間はバカにできません。",[13,7917,7918],{},"また人・会社によりますが無駄な飲み会や誘いが減ることによって自分に使える時間が増えるというメリットもあります。であればこの余った時間を運動、勉強、家族との時間、個人開発などに使用すると結構有意義な生活を送ることができるようになります。",[13,7920,7921,7922,7926],{},"私の場合も",[17,7923,7925],{"href":5342,"rel":7924},[21],"RouteShareという個人開発のサイト","を開発することができました。正直リモートのおかげで半年程度で作成できた感じはあります。",[36,7928,7929],{"id":7929},"さいごに",[13,7931,7932],{},"記事は以上となりますがこれからリモートを始めようとする方の参考になればと思います。もしほかにTipsなどがあればぜひ紹介してください。",{"title":408,"searchDepth":469,"depth":469,"links":7934},[7935,7946,7952],{"id":7663,"depth":463,"text":7663,"children":7936},[7937,7940,7941],{"id":7706,"depth":469,"text":7706,"children":7938},[7939],{"id":7718,"depth":475,"text":7718},{"id":7764,"depth":469,"text":7764},{"id":7788,"depth":469,"text":7789,"children":7942},[7943,7944,7945],{"id":7795,"depth":475,"text":7795},{"id":7807,"depth":475,"text":7808},{"id":7820,"depth":475,"text":7677},{"id":7855,"depth":463,"text":7855,"children":7947},[7948,7949,7950,7951],{"id":7861,"depth":469,"text":7862},{"id":7871,"depth":469,"text":7871},{"id":7880,"depth":469,"text":7880},{"id":7912,"depth":469,"text":7912},{"id":7929,"depth":463,"text":7929},[5320],"2023-08-27","プログラム業務で必要なリモートの備品など",{},"\u002Farticles\u002Fremote-work-tips",{"title":7652,"description":7955},"articles\u002Fremote-work-tips",[],"P5HWK9NpWKTTKKmcJYeirjb7d7U8MxoKkErRl9t50gg",{"id":7963,"title":7964,"body":7965,"category":8138,"createdAt":8139,"description":8140,"extension":2795,"index":2796,"meta":8141,"navigation":690,"path":8142,"publish":690,"seo":8143,"series":2796,"seriesTitle":2796,"stem":8144,"tag":8145,"thumbnail":8146,"updatedAt":2796,"__hash__":8147},"articles\u002Farticles\u002Fnever-run-app-on-upgrade.md","ライブラリアップデート時はアプリケーションを実行させるな！",{"type":10,"value":7966,"toc":8129},[7967,7970,7973,7984,7987,7990,7993,7996,8020,8023,8026,8029,8032,8035,8038,8041,8044,8051,8058,8065,8068,8077,8080,8083,8086,8090,8093,8104,8107,8110,8114,8117,8120,8123,8126],[13,7968,7969],{},"こんにちはjunです。最近のフレームワークやミドルウェアはバージョンあたりのサポート期限を短くして、頻繁にバージョンを更新することが多いです。マイナーはともかくメジャーバージョンでは変更点が多いので慎重に行う必要があります。",[13,7971,7972],{},"私も保守をしていたLaravelプロジェクトで8→9へのアップグレード作業がありました。マイグレーションガイドに従って他ライブラリのバージョン変更などを行い、検証してデモ環境で問題なかったため本番で実施しました。しかし本番でなぜか失敗してロールバックを2回も行う事態が発生しました。",[13,7974,7975,7976,7979,7980,7983],{},"結論からいうとLaravelの",[294,7977,7978],{},"php artisan down","を実行して",[294,7981,7982],{},"composer update","諸々を実行しましたが、ユーザーのリクエストがその時にも来ていたことによってLaravelが起動。キャッシュなどの影響によってアップデートしたアプリケーションが動かなくなったことが原因と思われます。",[13,7985,7986],{},"この記事では上記事態の詳細な経緯と原因、やらかしたときのロールバック・対策、今後の対策について書こうと思います。Laravelを実例として出しますが、DjangoやRails、expressなど他のフレームワークでのバージョンアップ作業でも参考になると思います。反面教師としてご活用ください。",[36,7988,7989],{"id":7989},"経緯",[13,7991,7992],{},"Laravelプロジェクトの8→9への変更にあたりアップグレードガイドを参考にして作業を行い、テストをパス、そしてデモ環境で問題なくアップデートが出来たため本番環境での実行を進めました。",[13,7994,7995],{},"流れとしては",[116,7997,7998,8003,8006,8014],{},[119,7999,8000,8002],{},[294,8001,7978],{},"を実行してページの表示や全てのリクエストに対して503を出すように変更。",[119,8004,8005],{},"変更したファイルをプル",[119,8007,8008,3398,8010,8013],{},[294,8009,7982],{},[294,8011,8012],{},"php artisan migrate","を行ってライブラリとDBを更新",[119,8015,8016,8019],{},[294,8017,8018],{},"php artisan up","でメンテナンスモードを解除。キャッシュの削除などを行う。",[13,8021,8022],{},"上記のように行いました。4で解除をしてサービスのURLを見てみると...",[108,8024],{":src":8025,":width":262,":center":263},"'never-run-app-on-upgrade\u002F500err.png'",[13,8027,8028],{},"(  ０Д０)",[13,8030,8031],{},"アプリケーション・サーバキャッシュの削除を行いましたがページは変わらず。しかし、php artisan tinkerやlocalhostにcurlした場合は問題なく応答しており原因が不明でした。",[13,8033,8034],{},"しばらく修正を試みて色々行いましたが結局直らず、メンテナンス終了時刻が迫ってきたためロールバックを行いました。",[13,8036,8037],{},"とりあえず元には戻りサービスが利用可能になったので一安心。しかし本番のみ発生するこの事象に頭を悩ませて他の人と相談しました。",[36,8039,8040],{"id":8040},"解決と原因",[13,8042,8043],{},"先方に相談してまた次の日にメンテナンスを実行。相談に乗っかってくれたインフラエンジニアさんがスタンバイしながら、私が上記の操作を行いました。そして今回も発生して一通りログなどをみても特に有効打になりそうな内容が見つかりませんでした。",[13,8045,8046,8047,8050],{},"しかし",[289,8048,8049],{},"なぜか100%登録されているはずのroutingがないというエラーがLaravelログに記録されており","、Laravelそのものが上手く起動していないことがわかりました。またロールバックを行い、今度は別のディレクトリをコピー作成してそこでアップデート作業を行いました。とりあえず本番環境中でマイグレーションを行ってそのファイル一式をローカルなりで解析しようという戦略に変更。",[13,8052,8053,8054,8057],{},"念の為ドキュメントルートの向き先を変更して同じ状態であることを確認すると...",[289,8055,8056],{},"なぜか正常のいつものサービスの画面が表示されました。","。",[13,8059,8060,8061,8064],{},"とりあえず問題なくアップデートは終了しことなきを得ました。しかし原因関しては予測とはなりますが、",[289,8062,8063],{},"php artisan downではcomposer update 中でもLaravel自体（index.php)が実行される。本体のライブラリは色々と更新中にも関わらず、スクリプトが実行されることで変なキャッシュが生成されたことが原因だったのではないか？"," という結論に至りました。",[36,8066,8067],{"id":8067},"対策と考察",[13,8069,8070,8072,8073,8076],{},[294,8071,7978],{},"はあくまでLarabvelが動的に503エラーを生成するだけであり、リクエスト自体はLaravelのindex.php自体は実行されていることが今回の要因と思われます。そのため今回のようなライブラリを更新する際などは",[289,8074,8075],{},"そもそもLaravel自体起動しないようにする","必要がありました。",[13,8078,8079],{},"方法としてはドキュメントルートや（ネットワークの）ルーティングを変更して、変更先にはメンテナンス画面のHTMLを置いておく。そして全てのリクエストに対してそのファイルを見せるように設定することが一番な気がします。",[13,8081,8082],{},"今回はLaravelでしたが、Djangoや他のフレームワークでもメンテナンス時はwebサーバレベルでルーティングを変更して、それらが起動しないようにすることが大切だと思います。",[36,8084,8085],{"id":8085},"やらかす前の対策とやらかした時のロールバック",[53,8087,8089],{"id":8088},"やらかす前に","やらかす前に..",[13,8091,8092],{},"まず本番環境では大小の変更に関わらず、必ずバックアップを行います。この場合Laravelレベルでなく、",[192,8094,8095,8098,8101],{},[119,8096,8097],{},"DBはダンプなりしてバックアップをとっておく。",[119,8099,8100],{},"アップロードファイルもローカルならばバックアップしておく。",[119,8102,8103],{},"git などで戻れる体制を整える。",[13,8105,8106],{},"そしてアップデートの場合、composer.jsonなどの旧バージョンの管理ファイルをバックアップしておくのを忘れないでください。とにかくバックアップさえあればなんとかなることが多いです。",[13,8108,8109],{},"そしてデモ環境などであらかじめアップグレードの予行練習はしましょう。",[53,8111,8113],{"id":8112},"やらかしたら","やらかしたら..",[13,8115,8116],{},"まずは落ち着きます。とにかく落ち着きましょう。私の場合、その後の原因解明zoomでも指が震えたままで結構タイプミスしていました笑。このような場合、同席したインフラエンジニアさんはタバコを一旦吸うそうです。",[13,8118,8119],{},"そして落ち着きの間に連絡の取れるかに一報を必ず入れましょう。焦っているときは人間変な行動をしてしまうことが多く、余計に自体を悪化させることがあります。その時に他に冷静な人がいるだけでもすぐに解決策が見つかったり、次に何をすべきかを考える余裕ができます。",[13,8121,8122],{},"そしてメンテナンス時間内であれば何か手順をミスっていないか、忘れていないかを再度確認をしていきましょう。どうしても時間が間に合わなそうであればロールバックを行い、少なくともサービスが実行できるようにしましょう。",[13,8124,8125],{},"最後にログ取得や詳細な状況を記録して今後の対策に当てましょう。",[13,8127,8128],{},"いやーそれにしても何年経っても本番環境って怖いです。",{"title":408,"searchDepth":469,"depth":469,"links":8130},[8131,8132,8133,8134],{"id":7989,"depth":463,"text":7989},{"id":8040,"depth":463,"text":8040},{"id":8067,"depth":463,"text":8067},{"id":8085,"depth":463,"text":8085,"children":8135},[8136,8137],{"id":8088,"depth":469,"text":8089},{"id":8112,"depth":469,"text":8113},[6556],"2023-08-23","LaravelやDjangoなどのフレームワークで",{},"\u002Farticles\u002Fnever-run-app-on-upgrade",{"title":7964,"description":8140},"articles\u002Fnever-run-app-on-upgrade",[],"never-run-app-on-upgrade\u002F500err.png","6Asz-KAzas2fuGFUpR0qDViaEOuIQy22DGaqgGP5kgo",{"id":8149,"title":8150,"body":8151,"category":8212,"createdAt":8213,"description":8214,"extension":2795,"index":2796,"meta":8215,"navigation":690,"path":8216,"publish":690,"seo":8217,"series":2796,"seriesTitle":2796,"stem":8218,"tag":8219,"thumbnail":8221,"updatedAt":2796,"__hash__":8222},"articles\u002Farticles\u002Fknow-users-background.md","顧客からの要望は必ず背景を聞こう。",{"type":10,"value":8152,"toc":8207},[8153,8156,8159,8164,8167,8170,8173,8176,8179,8182,8185,8188,8191,8194,8201,8204],[36,8154,8155],{"id":8155},"国語の問題",[13,8157,8158],{},"ある日、担当ディレクターとして完成したwebサイトのお客様から以下の様な連絡をもらいました。",[3431,8160,8161],{},[13,8162,8163],{},"サイトにアクセスカウンターを設置したいのですが、可能でしょうか？",[13,8165,8166],{},"実際のメールはもう少しいろいろありましたが、要望は上記の通りでした。",[13,8168,8169],{},"アクセスカウンターというと懐かしいやつです。サイトの下部にあって訪問者数を記録して、キリ番だと嬉しいあれです。しかし開発者的には結構面倒です。ユーザーのリクエスト数をDBなりファイルなりで保存して、都度それを表示しないといけません。",[13,8171,8172],{},"さらにいうとそのサイトは「静的サイト」（正確にはHTMLを出力している）であるため、アクセスカウンターの様な動的な機能の実装が非常に面倒でした。",[13,8174,8175],{},"あれこれ考えていましたが、ひとまず先方に電話を入れて確認をしました。そこで上記のメールではなかった「なぜアクセスカウンターをつけたいのか」という背景について聞いたところ、先方は「サイトの訪問者数を知りたい」と答えました。",[13,8177,8178],{},"この場合、サイトにはGoogle Analyticsが入っているためページごとの細かい閲覧数などを確認できる旨を伝えたところ、先方は満足して今回の要望はクローズしました。",[36,8180,8181],{"id":8181},"ここで気をつけたいこと",[13,8183,8184],{},"このことで気をつけたいことは「顧客から要望をもらった時、その背景や理由については必ず把握しましょう」ということです。先方からもらったメールには「要望＝アクセスカウンターを設置したい」ということのみが書かれており、「なぜアクセスカウンターを設置するに至ったのかの理由」がありませんでした。",[13,8186,8187],{},"また自分はアクセスカウンターと聞いた瞬間、静的サイトなのでどうやって実装しようかと考え込み始めました。（悪い癖）\nしかし先方の真意は「サイトの訪問者数を把握する」という「データとして利用するためのアクセスカウンターの設置」であり「コンテンツとしてのアクセスカウンターの設置」ではありません。しかし自分は過去の経験やITにいる立場上、後者として認識していまい見事にお客様と認識のずれが発生しました。",[13,8189,8190],{},"よくよく見て理由がないことに気づいて、電話で詳細に聞いたため今回は特に問題ありませんでした。",[36,8192,8193],{"id":8193},"顧客は要望しか言わない",[13,8195,8196,8197,8200],{},"お客様から来る要望は上記の様に",[289,8198,8199],{},"背景や理由が抜けていることが多いです。"," しかしこれは、お客様は開発者でないため専門的な知見や知識がないため仕方ありません。または上手く言語化して伝えられないということもあります。ここをうまく読み取るのが開発者やディレクターの仕事です。",[13,8202,8203],{},"実際の業務や要望を実現するにあたり、開発者は顧客からの要望の背景と理由を確認する様にしましょう。素晴らしいお客様は最初から理由をつけて話してくれることもありますが、大体はすっぽ抜けています。理由や背景を確認することで、開発者と顧客の認識のズレをなくすとともにより有効的な解決策や提案に発展することが多いです。",[13,8205,8206],{},"文字通りの内容でなく、きちんと要望や要求に対してはその背景と理由を尋ねて認識を合わせる様にしましょう。",{"title":408,"searchDepth":469,"depth":469,"links":8208},[8209,8210,8211],{"id":8155,"depth":463,"text":8155},{"id":8181,"depth":463,"text":8181},{"id":8193,"depth":463,"text":8193},[2793],"2023-02-01","アクセスカウンターをサイトに設置したいお客様の声",{},"\u002Farticles\u002Fknow-users-background",{"title":8150,"description":8214},"articles\u002Fknow-users-background",[8220],"direction","know-users-background\u002Fthumbnail.png","ijGmli97ihKnMk7TPk-upb6BQKjTQntVDe3CasufJYU",1780987151786]