株式会社メドレーのブログ - TECH PLAY

TECH PLAY

株式会社メドレー

株式会社メドレー の技術ブログ

1411

はじめまして。メドレーのエンジニア熊本です。新卒で入社し今年で 3 年目になりまして、 2019 年度エンジニア新卒の研修 を終えてから早 2 年が経とうとしています。 そんな私ですが去年の 11 月頃から先月までの間、とあるプロジェクトのリーダーを任せてもらっていたので、そのお話をさせていただきます。 はじめに 私は新卒研修を終えてから医療介護求人サイト ジョブメドレー のチームで開発をしていましたが、そのジョブメドレーを支える社内管理システムのリニューアルプロジェクトに初期から携わっていました。 こちらのプロジェクトにつきましては、弊社デザイナーの酒井が デザイナーがデザインツールを使わずに、React を使ってデザインした話 を、弊社エンジニアの山田が GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話 を以前ブログにしていますので、よろしければあわせてご覧ください。 その社内管理システムをどのような流れでリニューアルし、その中で自分の役割がどう変化しどう対応したのかなどについて、次の章からお話ししていきます。 プロジェクトについて リニューアルの背景やシステムの概要については上に紹介した記事でも説明しているため割愛しますが、求職者や求人を掲載する顧客に関する業務を行っているシステムをおよそ 1 年半かけて刷新するという大きなプロジェクトでした。 システムの中でも求職者関連を「Phase1」、顧客関連を「Phase2」として分割し、リニューアルを進めました。 プロジェクト内での自分の役割の変遷 Phase1 の最初期は先輩方がアーキテクチャの設計やスケジューリングをしていました。当時まだ新卒 1 年目で未熟な私でしたが、権限管理のテーブル設計をするタスクをアサインしてもらいました。ここでは詳細を省きますが、初めてのテーブル設計で右も左も分からない状態から責任感を持って何とか形にすることができ、(もちろんリニューアル中に多少の見直しはありましたが)大きな達成感を得たことを覚えています。 各種設計、技術選定、開発の進め方などが大方固まり本格的に開発が始まるわけですが、Phase1 の際は先輩社員がプロジェクトリーダーとして引っ張っていただき、自分は開発メンバーの一員として API の作成などに奮闘していました。 GraphQL といった技術やスケジュールが厳密に引かれたプロジェクトでの開発など初めて経験することも多々ありましたが、先輩方にサポートをいただいたり、同期と切磋琢磨しながら取り組めたおかげで、Phase1 を乗り切ることができました。 さて、ここからが本題になりますが、Phase2 になるとプロジェクトメンバーの入れ替えや私自身の目標設定も重なり、プロジェクトリーダーを任せてもらうことになります。まずはプロジェクトリーダーに任命されてから、どういった仕事をしていたのかご紹介します。 プロジェクトリーダーの仕事 プロジェクトリーダーとして期待されていたことは以下の通りです。 プロジェクト管理 システム設計 開発 チームマネジメント これを更に細分化し、私の実業務と照らし合わせながら並べてみると、多少粒度にばらつきがあるかもしれませんが以下のようなことが挙げられます。 要件定義・画面設計(ディレクターとデザイナー主導で進めつつ、エンジニアも実データや既存ロジックを踏まえた観点を持ち合わせて参加しました) 開発方針の検討 開発タスクへの落とし込み 技術調査・選定 API 設計 工数算出・スケジューリング 実装・レビュー QA(Quality Assurance)テスト リリースマネジメント Phase2 は段階的にリリースを行ったため、その度に 1 から 9 までを繰り返していたような流れになります。また、上記に加え、定例ミーティングでの報告や開発メンバーのタスクマネジメントも随時行っていました。 もちろん苦労したことは多く、全部を挙げようとするとキリがないのですが、その中でもいくつかに絞った上で紹介したいと思います。 苦労と工夫 1. 「そもそも何をやればいいのか」 まず最初に苦労したことは「そもそも何をやればいいのかわからない」ということでした。初めから先ほど挙げたような動きをイメージできていたわけではなく、記事や本を読み漁ったり先輩との 1on1 で質問攻めにしたりと基本的な知識を叩き込むわけですが、実際にとった最初の動きとしては「できる部分を見つけてやっていく」ということだったと思います。 自分がリーダーに任命された時点でのプロジェクトの状況としては要件定義や画面設計が進んでいる最中でしたが、これらがまとまるのを待つのではなく「全部決まらないとやれないこと」と「現時点でやれること」を切り分けて動きました。こうしたところから少しずつリズムを作り、最終的に先ほど列挙したような一通りのことがイメージ・実行できるようになったのだと思います。 2. 工数見積もり 一般的に工数見積もりに関する記事は世の中に多く存在しますが、私の場合は工数見積もりの方法がわからなかったというよりも、「どういう思想で見積もったのか、どういう選択肢があるのか」を曖昧にしていたことが当初の問題でした。 初めて見積もった時は単に開発タスクを積み上げた工数を報告して満足してしまいましたが、様々な方のフィードバックを受けプロダクト価値を高めるためにどういう動きができるのかを考える必要があったことを痛感しました。単純に工数を積み上げる場合や事業的な都合を踏まえてミニマムで開発する場合など、いくつかの選択肢をそれぞれの軸で考える必要があったことを学びました(この時期は夜な夜な夢の中で工数見積もりをしていたのも今ではいい思い出です)。 3. 意思決定 これはいつになっても正解が存在する類のものではないのですが、特に意思決定には苦労しました。意思決定といっても開発方針から技術選定まで様々な粒度のものがありますが、特に最初から苦労したのは技術的な決定でした。 それまで先輩に頼ることの多かった私がプロジェクトリーダーになった直後から何もかもできるようになるわけではないことは明々白々ですが、「自分が決めないと」と焦ってしまっていた時期もあったと思います。 そこで一度立ち止まって意識したことは、「何ができて何ができないのかを他者に明示する」ことでした。はっきりと自分に足りていないことを他者に伝えることで、周りもサポートしやすくなると思いますし、自分自身なにがやれることなのか明確になるので単純なことですが効果的であったと思います。他にも開発メンバーの提案で、インセプションデッキを取り入れてみたことも効果的でした。 また、意思決定とは文脈が少し変わってきますが、モブプロやペアプロを実施してチーム力を高め属人化をなくしつつ開発効率を向上させる取り組みも、時間が経てば経つほど効果を実感できて良かったと思います。このようにアジャイル開発の手法からチームにフィットする手法をいくつか取り入れることもできました。 プロジェクトを通して成長したこと これまで小出しで色々とお話しさせていただきましたが、自分が特に成長したと感じていることをまとめさせていただきます。 一通りの経験を通して得られたリード力 「API 設計だけ」ではなく一通り全てを任せていただいたことはとても大きな経験になりました。初めて個人ではなくチーム・プロジェクト全体として効率が良くなる動きを考える経験もできたと思います。 技術力 もちろん実装を通じて得た技術は数えきれないほどありますが、その中でも特に責任を持って他者のコードをレビューしたり、自分が書くコードの影響範囲やスコープを意識し続けたことが大きな糧になっている気がします。 リスク管理力 スケジュール遅延のリスク、方向性がずれてしまうリスク、技術的なリスク、様々ありますがこれらのリスクヘッジを考える力がプロジェクトリーダーには必要です。 リスク管理において「先読みが大切」とよく言われますが、私の場合はある先輩社員から「常に 2 週間先を見据えておけ」という具体的な日数のアドバイスをいただきました。具体的にすることであらゆることが想像しやすくなりましたし、それを 1 年以上毎日意識し実行し続けたことが、プロジェクトをやり切ることができた要因にもなっていると思います。もちろんこの言葉は家宝にしようと思っています。 価値に対する視野 何よりも「プロダクトのユーザーに価値を提供すること」の意味を理解しました。ここまでに書いてきたようなスケジュール管理やリスク管理などは、あくまでプロジェクトを遂行する上で必要な仕事の一つでしかないはずです。プロジェクトを通してシステムを使っている社員、更にはその先の顧客・求職者へ如何に価値を提供できるか考えるべきですが、一時期は「どうやるのか・なにをやるのか」というプロジェクト自体を完遂させることしか考えられていない時期もありました。 視野が狭くなっていたことに周りからの指摘で気づくことができ、それ以降は「そもそも本当にこの機能はいるのか」などユーザーの立場からの観点も徐々に身に付けることができました。これがきっかけとなり、周りとも頻繁に「なぜやるのか」を議論できるようになったと思います。新卒 1 年目で口酸っぱく言われていた「目的意識」をようやく腹落ちさせ体現することができました。 最後に 最後となりますが、プロジェクトリーダーについて語ってきた私ですが、入社するまでは Web 開発未経験でして、メドレーでの成長を非常に実感しています。そんなメドレーではエンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集していますので、少しでもご興味をお持ちいただけた方は、是非お気軽にお話しさせていただければと思います! ここまでお付き合いいただき、ありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめまして。メドレーのエンジニア熊本です。新卒で入社し今年で 3 年目になりまして、 2019 年度エンジニア新卒の研修 を終えてから早 2 年が経とうとしています。 そんな私ですが去年の 11 月頃から先月までの間、とあるプロジェクトのリーダーを任せてもらっていたので、そのお話をさせていただきます。 はじめに 私は新卒研修を終えてから医療介護求人サイト ジョブメドレー のチームで開発をしていましたが、そのジョブメドレーを支える社内管理システムのリニューアルプロジェクトに初期から携わっていました。 こちらのプロジェクトにつきましては、弊社デザイナーの酒井が デザイナーがデザインツールを使わずに、React を使ってデザインした話 を、弊社エンジニアの山田が GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話 を以前ブログにしていますので、よろしければあわせてご覧ください。 その社内管理システムをどのような流れでリニューアルし、その中で自分の役割がどう変化しどう対応したのかなどについて、次の章からお話ししていきます。 プロジェクトについて リニューアルの背景やシステムの概要については上に紹介した記事でも説明しているため割愛しますが、求職者や求人を掲載する顧客に関する業務を行っているシステムをおよそ 1 年半かけて刷新するという大きなプロジェクトでした。 システムの中でも求職者関連を「Phase1」、顧客関連を「Phase2」として分割し、リニューアルを進めました。 プロジェクト内での自分の役割の変遷 Phase1 の最初期は先輩方がアーキテクチャの設計やスケジューリングをしていました。当時まだ新卒 1 年目で未熟な私でしたが、権限管理のテーブル設計をするタスクをアサインしてもらいました。ここでは詳細を省きますが、初めてのテーブル設計で右も左も分からない状態から責任感を持って何とか形にすることができ、(もちろんリニューアル中に多少の見直しはありましたが)大きな達成感を得たことを覚えています。 各種設計、技術選定、開発の進め方などが大方固まり本格的に開発が始まるわけですが、Phase1 の際は先輩社員がプロジェクトリーダーとして引っ張っていただき、自分は開発メンバーの一員として API の作成などに奮闘していました。 GraphQL といった技術やスケジュールが厳密に引かれたプロジェクトでの開発など初めて経験することも多々ありましたが、先輩方にサポートをいただいたり、同期と切磋琢磨しながら取り組めたおかげで、Phase1 を乗り切ることができました。 さて、ここからが本題になりますが、Phase2 になるとプロジェクトメンバーの入れ替えや私自身の目標設定も重なり、プロジェクトリーダーを任せてもらうことになります。まずはプロジェクトリーダーに任命されてから、どういった仕事をしていたのかご紹介します。 プロジェクトリーダーの仕事 プロジェクトリーダーとして期待されていたことは以下の通りです。 プロジェクト管理 システム設計 開発 チームマネジメント これを更に細分化し、私の実業務と照らし合わせながら並べてみると、多少粒度にばらつきがあるかもしれませんが以下のようなことが挙げられます。 要件定義・画面設計(ディレクターとデザイナー主導で進めつつ、エンジニアも実データや既存ロジックを踏まえた観点を持ち合わせて参加しました) 開発方針の検討 開発タスクへの落とし込み 技術調査・選定 API 設計 工数算出・スケジューリング 実装・レビュー QA(Quality Assurance)テスト リリースマネジメント Phase2 は段階的にリリースを行ったため、その度に 1 から 9 までを繰り返していたような流れになります。また、上記に加え、定例ミーティングでの報告や開発メンバーのタスクマネジメントも随時行っていました。 もちろん苦労したことは多く、全部を挙げようとするとキリがないのですが、その中でもいくつかに絞った上で紹介したいと思います。 苦労と工夫 1. 「そもそも何をやればいいのか」 まず最初に苦労したことは「そもそも何をやればいいのかわからない」ということでした。初めから先ほど挙げたような動きをイメージできていたわけではなく、記事や本を読み漁ったり先輩との 1on1 で質問攻めにしたりと基本的な知識を叩き込むわけですが、実際にとった最初の動きとしては「できる部分を見つけてやっていく」ということだったと思います。 自分がリーダーに任命された時点でのプロジェクトの状況としては要件定義や画面設計が進んでいる最中でしたが、これらがまとまるのを待つのではなく「全部決まらないとやれないこと」と「現時点でやれること」を切り分けて動きました。こうしたところから少しずつリズムを作り、最終的に先ほど列挙したような一通りのことがイメージ・実行できるようになったのだと思います。 2. 工数見積もり 一般的に工数見積もりに関する記事は世の中に多く存在しますが、私の場合は工数見積もりの方法がわからなかったというよりも、「どういう思想で見積もったのか、どういう選択肢があるのか」を曖昧にしていたことが当初の問題でした。 初めて見積もった時は単に開発タスクを積み上げた工数を報告して満足してしまいましたが、様々な方のフィードバックを受けプロダクト価値を高めるためにどういう動きができるのかを考える必要があったことを痛感しました。単純に工数を積み上げる場合や事業的な都合を踏まえてミニマムで開発する場合など、いくつかの選択肢をそれぞれの軸で考える必要があったことを学びました(この時期は夜な夜な夢の中で工数見積もりをしていたのも今ではいい思い出です)。 3. 意思決定 これはいつになっても正解が存在する類のものではないのですが、特に意思決定には苦労しました。意思決定といっても開発方針から技術選定まで様々な粒度のものがありますが、特に最初から苦労したのは技術的な決定でした。 それまで先輩に頼ることの多かった私がプロジェクトリーダーになった直後から何もかもできるようになるわけではないことは明々白々ですが、「自分が決めないと」と焦ってしまっていた時期もあったと思います。 そこで一度立ち止まって意識したことは、「何ができて何ができないのかを他者に明示する」ことでした。はっきりと自分に足りていないことを他者に伝えることで、周りもサポートしやすくなると思いますし、自分自身なにがやれることなのか明確になるので単純なことですが効果的であったと思います。他にも開発メンバーの提案で、インセプションデッキを取り入れてみたことも効果的でした。 また、意思決定とは文脈が少し変わってきますが、モブプロやペアプロを実施してチーム力を高め属人化をなくしつつ開発効率を向上させる取り組みも、時間が経てば経つほど効果を実感できて良かったと思います。このようにアジャイル開発の手法からチームにフィットする手法をいくつか取り入れることもできました。 プロジェクトを通して成長したこと これまで小出しで色々とお話しさせていただきましたが、自分が特に成長したと感じていることをまとめさせていただきます。 一通りの経験を通して得られたリード力 「API 設計だけ」ではなく一通り全てを任せていただいたことはとても大きな経験になりました。初めて個人ではなくチーム・プロジェクト全体として効率が良くなる動きを考える経験もできたと思います。 技術力 もちろん実装を通じて得た技術は数えきれないほどありますが、その中でも特に責任を持って他者のコードをレビューしたり、自分が書くコードの影響範囲やスコープを意識し続けたことが大きな糧になっている気がします。 リスク管理力 スケジュール遅延のリスク、方向性がずれてしまうリスク、技術的なリスク、様々ありますがこれらのリスクヘッジを考える力がプロジェクトリーダーには必要です。 リスク管理において「先読みが大切」とよく言われますが、私の場合はある先輩社員から「常に 2 週間先を見据えておけ」という具体的な日数のアドバイスをいただきました。具体的にすることであらゆることが想像しやすくなりましたし、それを 1 年以上毎日意識し実行し続けたことが、プロジェクトをやり切ることができた要因にもなっていると思います。もちろんこの言葉は家宝にしようと思っています。 価値に対する視野 何よりも「プロダクトのユーザーに価値を提供すること」の意味を理解しました。ここまでに書いてきたようなスケジュール管理やリスク管理などは、あくまでプロジェクトを遂行する上で必要な仕事の一つでしかないはずです。プロジェクトを通してシステムを使っている社員、更にはその先の顧客・求職者へ如何に価値を提供できるか考えるべきですが、一時期は「どうやるのか・なにをやるのか」というプロジェクト自体を完遂させることしか考えられていない時期もありました。 視野が狭くなっていたことに周りからの指摘で気づくことができ、それ以降は「そもそも本当にこの機能はいるのか」などユーザーの立場からの観点も徐々に身に付けることができました。これがきっかけとなり、周りとも頻繁に「なぜやるのか」を議論できるようになったと思います。新卒 1 年目で口酸っぱく言われていた「目的意識」をようやく腹落ちさせ体現することができました。 最後に 最後となりますが、プロジェクトリーダーについて語ってきた私ですが、入社するまでは Web 開発未経験でして、メドレーでの成長を非常に実感しています。そんなメドレーではエンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集していますので、少しでもご興味をお持ちいただけた方は、是非お気軽にお話しさせていただければと思います! ここまでお付き合いいただき、ありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめまして。メドレーのエンジニア熊本です。新卒で入社し今年で 3 年目になりまして、 2019 年度エンジニア新卒の研修 を終えてから早 2 年が経とうとしています。 そんな私ですが去年の 11 月頃から先月までの間、とあるプロジェクトのリーダーを任せてもらっていたので、そのお話をさせていただきます。 はじめに 私は新卒研修を終えてから医療介護求人サイト ジョブメドレー のチームで開発をしていましたが、そのジョブメドレーを支える社内管理システムのリニューアルプロジェクトに初期から携わっていました。 こちらのプロジェクトにつきましては、弊社デザイナーの酒井が デザイナーがデザインツールを使わずに、React を使ってデザインした話 を、弊社エンジニアの山田が GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話 を以前ブログにしていますので、よろしければあわせてご覧ください。 その社内管理システムをどのような流れでリニューアルし、その中で自分の役割がどう変化しどう対応したのかなどについて、次の章からお話ししていきます。 プロジェクトについて リニューアルの背景やシステムの概要については上に紹介した記事でも説明しているため割愛しますが、求職者や求人を掲載する顧客に関する業務を行っているシステムをおよそ 1 年半かけて刷新するという大きなプロジェクトでした。 システムの中でも求職者関連を「Phase1」、顧客関連を「Phase2」として分割し、リニューアルを進めました。 プロジェクト内での自分の役割の変遷 Phase1 の最初期は先輩方がアーキテクチャの設計やスケジューリングをしていました。当時まだ新卒 1 年目で未熟な私でしたが、権限管理のテーブル設計をするタスクをアサインしてもらいました。ここでは詳細を省きますが、初めてのテーブル設計で右も左も分からない状態から責任感を持って何とか形にすることができ、(もちろんリニューアル中に多少の見直しはありましたが)大きな達成感を得たことを覚えています。 各種設計、技術選定、開発の進め方などが大方固まり本格的に開発が始まるわけですが、Phase1 の際は先輩社員がプロジェクトリーダーとして引っ張っていただき、自分は開発メンバーの一員として API の作成などに奮闘していました。 GraphQL といった技術やスケジュールが厳密に引かれたプロジェクトでの開発など初めて経験することも多々ありましたが、先輩方にサポートをいただいたり、同期と切磋琢磨しながら取り組めたおかげで、Phase1 を乗り切ることができました。 さて、ここからが本題になりますが、Phase2 になるとプロジェクトメンバーの入れ替えや私自身の目標設定も重なり、プロジェクトリーダーを任せてもらうことになります。まずはプロジェクトリーダーに任命されてから、どういった仕事をしていたのかご紹介します。 プロジェクトリーダーの仕事 プロジェクトリーダーとして期待されていたことは以下の通りです。 プロジェクト管理 システム設計 開発 チームマネジメント これを更に細分化し、私の実業務と照らし合わせながら並べてみると、多少粒度にばらつきがあるかもしれませんが以下のようなことが挙げられます。 要件定義・画面設計(ディレクターとデザイナー主導で進めつつ、エンジニアも実データや既存ロジックを踏まえた観点を持ち合わせて参加しました) 開発方針の検討 開発タスクへの落とし込み 技術調査・選定 API 設計 工数算出・スケジューリング 実装・レビュー QA(Quality Assurance)テスト リリースマネジメント Phase2 は段階的にリリースを行ったため、その度に 1 から 9 までを繰り返していたような流れになります。また、上記に加え、定例ミーティングでの報告や開発メンバーのタスクマネジメントも随時行っていました。 もちろん苦労したことは多く、全部を挙げようとするとキリがないのですが、その中でもいくつかに絞った上で紹介したいと思います。 苦労と工夫 1. 「そもそも何をやればいいのか」 まず最初に苦労したことは「そもそも何をやればいいのかわからない」ということでした。初めから先ほど挙げたような動きをイメージできていたわけではなく、記事や本を読み漁ったり先輩との 1on1 で質問攻めにしたりと基本的な知識を叩き込むわけですが、実際にとった最初の動きとしては「できる部分を見つけてやっていく」ということだったと思います。 自分がリーダーに任命された時点でのプロジェクトの状況としては要件定義や画面設計が進んでいる最中でしたが、これらがまとまるのを待つのではなく「全部決まらないとやれないこと」と「現時点でやれること」を切り分けて動きました。こうしたところから少しずつリズムを作り、最終的に先ほど列挙したような一通りのことがイメージ・実行できるようになったのだと思います。 2. 工数見積もり 一般的に工数見積もりに関する記事は世の中に多く存在しますが、私の場合は工数見積もりの方法がわからなかったというよりも、「どういう思想で見積もったのか、どういう選択肢があるのか」を曖昧にしていたことが当初の問題でした。 初めて見積もった時は単に開発タスクを積み上げた工数を報告して満足してしまいましたが、様々な方のフィードバックを受けプロダクト価値を高めるためにどういう動きができるのかを考える必要があったことを痛感しました。単純に工数を積み上げる場合や事業的な都合を踏まえてミニマムで開発する場合など、いくつかの選択肢をそれぞれの軸で考える必要があったことを学びました(この時期は夜な夜な夢の中で工数見積もりをしていたのも今ではいい思い出です)。 3. 意思決定 これはいつになっても正解が存在する類のものではないのですが、特に意思決定には苦労しました。意思決定といっても開発方針から技術選定まで様々な粒度のものがありますが、特に最初から苦労したのは技術的な決定でした。 それまで先輩に頼ることの多かった私がプロジェクトリーダーになった直後から何もかもできるようになるわけではないことは明々白々ですが、「自分が決めないと」と焦ってしまっていた時期もあったと思います。 そこで一度立ち止まって意識したことは、「何ができて何ができないのかを他者に明示する」ことでした。はっきりと自分に足りていないことを他者に伝えることで、周りもサポートしやすくなると思いますし、自分自身なにがやれることなのか明確になるので単純なことですが効果的であったと思います。他にも開発メンバーの提案で、インセプションデッキを取り入れてみたことも効果的でした。 また、意思決定とは文脈が少し変わってきますが、モブプロやペアプロを実施してチーム力を高め属人化をなくしつつ開発効率を向上させる取り組みも、時間が経てば経つほど効果を実感できて良かったと思います。このようにアジャイル開発の手法からチームにフィットする手法をいくつか取り入れることもできました。 プロジェクトを通して成長したこと これまで小出しで色々とお話しさせていただきましたが、自分が特に成長したと感じていることをまとめさせていただきます。 一通りの経験を通して得られたリード力 「API 設計だけ」ではなく一通り全てを任せていただいたことはとても大きな経験になりました。初めて個人ではなくチーム・プロジェクト全体として効率が良くなる動きを考える経験もできたと思います。 技術力 もちろん実装を通じて得た技術は数えきれないほどありますが、その中でも特に責任を持って他者のコードをレビューしたり、自分が書くコードの影響範囲やスコープを意識し続けたことが大きな糧になっている気がします。 リスク管理力 スケジュール遅延のリスク、方向性がずれてしまうリスク、技術的なリスク、様々ありますがこれらのリスクヘッジを考える力がプロジェクトリーダーには必要です。 リスク管理において「先読みが大切」とよく言われますが、私の場合はある先輩社員から「常に 2 週間先を見据えておけ」という具体的な日数のアドバイスをいただきました。具体的にすることであらゆることが想像しやすくなりましたし、それを 1 年以上毎日意識し実行し続けたことが、プロジェクトをやり切ることができた要因にもなっていると思います。もちろんこの言葉は家宝にしようと思っています。 価値に対する視野 何よりも「プロダクトのユーザーに価値を提供すること」の意味を理解しました。ここまでに書いてきたようなスケジュール管理やリスク管理などは、あくまでプロジェクトを遂行する上で必要な仕事の一つでしかないはずです。プロジェクトを通してシステムを使っている社員、更にはその先の顧客・求職者へ如何に価値を提供できるか考えるべきですが、一時期は「どうやるのか・なにをやるのか」というプロジェクト自体を完遂させることしか考えられていない時期もありました。 視野が狭くなっていたことに周りからの指摘で気づくことができ、それ以降は「そもそも本当にこの機能はいるのか」などユーザーの立場からの観点も徐々に身に付けることができました。これがきっかけとなり、周りとも頻繁に「なぜやるのか」を議論できるようになったと思います。新卒 1 年目で口酸っぱく言われていた「目的意識」をようやく腹落ちさせ体現することができました。 最後に 最後となりますが、プロジェクトリーダーについて語ってきた私ですが、入社するまでは Web 開発未経験でして、メドレーでの成長を非常に実感しています。そんなメドレーではエンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集していますので、少しでもご興味をお持ちいただけた方は、是非お気軽にお話しさせていただければと思います! ここまでお付き合いいただき、ありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめまして。メドレーのエンジニア熊本です。新卒で入社し今年で 3 年目になりまして、 2019 年度エンジニア新卒の研修 を終えてから早 2 年が経とうとしています。 そんな私ですが去年の 11 月頃から先月までの間、とあるプロジェクトのリーダーを任せてもらっていたので、そのお話をさせていただきます。 はじめに 私は新卒研修を終えてから医療介護求人サイト ジョブメドレー のチームで開発をしていましたが、そのジョブメドレーを支える社内管理システムのリニューアルプロジェクトに初期から携わっていました。 こちらのプロジェクトにつきましては、弊社デザイナーの酒井が デザイナーがデザインツールを使わずに、React を使ってデザインした話 を、弊社エンジニアの山田が GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話 を以前ブログにしていますので、よろしければあわせてご覧ください。 その社内管理システムをどのような流れでリニューアルし、その中で自分の役割がどう変化しどう対応したのかなどについて、次の章からお話ししていきます。 プロジェクトについて リニューアルの背景やシステムの概要については上に紹介した記事でも説明しているため割愛しますが、求職者や求人を掲載する顧客に関する業務を行っているシステムをおよそ 1 年半かけて刷新するという大きなプロジェクトでした。 システムの中でも求職者関連を「Phase1」、顧客関連を「Phase2」として分割し、リニューアルを進めました。 プロジェクト内での自分の役割の変遷 Phase1 の最初期は先輩方がアーキテクチャの設計やスケジューリングをしていました。当時まだ新卒 1 年目で未熟な私でしたが、権限管理のテーブル設計をするタスクをアサインしてもらいました。ここでは詳細を省きますが、初めてのテーブル設計で右も左も分からない状態から責任感を持って何とか形にすることができ、(もちろんリニューアル中に多少の見直しはありましたが)大きな達成感を得たことを覚えています。 各種設計、技術選定、開発の進め方などが大方固まり本格的に開発が始まるわけですが、Phase1 の際は先輩社員がプロジェクトリーダーとして引っ張っていただき、自分は開発メンバーの一員として API の作成などに奮闘していました。 GraphQL といった技術やスケジュールが厳密に引かれたプロジェクトでの開発など初めて経験することも多々ありましたが、先輩方にサポートをいただいたり、同期と切磋琢磨しながら取り組めたおかげで、Phase1 を乗り切ることができました。 さて、ここからが本題になりますが、Phase2 になるとプロジェクトメンバーの入れ替えや私自身の目標設定も重なり、プロジェクトリーダーを任せてもらうことになります。まずはプロジェクトリーダーに任命されてから、どういった仕事をしていたのかご紹介します。 プロジェクトリーダーの仕事 プロジェクトリーダーとして期待されていたことは以下の通りです。 プロジェクト管理 システム設計 開発 チームマネジメント これを更に細分化し、私の実業務と照らし合わせながら並べてみると、多少粒度にばらつきがあるかもしれませんが以下のようなことが挙げられます。 要件定義・画面設計(ディレクターとデザイナー主導で進めつつ、エンジニアも実データや既存ロジックを踏まえた観点を持ち合わせて参加しました) 開発方針の検討 開発タスクへの落とし込み 技術調査・選定 API 設計 工数算出・スケジューリング 実装・レビュー QA(Quality Assurance)テスト リリースマネジメント Phase2 は段階的にリリースを行ったため、その度に 1 から 9 までを繰り返していたような流れになります。また、上記に加え、定例ミーティングでの報告や開発メンバーのタスクマネジメントも随時行っていました。 もちろん苦労したことは多く、全部を挙げようとするとキリがないのですが、その中でもいくつかに絞った上で紹介したいと思います。 苦労と工夫 1. 「そもそも何をやればいいのか」 まず最初に苦労したことは「そもそも何をやればいいのかわからない」ということでした。初めから先ほど挙げたような動きをイメージできていたわけではなく、記事や本を読み漁ったり先輩との 1on1 で質問攻めにしたりと基本的な知識を叩き込むわけですが、実際にとった最初の動きとしては「できる部分を見つけてやっていく」ということだったと思います。 自分がリーダーに任命された時点でのプロジェクトの状況としては要件定義や画面設計が進んでいる最中でしたが、これらがまとまるのを待つのではなく「全部決まらないとやれないこと」と「現時点でやれること」を切り分けて動きました。こうしたところから少しずつリズムを作り、最終的に先ほど列挙したような一通りのことがイメージ・実行できるようになったのだと思います。 2. 工数見積もり 一般的に工数見積もりに関する記事は世の中に多く存在しますが、私の場合は工数見積もりの方法がわからなかったというよりも、「どういう思想で見積もったのか、どういう選択肢があるのか」を曖昧にしていたことが当初の問題でした。 初めて見積もった時は単に開発タスクを積み上げた工数を報告して満足してしまいましたが、様々な方のフィードバックを受けプロダクト価値を高めるためにどういう動きができるのかを考える必要があったことを痛感しました。単純に工数を積み上げる場合や事業的な都合を踏まえてミニマムで開発する場合など、いくつかの選択肢をそれぞれの軸で考える必要があったことを学びました(この時期は夜な夜な夢の中で工数見積もりをしていたのも今ではいい思い出です)。 3. 意思決定 これはいつになっても正解が存在する類のものではないのですが、特に意思決定には苦労しました。意思決定といっても開発方針から技術選定まで様々な粒度のものがありますが、特に最初から苦労したのは技術的な決定でした。 それまで先輩に頼ることの多かった私がプロジェクトリーダーになった直後から何もかもできるようになるわけではないことは明々白々ですが、「自分が決めないと」と焦ってしまっていた時期もあったと思います。 そこで一度立ち止まって意識したことは、「何ができて何ができないのかを他者に明示する」ことでした。はっきりと自分に足りていないことを他者に伝えることで、周りもサポートしやすくなると思いますし、自分自身なにがやれることなのか明確になるので単純なことですが効果的であったと思います。他にも開発メンバーの提案で、インセプションデッキを取り入れてみたことも効果的でした。 また、意思決定とは文脈が少し変わってきますが、モブプロやペアプロを実施してチーム力を高め属人化をなくしつつ開発効率を向上させる取り組みも、時間が経てば経つほど効果を実感できて良かったと思います。このようにアジャイル開発の手法からチームにフィットする手法をいくつか取り入れることもできました。 プロジェクトを通して成長したこと これまで小出しで色々とお話しさせていただきましたが、自分が特に成長したと感じていることをまとめさせていただきます。 一通りの経験を通して得られたリード力 「API 設計だけ」ではなく一通り全てを任せていただいたことはとても大きな経験になりました。初めて個人ではなくチーム・プロジェクト全体として効率が良くなる動きを考える経験もできたと思います。 技術力 もちろん実装を通じて得た技術は数えきれないほどありますが、その中でも特に責任を持って他者のコードをレビューしたり、自分が書くコードの影響範囲やスコープを意識し続けたことが大きな糧になっている気がします。 リスク管理力 スケジュール遅延のリスク、方向性がずれてしまうリスク、技術的なリスク、様々ありますがこれらのリスクヘッジを考える力がプロジェクトリーダーには必要です。 リスク管理において「先読みが大切」とよく言われますが、私の場合はある先輩社員から「常に 2 週間先を見据えておけ」という具体的な日数のアドバイスをいただきました。具体的にすることであらゆることが想像しやすくなりましたし、それを 1 年以上毎日意識し実行し続けたことが、プロジェクトをやり切ることができた要因にもなっていると思います。もちろんこの言葉は家宝にしようと思っています。 価値に対する視野 何よりも「プロダクトのユーザーに価値を提供すること」の意味を理解しました。ここまでに書いてきたようなスケジュール管理やリスク管理などは、あくまでプロジェクトを遂行する上で必要な仕事の一つでしかないはずです。プロジェクトを通してシステムを使っている社員、更にはその先の顧客・求職者へ如何に価値を提供できるか考えるべきですが、一時期は「どうやるのか・なにをやるのか」というプロジェクト自体を完遂させることしか考えられていない時期もありました。 視野が狭くなっていたことに周りからの指摘で気づくことができ、それ以降は「そもそも本当にこの機能はいるのか」などユーザーの立場からの観点も徐々に身に付けることができました。これがきっかけとなり、周りとも頻繁に「なぜやるのか」を議論できるようになったと思います。新卒 1 年目で口酸っぱく言われていた「目的意識」をようやく腹落ちさせ体現することができました。 最後に 最後となりますが、プロジェクトリーダーについて語ってきた私ですが、入社するまでは Web 開発未経験でして、メドレーでの成長を非常に実感しています。そんなメドレーではエンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集していますので、少しでもご興味をお持ちいただけた方は、是非お気軽にお話しさせていただければと思います! ここまでお付き合いいただき、ありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめまして。メドレーのエンジニア熊本です。新卒で入社し今年で 3 年目になりまして、 2019 年度エンジニア新卒の研修 を終えてから早 2 年が経とうとしています。 そんな私ですが去年の 11 月頃から先月までの間、とあるプロジェクトのリーダーを任せてもらっていたので、そのお話をさせていただきます。 はじめに 私は新卒研修を終えてから医療介護求人サイト ジョブメドレー のチームで開発をしていましたが、そのジョブメドレーを支える社内管理システムのリニューアルプロジェクトに初期から携わっていました。 こちらのプロジェクトにつきましては、弊社デザイナーの酒井が デザイナーがデザインツールを使わずに、React を使ってデザインした話 を、弊社エンジニアの山田が GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話 を以前ブログにしていますので、よろしければあわせてご覧ください。 その社内管理システムをどのような流れでリニューアルし、その中で自分の役割がどう変化しどう対応したのかなどについて、次の章からお話ししていきます。 プロジェクトについて リニューアルの背景やシステムの概要については上に紹介した記事でも説明しているため割愛しますが、求職者や求人を掲載する顧客に関する業務を行っているシステムをおよそ 1 年半かけて刷新するという大きなプロジェクトでした。 システムの中でも求職者関連を「Phase1」、顧客関連を「Phase2」として分割し、リニューアルを進めました。 プロジェクト内での自分の役割の変遷 Phase1 の最初期は先輩方がアーキテクチャの設計やスケジューリングをしていました。当時まだ新卒 1 年目で未熟な私でしたが、権限管理のテーブル設計をするタスクをアサインしてもらいました。ここでは詳細を省きますが、初めてのテーブル設計で右も左も分からない状態から責任感を持って何とか形にすることができ、(もちろんリニューアル中に多少の見直しはありましたが)大きな達成感を得たことを覚えています。 各種設計、技術選定、開発の進め方などが大方固まり本格的に開発が始まるわけですが、Phase1 の際は先輩社員がプロジェクトリーダーとして引っ張っていただき、自分は開発メンバーの一員として API の作成などに奮闘していました。 GraphQL といった技術やスケジュールが厳密に引かれたプロジェクトでの開発など初めて経験することも多々ありましたが、先輩方にサポートをいただいたり、同期と切磋琢磨しながら取り組めたおかげで、Phase1 を乗り切ることができました。 さて、ここからが本題になりますが、Phase2 になるとプロジェクトメンバーの入れ替えや私自身の目標設定も重なり、プロジェクトリーダーを任せてもらうことになります。まずはプロジェクトリーダーに任命されてから、どういった仕事をしていたのかご紹介します。 プロジェクトリーダーの仕事 プロジェクトリーダーとして期待されていたことは以下の通りです。 プロジェクト管理 システム設計 開発 チームマネジメント これを更に細分化し、私の実業務と照らし合わせながら並べてみると、多少粒度にばらつきがあるかもしれませんが以下のようなことが挙げられます。 要件定義・画面設計(ディレクターとデザイナー主導で進めつつ、エンジニアも実データや既存ロジックを踏まえた観点を持ち合わせて参加しました) 開発方針の検討 開発タスクへの落とし込み 技術調査・選定 API 設計 工数算出・スケジューリング 実装・レビュー QA(Quality Assurance)テスト リリースマネジメント Phase2 は段階的にリリースを行ったため、その度に 1 から 9 までを繰り返していたような流れになります。また、上記に加え、定例ミーティングでの報告や開発メンバーのタスクマネジメントも随時行っていました。 もちろん苦労したことは多く、全部を挙げようとするとキリがないのですが、その中でもいくつかに絞った上で紹介したいと思います。 苦労と工夫 1. 「そもそも何をやればいいのか」 まず最初に苦労したことは「そもそも何をやればいいのかわからない」ということでした。初めから先ほど挙げたような動きをイメージできていたわけではなく、記事や本を読み漁ったり先輩との 1on1 で質問攻めにしたりと基本的な知識を叩き込むわけですが、実際にとった最初の動きとしては「できる部分を見つけてやっていく」ということだったと思います。 自分がリーダーに任命された時点でのプロジェクトの状況としては要件定義や画面設計が進んでいる最中でしたが、これらがまとまるのを待つのではなく「全部決まらないとやれないこと」と「現時点でやれること」を切り分けて動きました。こうしたところから少しずつリズムを作り、最終的に先ほど列挙したような一通りのことがイメージ・実行できるようになったのだと思います。 2. 工数見積もり 一般的に工数見積もりに関する記事は世の中に多く存在しますが、私の場合は工数見積もりの方法がわからなかったというよりも、「どういう思想で見積もったのか、どういう選択肢があるのか」を曖昧にしていたことが当初の問題でした。 初めて見積もった時は単に開発タスクを積み上げた工数を報告して満足してしまいましたが、様々な方のフィードバックを受けプロダクト価値を高めるためにどういう動きができるのかを考える必要があったことを痛感しました。単純に工数を積み上げる場合や事業的な都合を踏まえてミニマムで開発する場合など、いくつかの選択肢をそれぞれの軸で考える必要があったことを学びました(この時期は夜な夜な夢の中で工数見積もりをしていたのも今ではいい思い出です)。 3. 意思決定 これはいつになっても正解が存在する類のものではないのですが、特に意思決定には苦労しました。意思決定といっても開発方針から技術選定まで様々な粒度のものがありますが、特に最初から苦労したのは技術的な決定でした。 それまで先輩に頼ることの多かった私がプロジェクトリーダーになった直後から何もかもできるようになるわけではないことは明々白々ですが、「自分が決めないと」と焦ってしまっていた時期もあったと思います。 そこで一度立ち止まって意識したことは、「何ができて何ができないのかを他者に明示する」ことでした。はっきりと自分に足りていないことを他者に伝えることで、周りもサポートしやすくなると思いますし、自分自身なにがやれることなのか明確になるので単純なことですが効果的であったと思います。他にも開発メンバーの提案で、インセプションデッキを取り入れてみたことも効果的でした。 また、意思決定とは文脈が少し変わってきますが、モブプロやペアプロを実施してチーム力を高め属人化をなくしつつ開発効率を向上させる取り組みも、時間が経てば経つほど効果を実感できて良かったと思います。このようにアジャイル開発の手法からチームにフィットする手法をいくつか取り入れることもできました。 プロジェクトを通して成長したこと これまで小出しで色々とお話しさせていただきましたが、自分が特に成長したと感じていることをまとめさせていただきます。 一通りの経験を通して得られたリード力 「API 設計だけ」ではなく一通り全てを任せていただいたことはとても大きな経験になりました。初めて個人ではなくチーム・プロジェクト全体として効率が良くなる動きを考える経験もできたと思います。 技術力 もちろん実装を通じて得た技術は数えきれないほどありますが、その中でも特に責任を持って他者のコードをレビューしたり、自分が書くコードの影響範囲やスコープを意識し続けたことが大きな糧になっている気がします。 リスク管理力 スケジュール遅延のリスク、方向性がずれてしまうリスク、技術的なリスク、様々ありますがこれらのリスクヘッジを考える力がプロジェクトリーダーには必要です。 リスク管理において「先読みが大切」とよく言われますが、私の場合はある先輩社員から「常に 2 週間先を見据えておけ」という具体的な日数のアドバイスをいただきました。具体的にすることであらゆることが想像しやすくなりましたし、それを 1 年以上毎日意識し実行し続けたことが、プロジェクトをやり切ることができた要因にもなっていると思います。もちろんこの言葉は家宝にしようと思っています。 価値に対する視野 何よりも「プロダクトのユーザーに価値を提供すること」の意味を理解しました。ここまでに書いてきたようなスケジュール管理やリスク管理などは、あくまでプロジェクトを遂行する上で必要な仕事の一つでしかないはずです。プロジェクトを通してシステムを使っている社員、更にはその先の顧客・求職者へ如何に価値を提供できるか考えるべきですが、一時期は「どうやるのか・なにをやるのか」というプロジェクト自体を完遂させることしか考えられていない時期もありました。 視野が狭くなっていたことに周りからの指摘で気づくことができ、それ以降は「そもそも本当にこの機能はいるのか」などユーザーの立場からの観点も徐々に身に付けることができました。これがきっかけとなり、周りとも頻繁に「なぜやるのか」を議論できるようになったと思います。新卒 1 年目で口酸っぱく言われていた「目的意識」をようやく腹落ちさせ体現することができました。 最後に 最後となりますが、プロジェクトリーダーについて語ってきた私ですが、入社するまでは Web 開発未経験でして、メドレーでの成長を非常に実感しています。そんなメドレーではエンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集していますので、少しでもご興味をお持ちいただけた方は、是非お気軽にお話しさせていただければと思います! ここまでお付き合いいただき、ありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめまして。メドレーのエンジニア熊本です。新卒で入社し今年で 3 年目になりまして、 2019 年度エンジニア新卒の研修 を終えてから早 2 年が経とうとしています。 そんな私ですが去年の 11 月頃から先月までの間、とあるプロジェクトのリーダーを任せてもらっていたので、そのお話をさせていただきます。 はじめに 私は新卒研修を終えてから医療介護求人サイト ジョブメドレー のチームで開発をしていましたが、そのジョブメドレーを支える社内管理システムのリニューアルプロジェクトに初期から携わっていました。 こちらのプロジェクトにつきましては、弊社デザイナーの酒井が デザイナーがデザインツールを使わずに、React を使ってデザインした話 を、弊社エンジニアの山田が GraphQL, TypeScript, React を用いて型安全に社内システムをリニューアルした話 を以前ブログにしていますので、よろしければあわせてご覧ください。 その社内管理システムをどのような流れでリニューアルし、その中で自分の役割がどう変化しどう対応したのかなどについて、次の章からお話ししていきます。 プロジェクトについて リニューアルの背景やシステムの概要については上に紹介した記事でも説明しているため割愛しますが、求職者や求人を掲載する顧客に関する業務を行っているシステムをおよそ 1 年半かけて刷新するという大きなプロジェクトでした。 システムの中でも求職者関連を「Phase1」、顧客関連を「Phase2」として分割し、リニューアルを進めました。 プロジェクト内での自分の役割の変遷 Phase1 の最初期は先輩方がアーキテクチャの設計やスケジューリングをしていました。当時まだ新卒 1 年目で未熟な私でしたが、権限管理のテーブル設計をするタスクをアサインしてもらいました。ここでは詳細を省きますが、初めてのテーブル設計で右も左も分からない状態から責任感を持って何とか形にすることができ、(もちろんリニューアル中に多少の見直しはありましたが)大きな達成感を得たことを覚えています。 各種設計、技術選定、開発の進め方などが大方固まり本格的に開発が始まるわけですが、Phase1 の際は先輩社員がプロジェクトリーダーとして引っ張っていただき、自分は開発メンバーの一員として API の作成などに奮闘していました。 GraphQL といった技術やスケジュールが厳密に引かれたプロジェクトでの開発など初めて経験することも多々ありましたが、先輩方にサポートをいただいたり、同期と切磋琢磨しながら取り組めたおかげで、Phase1 を乗り切ることができました。 さて、ここからが本題になりますが、Phase2 になるとプロジェクトメンバーの入れ替えや私自身の目標設定も重なり、プロジェクトリーダーを任せてもらうことになります。まずはプロジェクトリーダーに任命されてから、どういった仕事をしていたのかご紹介します。 プロジェクトリーダーの仕事 プロジェクトリーダーとして期待されていたことは以下の通りです。 プロジェクト管理 システム設計 開発 チームマネジメント これを更に細分化し、私の実業務と照らし合わせながら並べてみると、多少粒度にばらつきがあるかもしれませんが以下のようなことが挙げられます。 要件定義・画面設計(ディレクターとデザイナー主導で進めつつ、エンジニアも実データや既存ロジックを踏まえた観点を持ち合わせて参加しました) 開発方針の検討 開発タスクへの落とし込み 技術調査・選定 API 設計 工数算出・スケジューリング 実装・レビュー QA(Quality Assurance)テスト リリースマネジメント Phase2 は段階的にリリースを行ったため、その度に 1 から 9 までを繰り返していたような流れになります。また、上記に加え、定例ミーティングでの報告や開発メンバーのタスクマネジメントも随時行っていました。 もちろん苦労したことは多く、全部を挙げようとするとキリがないのですが、その中でもいくつかに絞った上で紹介したいと思います。 苦労と工夫 1. 「そもそも何をやればいいのか」 まず最初に苦労したことは「そもそも何をやればいいのかわからない」ということでした。初めから先ほど挙げたような動きをイメージできていたわけではなく、記事や本を読み漁ったり先輩との 1on1 で質問攻めにしたりと基本的な知識を叩き込むわけですが、実際にとった最初の動きとしては「できる部分を見つけてやっていく」ということだったと思います。 自分がリーダーに任命された時点でのプロジェクトの状況としては要件定義や画面設計が進んでいる最中でしたが、これらがまとまるのを待つのではなく「全部決まらないとやれないこと」と「現時点でやれること」を切り分けて動きました。こうしたところから少しずつリズムを作り、最終的に先ほど列挙したような一通りのことがイメージ・実行できるようになったのだと思います。 2. 工数見積もり 一般的に工数見積もりに関する記事は世の中に多く存在しますが、私の場合は工数見積もりの方法がわからなかったというよりも、「どういう思想で見積もったのか、どういう選択肢があるのか」を曖昧にしていたことが当初の問題でした。 初めて見積もった時は単に開発タスクを積み上げた工数を報告して満足してしまいましたが、様々な方のフィードバックを受けプロダクト価値を高めるためにどういう動きができるのかを考える必要があったことを痛感しました。単純に工数を積み上げる場合や事業的な都合を踏まえてミニマムで開発する場合など、いくつかの選択肢をそれぞれの軸で考える必要があったことを学びました(この時期は夜な夜な夢の中で工数見積もりをしていたのも今ではいい思い出です)。 3. 意思決定 これはいつになっても正解が存在する類のものではないのですが、特に意思決定には苦労しました。意思決定といっても開発方針から技術選定まで様々な粒度のものがありますが、特に最初から苦労したのは技術的な決定でした。 それまで先輩に頼ることの多かった私がプロジェクトリーダーになった直後から何もかもできるようになるわけではないことは明々白々ですが、「自分が決めないと」と焦ってしまっていた時期もあったと思います。 そこで一度立ち止まって意識したことは、「何ができて何ができないのかを他者に明示する」ことでした。はっきりと自分に足りていないことを他者に伝えることで、周りもサポートしやすくなると思いますし、自分自身なにがやれることなのか明確になるので単純なことですが効果的であったと思います。他にも開発メンバーの提案で、インセプションデッキを取り入れてみたことも効果的でした。 また、意思決定とは文脈が少し変わってきますが、モブプロやペアプロを実施してチーム力を高め属人化をなくしつつ開発効率を向上させる取り組みも、時間が経てば経つほど効果を実感できて良かったと思います。このようにアジャイル開発の手法からチームにフィットする手法をいくつか取り入れることもできました。 プロジェクトを通して成長したこと これまで小出しで色々とお話しさせていただきましたが、自分が特に成長したと感じていることをまとめさせていただきます。 一通りの経験を通して得られたリード力 「API 設計だけ」ではなく一通り全てを任せていただいたことはとても大きな経験になりました。初めて個人ではなくチーム・プロジェクト全体として効率が良くなる動きを考える経験もできたと思います。 技術力 もちろん実装を通じて得た技術は数えきれないほどありますが、その中でも特に責任を持って他者のコードをレビューしたり、自分が書くコードの影響範囲やスコープを意識し続けたことが大きな糧になっている気がします。 リスク管理力 スケジュール遅延のリスク、方向性がずれてしまうリスク、技術的なリスク、様々ありますがこれらのリスクヘッジを考える力がプロジェクトリーダーには必要です。 リスク管理において「先読みが大切」とよく言われますが、私の場合はある先輩社員から「常に 2 週間先を見据えておけ」という具体的な日数のアドバイスをいただきました。具体的にすることであらゆることが想像しやすくなりましたし、それを 1 年以上毎日意識し実行し続けたことが、プロジェクトをやり切ることができた要因にもなっていると思います。もちろんこの言葉は家宝にしようと思っています。 価値に対する視野 何よりも「プロダクトのユーザーに価値を提供すること」の意味を理解しました。ここまでに書いてきたようなスケジュール管理やリスク管理などは、あくまでプロジェクトを遂行する上で必要な仕事の一つでしかないはずです。プロジェクトを通してシステムを使っている社員、更にはその先の顧客・求職者へ如何に価値を提供できるか考えるべきですが、一時期は「どうやるのか・なにをやるのか」というプロジェクト自体を完遂させることしか考えられていない時期もありました。 視野が狭くなっていたことに周りからの指摘で気づくことができ、それ以降は「そもそも本当にこの機能はいるのか」などユーザーの立場からの観点も徐々に身に付けることができました。これがきっかけとなり、周りとも頻繁に「なぜやるのか」を議論できるようになったと思います。新卒 1 年目で口酸っぱく言われていた「目的意識」をようやく腹落ちさせ体現することができました。 最後に 最後となりますが、プロジェクトリーダーについて語ってきた私ですが、入社するまでは Web 開発未経験でして、メドレーでの成長を非常に実感しています。そんなメドレーではエンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集していますので、少しでもご興味をお持ちいただけた方は、是非お気軽にお話しさせていただければと思います! ここまでお付き合いいただき、ありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 https://www.medley.jp/jobs/
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
事業本部 プロダクト開発室エンジニアの日下です。 オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」 の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。 今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。 画面共有機能実装の背景 CLINICS とオンライン診療 普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。 CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。 ※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。 CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。 クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチ され、 2018 年にはクラウド型電子カルテ機能を 、 2019 年には予約管理システム機能を 、 2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し 、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。 プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。 画面共有機能の需要の高まり、実装の決定 このように CLINICS の改善を日々行なっている中、昨年から始まった 新型コロナウイルス感染症(COVID-19) の流行に伴った需要の増加により、オンライン診療の件数が急増しました。 CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。 対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。 こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。 画面共有機能の実装 画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。 ※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。 オンライン診察開始までの処理 オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。 カメラ・マイクのストリームの取得 オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。 マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); SkyWay 経由でオンライン診察を始める WebRTC で P2P のビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaS の SkyWay 及びその SDK を利用することで簡略化しています。 オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。 import Peer , { MediaConnection } from "skyway-js" ; const peer = new Peer ({ key: "your-api-key" }); // 事前に患者と共有しておいた peer id に対して call メソッドと MediaStream を渡すことで診察を開始できる。 const mediaConnection : MediaConnection = peer . call ( "shared-peer-id" , userMediaStream ); // 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある ここまでがオンライン診察を開始するまでの処理です。 ※ 詳細は SkyWay 公式の チュートリアル などを参照ください。 画面共有の処理 ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。 画面の MediaStream の取得 まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。 const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); 画面共有用の MediaStream を作る getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。 これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。 MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る 画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。 MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。 また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks() ・ MediaStream.getVideoTracks() として実装されています。 これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。 const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingMediaStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); MediaStream の入れ替え 最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。 例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。 今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。 この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッド に対して新しい MediaStream を渡すことで実現ができます。 // 画面共有用の MediaStream を渡すことで、画面共有を開始する // MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する mediaConnection . replaceStream ( sharingMediaStream ); 実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。 実装の全体概要 以上の流れを実装すると、次のようなコードになります。 import Peer , { MediaConnection } from "skyway-js" ; /** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/ // getUserMedia()でカメラ・マイクのストリームを取得 const userMediaStream : MediaStream = await navigator . mediaDevices . getUserMedia ({ video: true , audio: true , }); // Skyway sdk の初期化処理 const peer = new Peer ({ key: "your-api-key" }); // オンライン診察の開始 const mediaConnection : MediaConnection = peer . call ( "peerId" , userMediaStream ); /** 画面共有を開始する処理 **/ // 画面共有する画面の stream を取る const displayStream : MediaStream = await navigator . mediaDevices . getDisplayMedia ( { video: true } ); const [ displayVideoTrack ]: MediaStreamTrack [] = displayStream . getVideoTracks (); // 画面共有の音声はマイクの音声を利用したいので、userMediaStream から audioTrack を取り出しておく const [ userAudioTrack ]: MediaStreamTrack [] = userMediaStream . getAudioTracks (); // 画面共有するための MediaStream を作成する(画面の videoTrack、マイクの audioTrack を持つ MediaStream を作る) const sharingStream : MediaStream = new MediaStream ([ displayVideoTrack , userAudioTrack , ]); // 画面共有用の MediaStream を渡すことで、画面共有を開始する mediaConnection . replaceStream ( sharingStream ); 開発中に遭遇した問題への対応 スリープモード・共有を停止ボタンを押したときの対応 Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。 これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。 displayVideoTrack . addEventListener ( "ended" , handleEndedEvent , { once: true }); TypeScript の型の対応 現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型 を流用する形で対応をしました。 declare global { interface MediaDevices { getDisplayMedia ( constraints : MediaStreamConstraints ): Promise < MediaStream >; } } これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、 TypeScript4.4 で対応がされるようです 。 まとめ 昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。 PC 画面の MediaStream は getDisplayMedia() を使うことで取得 MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成 接続中の MediaStream の変更は SkyWay の SDK の MediaConnection.replaceStream() を使う 最後に CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひ こちら にご連絡いただければと思います。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめに はじめまして、コーポレートエンジニアの山下です。 2020 年に Slack を活用した ChatOps 稟議ワークフローを内製で開発したのですが、さらに、2021 年 4 月にこの Slack 稟議と電子契約システムである クラウドサイン を連携させて、電子契約をもっと便利に使い、生産性の向上を実現しましたのでお話しいたします。 まず、当社の稟議システムは 2020 年 12 月の当社の記事 のおさらいになりますが、稟議の作業が Slack 上で完結する、 ChatOps による稟議ワークフロー となっております。本稿については 2021 年 7 月に執筆しておりますので丁度導入から 1 年程経過し、その間大きなトラブルも無く、今も当社の極めて迅速な意思決定の一助になっています。ChatOps による稟議ワークフローについては、直近、2021 年 6 月に LayerX 社が LayerX ワークフローの新機能として発表 し、サービスとしても提供され、 日経新聞 でも取り上げられていることから、今現在のパラダイムとして、先進的で有効な一手法であったと再認識しております。 今回、新型コロナウイルス感染拡大防止に伴うリモートワークの加速という状況もあり、当社で 2021 年 4 月に電子契約システムとしてクラウドサインを導入しました。電子契約に限らず、契約押印作業は稟議の後続作業に当たるため、ただ導入して使用するのみならず、クラウドサインの API を利用して稟議上にあるデータを電子契約に送信させることでシームレスな連携を実現しています。本稿では当社が行ったシームレスな連携手法について詳細をご説明いたします。 TeamSpirit とクラウドサインの API 連携について 実装概要 弊社の稟議システムである TeamSpirit とクラウドサインとの連携についてお話しします。まず、本稿の開発部分とシステム構成は下記になっております。 処理内容の詳細は後ほど述べますが、概要としては TeamSprit(Apex)からクラウドサインの API をコールし、クラウドサイン上で作成した契約文書へ稟議に記載されている契約書ファイルや先方担当者等の情報を連携する仕組みとなっております。これにより契約担当者はクラウドサインにログイン後、下記の 3 ステップで先方に送信できるようになっています。 記載内容の確認 押印・署名箇所の設定 先方への送信 クラウドサインを使用して契約文書を一から作成する場合のユーザ作業と、当社で採用した API 連携行った場合のユーザ作業を比較したものが下記の表です。作業が半分程度削減されたことが分かります。 作業項番 一から作成する場合 API 連携を利用した当社の場合 1 ログイン ログイン 2 契約文書の作成(件名、契約文書としての宛名設定等) なし 3 契約書ファイルのアップロード なし 4 先方の送信先設定 なし 5 押印欄の設定 押印欄の設定 6 先方への送信 先方への送信 実装 今回の開発で使用した クラウドサイン API は下記の 5 つの API を使用しました (※以降、クラウドサイン API に倣い、変数を表現する場合は {} で括ります)。 API 種類 使用用途 post /token アクセストークンの取得 post /document 契約文書の作成 put /documents/{documentID}/attribute 契約文書の作成で設定できない、細かい項目の設定 post /documents/{documentID}/files ファイルのアップロード post /documents/{documentID}/participants 先方の送信先設定 全体像で記載したクラウドサインの連携部について、上記の API を織り交ぜて詳細化すると下図のようになります。 実装方法としてはクラウドサイン API のリファレンスを参照し、テスト実行時に出力される curl コマンドを参考に同様のレスポンスを得るように Apex で HTTP リクエストを実装しました。アクセストークンの取得を例にとると下記のようになります。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/token' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=xxxxxxyyyyyyzzzzzz' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/token' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Content-Type' , 'application/x-www-form-urlencoded' ); req . setBody ( 'client_id=' + 'xxxxxxyyyyyyzzzzzz' ); 私自身、Apex もクラウドサイン API もこの案件を担当するまで触ったことがありませんでしたが、リクエストの試行から実装まで 2 週間かからない程度で実装することができました。 ただし、実装や運用にあたっては下記 2 点について注意が必要になります。 Apex からクラウドサインへのファイルのアップロードは単純ではない アクセストークンの有効期限はクラウドサインでコントロールされる 1. Apex からクラウドサインへのファイルのアップロードは単純ではない ファイルのアップロードについては今回使用した API の中で、唯一、テスト実行の curl と Apex のリクエスト実装で差分が生まれます。まず、その差分を確認するために curl コマンド例と Apex のリクエスト実装例で header、body にセットしている値を比較してみます。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/documents/{document_id}/files' \ -H 'accept: application/json' \ -H 'Authorization: AAAAAABBBBBBCCCCCC' \ -H 'Content-Type: multipart/form-data' \ -F 'name=テスト' \ -F 'uploadfile=@テスト.pdf;type=application/pdf' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/documents/{document_id}/files' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Authorization' , ‘AAAAAABBBBBBCCCCCC’); req . setHeader ( 'Content-Type' , 'multipart/form-data; boundary={boundary}' ); // ※1 req . setBodyAsBlob ({multipartBody}); // ※2 主な違いは ※1 , ※2 とコメントした部分になります。 Apex では HTTP リクエストの値を手で書いていくことになるので、テスト実行例のように curl がよしなに処理している部分(-F オプションの部分や Apex で記載している boundary)も実装しなければなりません。これが単純に実装できない理由になります。 boundary については multipart/form-data を送信する際に必要な境界でヘッダーでどの文字列が境界であるかを設定します。 curl の-F オプションで定義していた文字列とファイル指定部分は、Apex でファイル(バイナリ)を扱うため、その body に含まれる文字列も含めて Blob 型で扱う必要があります( Content-Transfer-Encoding: base64 に API 提供側が対応している場合は例外になります)。そのため、文字列とバイナリデータを結合し一つの Blob にする方法は下記になります。 「バイナリデータ」、「body の開始からバイナリデータまでの文字列」、「バイナリデータ以降から終端までの文字列」の 3 グループに分ける。 3 グループをそれぞれ Base64 で符号化する。 符号化した「バイナリデータ」と「body の開始からバイナリデータまでの文字列」について、Base64 のデータパディングを示す”=”が含まれないように改行コードで調整する。 「body の開始からバイナリデータまでの文字列」、「バイナリデータ」、「バイナリデータ以降から終端までの文字列」の順で結合する。 結合した Base64 のデータを復号して、一つの Blob とする。 2. アクセストークンの有効期限はクラウドサインでコントロールされる アクセストークンやその有効期限は token API を発行した際のレスポンスとしてクラウドサインから発行されます。 発行されたレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 3600 } このレスポンスの内、expires_in の値がトークンの有効期限になります。掲題の通り、有効期限の管理はクラウドサイン側で行われ、有効期限内に再度トークンのリクエストを行った場合、経過した時間だけ expires_in の値が小さくなった結果が返ってきて、access_token などは同じ値が取得されます。有効期限内に token API を再度実行した結果が下記になります。 有効期限切れ前に token API を発行した際のレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 762 } 一方、有効期限後にトークンのリクエストを実行すると、それまでと異なるアクセストークンを取得し、新しい有効期限が設定されます。 有効期限切れ後に token API を発行した際のレスポンス例 { "access_token" : "XXXXXXYYYYYYZZZZZZ" , "token_type" : "xxxx" , "expires_in" : 3600 } そのため、API 連携が一度動いた後、有効期限ぎりぎりでもう一度 API 連携が動いてしまった場合、タイミングが悪いと契約文書の作成から最終処理である先方の送信先設定までのプロセス内のどこかから、トークンの有効期限切れが発生する可能性が想定されます。実際に期限切れが発生した場合、発生時以降に発行したその回の API 連携処理が失敗します。 トークンの有効期限切れが発生した際、API リファレンスより HTTP ステータスコードが 401 かつエラー内容が”unauthorized”で応答されることから、当社ではこのエラーを受けた場合にトークンを再取得して処理をリトライするように実装しました。 押印文書作成を例にとると下記のような実装イメージになります。 //クラウドサイン上に押印文書を作成し、作成した文書 ID を取得する public String getDocumentId ( String authToken, String title, String message){ ・・・中略・・・ HTTPResponse res = http . send (req); if ( res . getStatusCode () == 200 ){ ・・・正常に終了した際の処理・・・ } //タイミングが悪く token がタイムアウトした場合、トークンを取得し直して、リトライする else if ( res . getStatusCode () == 401 ){ //レスポンスの内容を確認するため、エラーレスポンスの中身を取得する Map < String , Object > responseBody = new Map < String , Object >(); responseBody = ( Map <String, Object>) JSON . deserializeUntyped ( res . getBody ()); String errorVal = (String) responseBody . get ( 'error' ); //リファレンス上、アクセストークンが無効(有効期限切れ)の場合、'unauthorized’となる if ( errorVal . equals ( 'unauthorized' )){ //クラウドサインのアクセストークンの再取得 authToken = getAuthToken (); //単純再帰で再実行する。 documentId = getDocumentId (authToken, title, message); } ・・・中略・・・ } ・・・以下省略・・・ } 実装を終えて 上記を実装した結果、稟議と入力内容が同じ、または、稟議から生成できる内容は全てシステム連携で自動生成するため、押印担当は稟議とクラウドサインの画面を並べて転記するような煩雑な作業を必要としない環境になりました。また、契約書の製本、郵送等の紙媒体であるが故の事務の削減ができるようになる等の、電子契約を導入することのそもそものメリットも併せて享受しています。 当社では 2021 年 4 月後半からクラウドサインを導入しましたが、2021 年 6 月時点ではすでに 月間で締結した契約書の「3 割以上」が電子契約を活用しており 、押印担当の展望として今後も利用を拡大していく予定です。 コーポレートエンジニア募集中 メドレーのコーポレート部門では、本稿のように、SaaS の導入ひとつとっても検討を尽くし、既存のシステムと有機的に結合させることで「徹底的に合理性を追求した組織基盤や、仕掛けづくり」を行っています。 面白そう!興味がある!と感じた方は、ぜひ当社採用ページからご応募お願いします! 最後までお読みいただきありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめに はじめまして、コーポレートエンジニアの山下です。 2020 年に Slack を活用した ChatOps 稟議ワークフローを内製で開発したのですが、さらに、2021 年 4 月にこの Slack 稟議と電子契約システムである クラウドサイン を連携させて、電子契約をもっと便利に使い、生産性の向上を実現しましたのでお話しいたします。 まず、当社の稟議システムは 2020 年 12 月の当社の記事 のおさらいになりますが、稟議の作業が Slack 上で完結する、 ChatOps による稟議ワークフロー となっております。本稿については 2021 年 7 月に執筆しておりますので丁度導入から 1 年程経過し、その間大きなトラブルも無く、今も当社の極めて迅速な意思決定の一助になっています。ChatOps による稟議ワークフローについては、直近、2021 年 6 月に LayerX 社が LayerX ワークフローの新機能として発表 し、サービスとしても提供され、 日経新聞 でも取り上げられていることから、今現在のパラダイムとして、先進的で有効な一手法であったと再認識しております。 今回、新型コロナウイルス感染拡大防止に伴うリモートワークの加速という状況もあり、当社で 2021 年 4 月に電子契約システムとしてクラウドサインを導入しました。電子契約に限らず、契約押印作業は稟議の後続作業に当たるため、ただ導入して使用するのみならず、クラウドサインの API を利用して稟議上にあるデータを電子契約に送信させることでシームレスな連携を実現しています。本稿では当社が行ったシームレスな連携手法について詳細をご説明いたします。 TeamSpirit とクラウドサインの API 連携について 実装概要 弊社の稟議システムである TeamSpirit とクラウドサインとの連携についてお話しします。まず、本稿の開発部分とシステム構成は下記になっております。 処理内容の詳細は後ほど述べますが、概要としては TeamSprit(Apex)からクラウドサインの API をコールし、クラウドサイン上で作成した契約文書へ稟議に記載されている契約書ファイルや先方担当者等の情報を連携する仕組みとなっております。これにより契約担当者はクラウドサインにログイン後、下記の 3 ステップで先方に送信できるようになっています。 記載内容の確認 押印・署名箇所の設定 先方への送信 クラウドサインを使用して契約文書を一から作成する場合のユーザ作業と、当社で採用した API 連携行った場合のユーザ作業を比較したものが下記の表です。作業が半分程度削減されたことが分かります。 作業項番 一から作成する場合 API 連携を利用した当社の場合 1 ログイン ログイン 2 契約文書の作成(件名、契約文書としての宛名設定等) なし 3 契約書ファイルのアップロード なし 4 先方の送信先設定 なし 5 押印欄の設定 押印欄の設定 6 先方への送信 先方への送信 実装 今回の開発で使用した クラウドサイン API は下記の 5 つの API を使用しました (※以降、クラウドサイン API に倣い、変数を表現する場合は {} で括ります)。 API 種類 使用用途 post /token アクセストークンの取得 post /document 契約文書の作成 put /documents/{documentID}/attribute 契約文書の作成で設定できない、細かい項目の設定 post /documents/{documentID}/files ファイルのアップロード post /documents/{documentID}/participants 先方の送信先設定 全体像で記載したクラウドサインの連携部について、上記の API を織り交ぜて詳細化すると下図のようになります。 実装方法としてはクラウドサイン API のリファレンスを参照し、テスト実行時に出力される curl コマンドを参考に同様のレスポンスを得るように Apex で HTTP リクエストを実装しました。アクセストークンの取得を例にとると下記のようになります。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/token' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=xxxxxxyyyyyyzzzzzz' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/token' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Content-Type' , 'application/x-www-form-urlencoded' ); req . setBody ( 'client_id=' + 'xxxxxxyyyyyyzzzzzz' ); 私自身、Apex もクラウドサイン API もこの案件を担当するまで触ったことがありませんでしたが、リクエストの試行から実装まで 2 週間かからない程度で実装することができました。 ただし、実装や運用にあたっては下記 2 点について注意が必要になります。 Apex からクラウドサインへのファイルのアップロードは単純ではない アクセストークンの有効期限はクラウドサインでコントロールされる 1. Apex からクラウドサインへのファイルのアップロードは単純ではない ファイルのアップロードについては今回使用した API の中で、唯一、テスト実行の curl と Apex のリクエスト実装で差分が生まれます。まず、その差分を確認するために curl コマンド例と Apex のリクエスト実装例で header、body にセットしている値を比較してみます。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/documents/{document_id}/files' \ -H 'accept: application/json' \ -H 'Authorization: AAAAAABBBBBBCCCCCC' \ -H 'Content-Type: multipart/form-data' \ -F 'name=テスト' \ -F 'uploadfile=@テスト.pdf;type=application/pdf' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/documents/{document_id}/files' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Authorization' , ‘AAAAAABBBBBBCCCCCC’); req . setHeader ( 'Content-Type' , 'multipart/form-data; boundary={boundary}' ); // ※1 req . setBodyAsBlob ({multipartBody}); // ※2 主な違いは ※1 , ※2 とコメントした部分になります。 Apex では HTTP リクエストの値を手で書いていくことになるので、テスト実行例のように curl がよしなに処理している部分(-F オプションの部分や Apex で記載している boundary)も実装しなければなりません。これが単純に実装できない理由になります。 boundary については multipart/form-data を送信する際に必要な境界でヘッダーでどの文字列が境界であるかを設定します。 curl の-F オプションで定義していた文字列とファイル指定部分は、Apex でファイル(バイナリ)を扱うため、その body に含まれる文字列も含めて Blob 型で扱う必要があります( Content-Transfer-Encoding: base64 に API 提供側が対応している場合は例外になります)。そのため、文字列とバイナリデータを結合し一つの Blob にする方法は下記になります。 「バイナリデータ」、「body の開始からバイナリデータまでの文字列」、「バイナリデータ以降から終端までの文字列」の 3 グループに分ける。 3 グループをそれぞれ Base64 で符号化する。 符号化した「バイナリデータ」と「body の開始からバイナリデータまでの文字列」について、Base64 のデータパディングを示す”=”が含まれないように改行コードで調整する。 「body の開始からバイナリデータまでの文字列」、「バイナリデータ」、「バイナリデータ以降から終端までの文字列」の順で結合する。 結合した Base64 のデータを復号して、一つの Blob とする。 2. アクセストークンの有効期限はクラウドサインでコントロールされる アクセストークンやその有効期限は token API を発行した際のレスポンスとしてクラウドサインから発行されます。 発行されたレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 3600 } このレスポンスの内、expires_in の値がトークンの有効期限になります。掲題の通り、有効期限の管理はクラウドサイン側で行われ、有効期限内に再度トークンのリクエストを行った場合、経過した時間だけ expires_in の値が小さくなった結果が返ってきて、access_token などは同じ値が取得されます。有効期限内に token API を再度実行した結果が下記になります。 有効期限切れ前に token API を発行した際のレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 762 } 一方、有効期限後にトークンのリクエストを実行すると、それまでと異なるアクセストークンを取得し、新しい有効期限が設定されます。 有効期限切れ後に token API を発行した際のレスポンス例 { "access_token" : "XXXXXXYYYYYYZZZZZZ" , "token_type" : "xxxx" , "expires_in" : 3600 } そのため、API 連携が一度動いた後、有効期限ぎりぎりでもう一度 API 連携が動いてしまった場合、タイミングが悪いと契約文書の作成から最終処理である先方の送信先設定までのプロセス内のどこかから、トークンの有効期限切れが発生する可能性が想定されます。実際に期限切れが発生した場合、発生時以降に発行したその回の API 連携処理が失敗します。 トークンの有効期限切れが発生した際、API リファレンスより HTTP ステータスコードが 401 かつエラー内容が”unauthorized”で応答されることから、当社ではこのエラーを受けた場合にトークンを再取得して処理をリトライするように実装しました。 押印文書作成を例にとると下記のような実装イメージになります。 //クラウドサイン上に押印文書を作成し、作成した文書 ID を取得する public String getDocumentId ( String authToken, String title, String message){ ・・・中略・・・ HTTPResponse res = http . send (req); if ( res . getStatusCode () == 200 ){ ・・・正常に終了した際の処理・・・ } //タイミングが悪く token がタイムアウトした場合、トークンを取得し直して、リトライする else if ( res . getStatusCode () == 401 ){ //レスポンスの内容を確認するため、エラーレスポンスの中身を取得する Map < String , Object > responseBody = new Map < String , Object >(); responseBody = ( Map <String, Object>) JSON . deserializeUntyped ( res . getBody ()); String errorVal = (String) responseBody . get ( 'error' ); //リファレンス上、アクセストークンが無効(有効期限切れ)の場合、'unauthorized’となる if ( errorVal . equals ( 'unauthorized' )){ //クラウドサインのアクセストークンの再取得 authToken = getAuthToken (); //単純再帰で再実行する。 documentId = getDocumentId (authToken, title, message); } ・・・中略・・・ } ・・・以下省略・・・ } 実装を終えて 上記を実装した結果、稟議と入力内容が同じ、または、稟議から生成できる内容は全てシステム連携で自動生成するため、押印担当は稟議とクラウドサインの画面を並べて転記するような煩雑な作業を必要としない環境になりました。また、契約書の製本、郵送等の紙媒体であるが故の事務の削減ができるようになる等の、電子契約を導入することのそもそものメリットも併せて享受しています。 当社では 2021 年 4 月後半からクラウドサインを導入しましたが、2021 年 6 月時点ではすでに 月間で締結した契約書の「3 割以上」が電子契約を活用しており 、押印担当の展望として今後も利用を拡大していく予定です。 コーポレートエンジニア募集中 メドレーのコーポレート部門では、本稿のように、SaaS の導入ひとつとっても検討を尽くし、既存のシステムと有機的に結合させることで「徹底的に合理性を追求した組織基盤や、仕掛けづくり」を行っています。 面白そう!興味がある!と感じた方は、ぜひ当社採用ページからご応募お願いします! 最後までお読みいただきありがとうございました。 https://www.medley.jp/jobs/
はじめに はじめまして、コーポレートエンジニアの山下です。 2020 年に Slack を活用した ChatOps 稟議ワークフローを内製で開発したのですが、さらに、2021 年 4 月にこの Slack 稟議と電子契約システムである クラウドサイン を連携させて、電子契約をもっと便利に使い、生産性の向上を実現しましたのでお話しいたします。 まず、当社の稟議システムは 2020 年 12 月の当社の記事 のおさらいになりますが、稟議の作業が Slack 上で完結する、 ChatOps による稟議ワークフロー となっております。本稿については 2021 年 7 月に執筆しておりますので丁度導入から 1 年程経過し、その間大きなトラブルも無く、今も当社の極めて迅速な意思決定の一助になっています。ChatOps による稟議ワークフローについては、直近、2021 年 6 月に LayerX 社が LayerX ワークフローの新機能として発表 し、サービスとしても提供され、 日経新聞 でも取り上げられていることから、今現在のパラダイムとして、先進的で有効な一手法であったと再認識しております。 今回、新型コロナウイルス感染拡大防止に伴うリモートワークの加速という状況もあり、当社で 2021 年 4 月に電子契約システムとしてクラウドサインを導入しました。電子契約に限らず、契約押印作業は稟議の後続作業に当たるため、ただ導入して使用するのみならず、クラウドサインの API を利用して稟議上にあるデータを電子契約に送信させることでシームレスな連携を実現しています。本稿では当社が行ったシームレスな連携手法について詳細をご説明いたします。 TeamSpirit とクラウドサインの API 連携について 実装概要 弊社の稟議システムである TeamSpirit とクラウドサインとの連携についてお話しします。まず、本稿の開発部分とシステム構成は下記になっております。 処理内容の詳細は後ほど述べますが、概要としては TeamSprit(Apex)からクラウドサインの API をコールし、クラウドサイン上で作成した契約文書へ稟議に記載されている契約書ファイルや先方担当者等の情報を連携する仕組みとなっております。これにより契約担当者はクラウドサインにログイン後、下記の 3 ステップで先方に送信できるようになっています。 記載内容の確認 押印・署名箇所の設定 先方への送信 クラウドサインを使用して契約文書を一から作成する場合のユーザ作業と、当社で採用した API 連携行った場合のユーザ作業を比較したものが下記の表です。作業が半分程度削減されたことが分かります。 作業項番 一から作成する場合 API 連携を利用した当社の場合 1 ログイン ログイン 2 契約文書の作成(件名、契約文書としての宛名設定等) なし 3 契約書ファイルのアップロード なし 4 先方の送信先設定 なし 5 押印欄の設定 押印欄の設定 6 先方への送信 先方への送信 実装 今回の開発で使用した クラウドサイン API は下記の 5 つの API を使用しました (※以降、クラウドサイン API に倣い、変数を表現する場合は {} で括ります)。 API 種類 使用用途 post /token アクセストークンの取得 post /document 契約文書の作成 put /documents/{documentID}/attribute 契約文書の作成で設定できない、細かい項目の設定 post /documents/{documentID}/files ファイルのアップロード post /documents/{documentID}/participants 先方の送信先設定 全体像で記載したクラウドサインの連携部について、上記の API を織り交ぜて詳細化すると下図のようになります。 実装方法としてはクラウドサイン API のリファレンスを参照し、テスト実行時に出力される curl コマンドを参考に同様のレスポンスを得るように Apex で HTTP リクエストを実装しました。アクセストークンの取得を例にとると下記のようになります。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/token' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=xxxxxxyyyyyyzzzzzz' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/token' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Content-Type' , 'application/x-www-form-urlencoded' ); req . setBody ( 'client_id=' + 'xxxxxxyyyyyyzzzzzz' ); 私自身、Apex もクラウドサイン API もこの案件を担当するまで触ったことがありませんでしたが、リクエストの試行から実装まで 2 週間かからない程度で実装することができました。 ただし、実装や運用にあたっては下記 2 点について注意が必要になります。 Apex からクラウドサインへのファイルのアップロードは単純ではない アクセストークンの有効期限はクラウドサインでコントロールされる 1. Apex からクラウドサインへのファイルのアップロードは単純ではない ファイルのアップロードについては今回使用した API の中で、唯一、テスト実行の curl と Apex のリクエスト実装で差分が生まれます。まず、その差分を確認するために curl コマンド例と Apex のリクエスト実装例で header、body にセットしている値を比較してみます。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/documents/{document_id}/files' \ -H 'accept: application/json' \ -H 'Authorization: AAAAAABBBBBBCCCCCC' \ -H 'Content-Type: multipart/form-data' \ -F 'name=テスト' \ -F 'uploadfile=@テスト.pdf;type=application/pdf' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/documents/{document_id}/files' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Authorization' , ‘AAAAAABBBBBBCCCCCC’); req . setHeader ( 'Content-Type' , 'multipart/form-data; boundary={boundary}' ); // ※1 req . setBodyAsBlob ({multipartBody}); // ※2 主な違いは ※1 , ※2 とコメントした部分になります。 Apex では HTTP リクエストの値を手で書いていくことになるので、テスト実行例のように curl がよしなに処理している部分(-F オプションの部分や Apex で記載している boundary)も実装しなければなりません。これが単純に実装できない理由になります。 boundary については multipart/form-data を送信する際に必要な境界でヘッダーでどの文字列が境界であるかを設定します。 curl の-F オプションで定義していた文字列とファイル指定部分は、Apex でファイル(バイナリ)を扱うため、その body に含まれる文字列も含めて Blob 型で扱う必要があります( Content-Transfer-Encoding: base64 に API 提供側が対応している場合は例外になります)。そのため、文字列とバイナリデータを結合し一つの Blob にする方法は下記になります。 「バイナリデータ」、「body の開始からバイナリデータまでの文字列」、「バイナリデータ以降から終端までの文字列」の 3 グループに分ける。 3 グループをそれぞれ Base64 で符号化する。 符号化した「バイナリデータ」と「body の開始からバイナリデータまでの文字列」について、Base64 のデータパディングを示す”=”が含まれないように改行コードで調整する。 「body の開始からバイナリデータまでの文字列」、「バイナリデータ」、「バイナリデータ以降から終端までの文字列」の順で結合する。 結合した Base64 のデータを復号して、一つの Blob とする。 2. アクセストークンの有効期限はクラウドサインでコントロールされる アクセストークンやその有効期限は token API を発行した際のレスポンスとしてクラウドサインから発行されます。 発行されたレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 3600 } このレスポンスの内、expires_in の値がトークンの有効期限になります。掲題の通り、有効期限の管理はクラウドサイン側で行われ、有効期限内に再度トークンのリクエストを行った場合、経過した時間だけ expires_in の値が小さくなった結果が返ってきて、access_token などは同じ値が取得されます。有効期限内に token API を再度実行した結果が下記になります。 有効期限切れ前に token API を発行した際のレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 762 } 一方、有効期限後にトークンのリクエストを実行すると、それまでと異なるアクセストークンを取得し、新しい有効期限が設定されます。 有効期限切れ後に token API を発行した際のレスポンス例 { "access_token" : "XXXXXXYYYYYYZZZZZZ" , "token_type" : "xxxx" , "expires_in" : 3600 } そのため、API 連携が一度動いた後、有効期限ぎりぎりでもう一度 API 連携が動いてしまった場合、タイミングが悪いと契約文書の作成から最終処理である先方の送信先設定までのプロセス内のどこかから、トークンの有効期限切れが発生する可能性が想定されます。実際に期限切れが発生した場合、発生時以降に発行したその回の API 連携処理が失敗します。 トークンの有効期限切れが発生した際、API リファレンスより HTTP ステータスコードが 401 かつエラー内容が”unauthorized”で応答されることから、当社ではこのエラーを受けた場合にトークンを再取得して処理をリトライするように実装しました。 押印文書作成を例にとると下記のような実装イメージになります。 //クラウドサイン上に押印文書を作成し、作成した文書 ID を取得する public String getDocumentId ( String authToken, String title, String message){ ・・・中略・・・ HTTPResponse res = http . send (req); if ( res . getStatusCode () == 200 ){ ・・・正常に終了した際の処理・・・ } //タイミングが悪く token がタイムアウトした場合、トークンを取得し直して、リトライする else if ( res . getStatusCode () == 401 ){ //レスポンスの内容を確認するため、エラーレスポンスの中身を取得する Map < String , Object > responseBody = new Map < String , Object >(); responseBody = ( Map <String, Object>) JSON . deserializeUntyped ( res . getBody ()); String errorVal = (String) responseBody . get ( 'error' ); //リファレンス上、アクセストークンが無効(有効期限切れ)の場合、'unauthorized’となる if ( errorVal . equals ( 'unauthorized' )){ //クラウドサインのアクセストークンの再取得 authToken = getAuthToken (); //単純再帰で再実行する。 documentId = getDocumentId (authToken, title, message); } ・・・中略・・・ } ・・・以下省略・・・ } 実装を終えて 上記を実装した結果、稟議と入力内容が同じ、または、稟議から生成できる内容は全てシステム連携で自動生成するため、押印担当は稟議とクラウドサインの画面を並べて転記するような煩雑な作業を必要としない環境になりました。また、契約書の製本、郵送等の紙媒体であるが故の事務の削減ができるようになる等の、電子契約を導入することのそもそものメリットも併せて享受しています。 当社では 2021 年 4 月後半からクラウドサインを導入しましたが、2021 年 6 月時点ではすでに 月間で締結した契約書の「3 割以上」が電子契約を活用しており 、押印担当の展望として今後も利用を拡大していく予定です。 コーポレートエンジニア募集中 メドレーのコーポレート部門では、本稿のように、SaaS の導入ひとつとっても検討を尽くし、既存のシステムと有機的に結合させることで「徹底的に合理性を追求した組織基盤や、仕掛けづくり」を行っています。 面白そう!興味がある!と感じた方は、ぜひ当社採用ページからご応募お願いします! 最後までお読みいただきありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめに はじめまして、コーポレートエンジニアの山下です。 2020 年に Slack を活用した ChatOps 稟議ワークフローを内製で開発したのですが、さらに、2021 年 4 月にこの Slack 稟議と電子契約システムである クラウドサイン を連携させて、電子契約をもっと便利に使い、生産性の向上を実現しましたのでお話しいたします。 まず、当社の稟議システムは 2020 年 12 月の当社の記事 のおさらいになりますが、稟議の作業が Slack 上で完結する、 ChatOps による稟議ワークフロー となっております。本稿については 2021 年 7 月に執筆しておりますので丁度導入から 1 年程経過し、その間大きなトラブルも無く、今も当社の極めて迅速な意思決定の一助になっています。ChatOps による稟議ワークフローについては、直近、2021 年 6 月に LayerX 社が LayerX ワークフローの新機能として発表 し、サービスとしても提供され、 日経新聞 でも取り上げられていることから、今現在のパラダイムとして、先進的で有効な一手法であったと再認識しております。 今回、新型コロナウイルス感染拡大防止に伴うリモートワークの加速という状況もあり、当社で 2021 年 4 月に電子契約システムとしてクラウドサインを導入しました。電子契約に限らず、契約押印作業は稟議の後続作業に当たるため、ただ導入して使用するのみならず、クラウドサインの API を利用して稟議上にあるデータを電子契約に送信させることでシームレスな連携を実現しています。本稿では当社が行ったシームレスな連携手法について詳細をご説明いたします。 TeamSpirit とクラウドサインの API 連携について 実装概要 弊社の稟議システムである TeamSpirit とクラウドサインとの連携についてお話しします。まず、本稿の開発部分とシステム構成は下記になっております。 処理内容の詳細は後ほど述べますが、概要としては TeamSprit(Apex)からクラウドサインの API をコールし、クラウドサイン上で作成した契約文書へ稟議に記載されている契約書ファイルや先方担当者等の情報を連携する仕組みとなっております。これにより契約担当者はクラウドサインにログイン後、下記の 3 ステップで先方に送信できるようになっています。 記載内容の確認 押印・署名箇所の設定 先方への送信 クラウドサインを使用して契約文書を一から作成する場合のユーザ作業と、当社で採用した API 連携行った場合のユーザ作業を比較したものが下記の表です。作業が半分程度削減されたことが分かります。 作業項番 一から作成する場合 API 連携を利用した当社の場合 1 ログイン ログイン 2 契約文書の作成(件名、契約文書としての宛名設定等) なし 3 契約書ファイルのアップロード なし 4 先方の送信先設定 なし 5 押印欄の設定 押印欄の設定 6 先方への送信 先方への送信 実装 今回の開発で使用した クラウドサイン API は下記の 5 つの API を使用しました (※以降、クラウドサイン API に倣い、変数を表現する場合は {} で括ります)。 API 種類 使用用途 post /token アクセストークンの取得 post /document 契約文書の作成 put /documents/{documentID}/attribute 契約文書の作成で設定できない、細かい項目の設定 post /documents/{documentID}/files ファイルのアップロード post /documents/{documentID}/participants 先方の送信先設定 全体像で記載したクラウドサインの連携部について、上記の API を織り交ぜて詳細化すると下図のようになります。 実装方法としてはクラウドサイン API のリファレンスを参照し、テスト実行時に出力される curl コマンドを参考に同様のレスポンスを得るように Apex で HTTP リクエストを実装しました。アクセストークンの取得を例にとると下記のようになります。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/token' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=xxxxxxyyyyyyzzzzzz' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/token' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Content-Type' , 'application/x-www-form-urlencoded' ); req . setBody ( 'client_id=' + 'xxxxxxyyyyyyzzzzzz' ); 私自身、Apex もクラウドサイン API もこの案件を担当するまで触ったことがありませんでしたが、リクエストの試行から実装まで 2 週間かからない程度で実装することができました。 ただし、実装や運用にあたっては下記 2 点について注意が必要になります。 Apex からクラウドサインへのファイルのアップロードは単純ではない アクセストークンの有効期限はクラウドサインでコントロールされる 1. Apex からクラウドサインへのファイルのアップロードは単純ではない ファイルのアップロードについては今回使用した API の中で、唯一、テスト実行の curl と Apex のリクエスト実装で差分が生まれます。まず、その差分を確認するために curl コマンド例と Apex のリクエスト実装例で header、body にセットしている値を比較してみます。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/documents/{document_id}/files' \ -H 'accept: application/json' \ -H 'Authorization: AAAAAABBBBBBCCCCCC' \ -H 'Content-Type: multipart/form-data' \ -F 'name=テスト' \ -F 'uploadfile=@テスト.pdf;type=application/pdf' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/documents/{document_id}/files' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Authorization' , ‘AAAAAABBBBBBCCCCCC’); req . setHeader ( 'Content-Type' , 'multipart/form-data; boundary={boundary}' ); // ※1 req . setBodyAsBlob ({multipartBody}); // ※2 主な違いは ※1 , ※2 とコメントした部分になります。 Apex では HTTP リクエストの値を手で書いていくことになるので、テスト実行例のように curl がよしなに処理している部分(-F オプションの部分や Apex で記載している boundary)も実装しなければなりません。これが単純に実装できない理由になります。 boundary については multipart/form-data を送信する際に必要な境界でヘッダーでどの文字列が境界であるかを設定します。 curl の-F オプションで定義していた文字列とファイル指定部分は、Apex でファイル(バイナリ)を扱うため、その body に含まれる文字列も含めて Blob 型で扱う必要があります( Content-Transfer-Encoding: base64 に API 提供側が対応している場合は例外になります)。そのため、文字列とバイナリデータを結合し一つの Blob にする方法は下記になります。 「バイナリデータ」、「body の開始からバイナリデータまでの文字列」、「バイナリデータ以降から終端までの文字列」の 3 グループに分ける。 3 グループをそれぞれ Base64 で符号化する。 符号化した「バイナリデータ」と「body の開始からバイナリデータまでの文字列」について、Base64 のデータパディングを示す”=”が含まれないように改行コードで調整する。 「body の開始からバイナリデータまでの文字列」、「バイナリデータ」、「バイナリデータ以降から終端までの文字列」の順で結合する。 結合した Base64 のデータを復号して、一つの Blob とする。 2. アクセストークンの有効期限はクラウドサインでコントロールされる アクセストークンやその有効期限は token API を発行した際のレスポンスとしてクラウドサインから発行されます。 発行されたレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 3600 } このレスポンスの内、expires_in の値がトークンの有効期限になります。掲題の通り、有効期限の管理はクラウドサイン側で行われ、有効期限内に再度トークンのリクエストを行った場合、経過した時間だけ expires_in の値が小さくなった結果が返ってきて、access_token などは同じ値が取得されます。有効期限内に token API を再度実行した結果が下記になります。 有効期限切れ前に token API を発行した際のレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 762 } 一方、有効期限後にトークンのリクエストを実行すると、それまでと異なるアクセストークンを取得し、新しい有効期限が設定されます。 有効期限切れ後に token API を発行した際のレスポンス例 { "access_token" : "XXXXXXYYYYYYZZZZZZ" , "token_type" : "xxxx" , "expires_in" : 3600 } そのため、API 連携が一度動いた後、有効期限ぎりぎりでもう一度 API 連携が動いてしまった場合、タイミングが悪いと契約文書の作成から最終処理である先方の送信先設定までのプロセス内のどこかから、トークンの有効期限切れが発生する可能性が想定されます。実際に期限切れが発生した場合、発生時以降に発行したその回の API 連携処理が失敗します。 トークンの有効期限切れが発生した際、API リファレンスより HTTP ステータスコードが 401 かつエラー内容が”unauthorized”で応答されることから、当社ではこのエラーを受けた場合にトークンを再取得して処理をリトライするように実装しました。 押印文書作成を例にとると下記のような実装イメージになります。 //クラウドサイン上に押印文書を作成し、作成した文書 ID を取得する public String getDocumentId ( String authToken, String title, String message){ ・・・中略・・・ HTTPResponse res = http . send (req); if ( res . getStatusCode () == 200 ){ ・・・正常に終了した際の処理・・・ } //タイミングが悪く token がタイムアウトした場合、トークンを取得し直して、リトライする else if ( res . getStatusCode () == 401 ){ //レスポンスの内容を確認するため、エラーレスポンスの中身を取得する Map < String , Object > responseBody = new Map < String , Object >(); responseBody = ( Map <String, Object>) JSON . deserializeUntyped ( res . getBody ()); String errorVal = (String) responseBody . get ( 'error' ); //リファレンス上、アクセストークンが無効(有効期限切れ)の場合、'unauthorized’となる if ( errorVal . equals ( 'unauthorized' )){ //クラウドサインのアクセストークンの再取得 authToken = getAuthToken (); //単純再帰で再実行する。 documentId = getDocumentId (authToken, title, message); } ・・・中略・・・ } ・・・以下省略・・・ } 実装を終えて 上記を実装した結果、稟議と入力内容が同じ、または、稟議から生成できる内容は全てシステム連携で自動生成するため、押印担当は稟議とクラウドサインの画面を並べて転記するような煩雑な作業を必要としない環境になりました。また、契約書の製本、郵送等の紙媒体であるが故の事務の削減ができるようになる等の、電子契約を導入することのそもそものメリットも併せて享受しています。 当社では 2021 年 4 月後半からクラウドサインを導入しましたが、2021 年 6 月時点ではすでに 月間で締結した契約書の「3 割以上」が電子契約を活用しており 、押印担当の展望として今後も利用を拡大していく予定です。 コーポレートエンジニア募集中 メドレーのコーポレート部門では、本稿のように、SaaS の導入ひとつとっても検討を尽くし、既存のシステムと有機的に結合させることで「徹底的に合理性を追求した組織基盤や、仕掛けづくり」を行っています。 面白そう!興味がある!と感じた方は、ぜひ当社採用ページからご応募お願いします! 最後までお読みいただきありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめに はじめまして、コーポレートエンジニアの山下です。 2020 年に Slack を活用した ChatOps 稟議ワークフローを内製で開発したのですが、さらに、2021 年 4 月にこの Slack 稟議と電子契約システムである クラウドサイン を連携させて、電子契約をもっと便利に使い、生産性の向上を実現しましたのでお話しいたします。 まず、当社の稟議システムは 2020 年 12 月の当社の記事 のおさらいになりますが、稟議の作業が Slack 上で完結する、 ChatOps による稟議ワークフロー となっております。本稿については 2021 年 7 月に執筆しておりますので丁度導入から 1 年程経過し、その間大きなトラブルも無く、今も当社の極めて迅速な意思決定の一助になっています。ChatOps による稟議ワークフローについては、直近、2021 年 6 月に LayerX 社が LayerX ワークフローの新機能として発表 し、サービスとしても提供され、 日経新聞 でも取り上げられていることから、今現在のパラダイムとして、先進的で有効な一手法であったと再認識しております。 今回、新型コロナウイルス感染拡大防止に伴うリモートワークの加速という状況もあり、当社で 2021 年 4 月に電子契約システムとしてクラウドサインを導入しました。電子契約に限らず、契約押印作業は稟議の後続作業に当たるため、ただ導入して使用するのみならず、クラウドサインの API を利用して稟議上にあるデータを電子契約に送信させることでシームレスな連携を実現しています。本稿では当社が行ったシームレスな連携手法について詳細をご説明いたします。 TeamSpirit とクラウドサインの API 連携について 実装概要 弊社の稟議システムである TeamSpirit とクラウドサインとの連携についてお話しします。まず、本稿の開発部分とシステム構成は下記になっております。 処理内容の詳細は後ほど述べますが、概要としては TeamSprit(Apex)からクラウドサインの API をコールし、クラウドサイン上で作成した契約文書へ稟議に記載されている契約書ファイルや先方担当者等の情報を連携する仕組みとなっております。これにより契約担当者はクラウドサインにログイン後、下記の 3 ステップで先方に送信できるようになっています。 記載内容の確認 押印・署名箇所の設定 先方への送信 クラウドサインを使用して契約文書を一から作成する場合のユーザ作業と、当社で採用した API 連携行った場合のユーザ作業を比較したものが下記の表です。作業が半分程度削減されたことが分かります。 作業項番 一から作成する場合 API 連携を利用した当社の場合 1 ログイン ログイン 2 契約文書の作成(件名、契約文書としての宛名設定等) なし 3 契約書ファイルのアップロード なし 4 先方の送信先設定 なし 5 押印欄の設定 押印欄の設定 6 先方への送信 先方への送信 実装 今回の開発で使用した クラウドサイン API は下記の 5 つの API を使用しました (※以降、クラウドサイン API に倣い、変数を表現する場合は {} で括ります)。 API 種類 使用用途 post /token アクセストークンの取得 post /document 契約文書の作成 put /documents/{documentID}/attribute 契約文書の作成で設定できない、細かい項目の設定 post /documents/{documentID}/files ファイルのアップロード post /documents/{documentID}/participants 先方の送信先設定 全体像で記載したクラウドサインの連携部について、上記の API を織り交ぜて詳細化すると下図のようになります。 実装方法としてはクラウドサイン API のリファレンスを参照し、テスト実行時に出力される curl コマンドを参考に同様のレスポンスを得るように Apex で HTTP リクエストを実装しました。アクセストークンの取得を例にとると下記のようになります。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/token' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=xxxxxxyyyyyyzzzzzz' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/token' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Content-Type' , 'application/x-www-form-urlencoded' ); req . setBody ( 'client_id=' + 'xxxxxxyyyyyyzzzzzz' ); 私自身、Apex もクラウドサイン API もこの案件を担当するまで触ったことがありませんでしたが、リクエストの試行から実装まで 2 週間かからない程度で実装することができました。 ただし、実装や運用にあたっては下記 2 点について注意が必要になります。 Apex からクラウドサインへのファイルのアップロードは単純ではない アクセストークンの有効期限はクラウドサインでコントロールされる 1. Apex からクラウドサインへのファイルのアップロードは単純ではない ファイルのアップロードについては今回使用した API の中で、唯一、テスト実行の curl と Apex のリクエスト実装で差分が生まれます。まず、その差分を確認するために curl コマンド例と Apex のリクエスト実装例で header、body にセットしている値を比較してみます。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/documents/{document_id}/files' \ -H 'accept: application/json' \ -H 'Authorization: AAAAAABBBBBBCCCCCC' \ -H 'Content-Type: multipart/form-data' \ -F 'name=テスト' \ -F 'uploadfile=@テスト.pdf;type=application/pdf' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/documents/{document_id}/files' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Authorization' , ‘AAAAAABBBBBBCCCCCC’); req . setHeader ( 'Content-Type' , 'multipart/form-data; boundary={boundary}' ); // ※1 req . setBodyAsBlob ({multipartBody}); // ※2 主な違いは ※1 , ※2 とコメントした部分になります。 Apex では HTTP リクエストの値を手で書いていくことになるので、テスト実行例のように curl がよしなに処理している部分(-F オプションの部分や Apex で記載している boundary)も実装しなければなりません。これが単純に実装できない理由になります。 boundary については multipart/form-data を送信する際に必要な境界でヘッダーでどの文字列が境界であるかを設定します。 curl の-F オプションで定義していた文字列とファイル指定部分は、Apex でファイル(バイナリ)を扱うため、その body に含まれる文字列も含めて Blob 型で扱う必要があります( Content-Transfer-Encoding: base64 に API 提供側が対応している場合は例外になります)。そのため、文字列とバイナリデータを結合し一つの Blob にする方法は下記になります。 「バイナリデータ」、「body の開始からバイナリデータまでの文字列」、「バイナリデータ以降から終端までの文字列」の 3 グループに分ける。 3 グループをそれぞれ Base64 で符号化する。 符号化した「バイナリデータ」と「body の開始からバイナリデータまでの文字列」について、Base64 のデータパディングを示す”=”が含まれないように改行コードで調整する。 「body の開始からバイナリデータまでの文字列」、「バイナリデータ」、「バイナリデータ以降から終端までの文字列」の順で結合する。 結合した Base64 のデータを復号して、一つの Blob とする。 2. アクセストークンの有効期限はクラウドサインでコントロールされる アクセストークンやその有効期限は token API を発行した際のレスポンスとしてクラウドサインから発行されます。 発行されたレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 3600 } このレスポンスの内、expires_in の値がトークンの有効期限になります。掲題の通り、有効期限の管理はクラウドサイン側で行われ、有効期限内に再度トークンのリクエストを行った場合、経過した時間だけ expires_in の値が小さくなった結果が返ってきて、access_token などは同じ値が取得されます。有効期限内に token API を再度実行した結果が下記になります。 有効期限切れ前に token API を発行した際のレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 762 } 一方、有効期限後にトークンのリクエストを実行すると、それまでと異なるアクセストークンを取得し、新しい有効期限が設定されます。 有効期限切れ後に token API を発行した際のレスポンス例 { "access_token" : "XXXXXXYYYYYYZZZZZZ" , "token_type" : "xxxx" , "expires_in" : 3600 } そのため、API 連携が一度動いた後、有効期限ぎりぎりでもう一度 API 連携が動いてしまった場合、タイミングが悪いと契約文書の作成から最終処理である先方の送信先設定までのプロセス内のどこかから、トークンの有効期限切れが発生する可能性が想定されます。実際に期限切れが発生した場合、発生時以降に発行したその回の API 連携処理が失敗します。 トークンの有効期限切れが発生した際、API リファレンスより HTTP ステータスコードが 401 かつエラー内容が”unauthorized”で応答されることから、当社ではこのエラーを受けた場合にトークンを再取得して処理をリトライするように実装しました。 押印文書作成を例にとると下記のような実装イメージになります。 //クラウドサイン上に押印文書を作成し、作成した文書 ID を取得する public String getDocumentId ( String authToken, String title, String message){ ・・・中略・・・ HTTPResponse res = http . send (req); if ( res . getStatusCode () == 200 ){ ・・・正常に終了した際の処理・・・ } //タイミングが悪く token がタイムアウトした場合、トークンを取得し直して、リトライする else if ( res . getStatusCode () == 401 ){ //レスポンスの内容を確認するため、エラーレスポンスの中身を取得する Map < String , Object > responseBody = new Map < String , Object >(); responseBody = ( Map <String, Object>) JSON . deserializeUntyped ( res . getBody ()); String errorVal = (String) responseBody . get ( 'error' ); //リファレンス上、アクセストークンが無効(有効期限切れ)の場合、'unauthorized’となる if ( errorVal . equals ( 'unauthorized' )){ //クラウドサインのアクセストークンの再取得 authToken = getAuthToken (); //単純再帰で再実行する。 documentId = getDocumentId (authToken, title, message); } ・・・中略・・・ } ・・・以下省略・・・ } 実装を終えて 上記を実装した結果、稟議と入力内容が同じ、または、稟議から生成できる内容は全てシステム連携で自動生成するため、押印担当は稟議とクラウドサインの画面を並べて転記するような煩雑な作業を必要としない環境になりました。また、契約書の製本、郵送等の紙媒体であるが故の事務の削減ができるようになる等の、電子契約を導入することのそもそものメリットも併せて享受しています。 当社では 2021 年 4 月後半からクラウドサインを導入しましたが、2021 年 6 月時点ではすでに 月間で締結した契約書の「3 割以上」が電子契約を活用しており 、押印担当の展望として今後も利用を拡大していく予定です。 コーポレートエンジニア募集中 メドレーのコーポレート部門では、本稿のように、SaaS の導入ひとつとっても検討を尽くし、既存のシステムと有機的に結合させることで「徹底的に合理性を追求した組織基盤や、仕掛けづくり」を行っています。 面白そう!興味がある!と感じた方は、ぜひ当社採用ページからご応募お願いします! 最後までお読みいただきありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp
はじめに はじめまして、コーポレートエンジニアの山下です。 2020 年に Slack を活用した ChatOps 稟議ワークフローを内製で開発したのですが、さらに、2021 年 4 月にこの Slack 稟議と電子契約システムである クラウドサイン を連携させて、電子契約をもっと便利に使い、生産性の向上を実現しましたのでお話しいたします。 まず、当社の稟議システムは 2020 年 12 月の当社の記事 のおさらいになりますが、稟議の作業が Slack 上で完結する、 ChatOps による稟議ワークフロー となっております。本稿については 2021 年 7 月に執筆しておりますので丁度導入から 1 年程経過し、その間大きなトラブルも無く、今も当社の極めて迅速な意思決定の一助になっています。ChatOps による稟議ワークフローについては、直近、2021 年 6 月に LayerX 社が LayerX ワークフローの新機能として発表 し、サービスとしても提供され、 日経新聞 でも取り上げられていることから、今現在のパラダイムとして、先進的で有効な一手法であったと再認識しております。 今回、新型コロナウイルス感染拡大防止に伴うリモートワークの加速という状況もあり、当社で 2021 年 4 月に電子契約システムとしてクラウドサインを導入しました。電子契約に限らず、契約押印作業は稟議の後続作業に当たるため、ただ導入して使用するのみならず、クラウドサインの API を利用して稟議上にあるデータを電子契約に送信させることでシームレスな連携を実現しています。本稿では当社が行ったシームレスな連携手法について詳細をご説明いたします。 TeamSpirit とクラウドサインの API 連携について 実装概要 弊社の稟議システムである TeamSpirit とクラウドサインとの連携についてお話しします。まず、本稿の開発部分とシステム構成は下記になっております。 処理内容の詳細は後ほど述べますが、概要としては TeamSprit(Apex)からクラウドサインの API をコールし、クラウドサイン上で作成した契約文書へ稟議に記載されている契約書ファイルや先方担当者等の情報を連携する仕組みとなっております。これにより契約担当者はクラウドサインにログイン後、下記の 3 ステップで先方に送信できるようになっています。 記載内容の確認 押印・署名箇所の設定 先方への送信 クラウドサインを使用して契約文書を一から作成する場合のユーザ作業と、当社で採用した API 連携行った場合のユーザ作業を比較したものが下記の表です。作業が半分程度削減されたことが分かります。 作業項番 一から作成する場合 API 連携を利用した当社の場合 1 ログイン ログイン 2 契約文書の作成(件名、契約文書としての宛名設定等) なし 3 契約書ファイルのアップロード なし 4 先方の送信先設定 なし 5 押印欄の設定 押印欄の設定 6 先方への送信 先方への送信 実装 今回の開発で使用した クラウドサイン API は下記の 5 つの API を使用しました (※以降、クラウドサイン API に倣い、変数を表現する場合は {} で括ります)。 API 種類 使用用途 post /token アクセストークンの取得 post /document 契約文書の作成 put /documents/{documentID}/attribute 契約文書の作成で設定できない、細かい項目の設定 post /documents/{documentID}/files ファイルのアップロード post /documents/{documentID}/participants 先方の送信先設定 全体像で記載したクラウドサインの連携部について、上記の API を織り交ぜて詳細化すると下図のようになります。 実装方法としてはクラウドサイン API のリファレンスを参照し、テスト実行時に出力される curl コマンドを参考に同様のレスポンスを得るように Apex で HTTP リクエストを実装しました。アクセストークンの取得を例にとると下記のようになります。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/token' \ -H 'accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'client_id=xxxxxxyyyyyyzzzzzz' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/token' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Content-Type' , 'application/x-www-form-urlencoded' ); req . setBody ( 'client_id=' + 'xxxxxxyyyyyyzzzzzz' ); 私自身、Apex もクラウドサイン API もこの案件を担当するまで触ったことがありませんでしたが、リクエストの試行から実装まで 2 週間かからない程度で実装することができました。 ただし、実装や運用にあたっては下記 2 点について注意が必要になります。 Apex からクラウドサインへのファイルのアップロードは単純ではない アクセストークンの有効期限はクラウドサインでコントロールされる 1. Apex からクラウドサインへのファイルのアップロードは単純ではない ファイルのアップロードについては今回使用した API の中で、唯一、テスト実行の curl と Apex のリクエスト実装で差分が生まれます。まず、その差分を確認するために curl コマンド例と Apex のリクエスト実装例で header、body にセットしている値を比較してみます。 API リファレンスでの curl コマンド例 curl -X 'POST' \ 'https://api.cloudsign.jp/documents/{document_id}/files' \ -H 'accept: application/json' \ -H 'Authorization: AAAAAABBBBBBCCCCCC' \ -H 'Content-Type: multipart/form-data' \ -F 'name=テスト' \ -F 'uploadfile=@テスト.pdf;type=application/pdf' Apex でのリクエスト実装例 HttpRequest req = new HttpRequest (); req . setMethod ( 'POST' ); req . setEndpoint ( 'https://api.cloudsign.jp/documents/{document_id}/files' ); req . setHeader ( 'accept' , 'application/json' ); req . setHeader ( 'Authorization' , ‘AAAAAABBBBBBCCCCCC’); req . setHeader ( 'Content-Type' , 'multipart/form-data; boundary={boundary}' ); // ※1 req . setBodyAsBlob ({multipartBody}); // ※2 主な違いは ※1 , ※2 とコメントした部分になります。 Apex では HTTP リクエストの値を手で書いていくことになるので、テスト実行例のように curl がよしなに処理している部分(-F オプションの部分や Apex で記載している boundary)も実装しなければなりません。これが単純に実装できない理由になります。 boundary については multipart/form-data を送信する際に必要な境界でヘッダーでどの文字列が境界であるかを設定します。 curl の-F オプションで定義していた文字列とファイル指定部分は、Apex でファイル(バイナリ)を扱うため、その body に含まれる文字列も含めて Blob 型で扱う必要があります( Content-Transfer-Encoding: base64 に API 提供側が対応している場合は例外になります)。そのため、文字列とバイナリデータを結合し一つの Blob にする方法は下記になります。 「バイナリデータ」、「body の開始からバイナリデータまでの文字列」、「バイナリデータ以降から終端までの文字列」の 3 グループに分ける。 3 グループをそれぞれ Base64 で符号化する。 符号化した「バイナリデータ」と「body の開始からバイナリデータまでの文字列」について、Base64 のデータパディングを示す”=”が含まれないように改行コードで調整する。 「body の開始からバイナリデータまでの文字列」、「バイナリデータ」、「バイナリデータ以降から終端までの文字列」の順で結合する。 結合した Base64 のデータを復号して、一つの Blob とする。 2. アクセストークンの有効期限はクラウドサインでコントロールされる アクセストークンやその有効期限は token API を発行した際のレスポンスとしてクラウドサインから発行されます。 発行されたレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 3600 } このレスポンスの内、expires_in の値がトークンの有効期限になります。掲題の通り、有効期限の管理はクラウドサイン側で行われ、有効期限内に再度トークンのリクエストを行った場合、経過した時間だけ expires_in の値が小さくなった結果が返ってきて、access_token などは同じ値が取得されます。有効期限内に token API を再度実行した結果が下記になります。 有効期限切れ前に token API を発行した際のレスポンス例 { "access_token" : "AAAAAABBBBBBCCCCCC" , "token_type" : “xxxx” , "expires_in" : 762 } 一方、有効期限後にトークンのリクエストを実行すると、それまでと異なるアクセストークンを取得し、新しい有効期限が設定されます。 有効期限切れ後に token API を発行した際のレスポンス例 { "access_token" : "XXXXXXYYYYYYZZZZZZ" , "token_type" : "xxxx" , "expires_in" : 3600 } そのため、API 連携が一度動いた後、有効期限ぎりぎりでもう一度 API 連携が動いてしまった場合、タイミングが悪いと契約文書の作成から最終処理である先方の送信先設定までのプロセス内のどこかから、トークンの有効期限切れが発生する可能性が想定されます。実際に期限切れが発生した場合、発生時以降に発行したその回の API 連携処理が失敗します。 トークンの有効期限切れが発生した際、API リファレンスより HTTP ステータスコードが 401 かつエラー内容が”unauthorized”で応答されることから、当社ではこのエラーを受けた場合にトークンを再取得して処理をリトライするように実装しました。 押印文書作成を例にとると下記のような実装イメージになります。 //クラウドサイン上に押印文書を作成し、作成した文書 ID を取得する public String getDocumentId ( String authToken, String title, String message){ ・・・中略・・・ HTTPResponse res = http . send (req); if ( res . getStatusCode () == 200 ){ ・・・正常に終了した際の処理・・・ } //タイミングが悪く token がタイムアウトした場合、トークンを取得し直して、リトライする else if ( res . getStatusCode () == 401 ){ //レスポンスの内容を確認するため、エラーレスポンスの中身を取得する Map < String , Object > responseBody = new Map < String , Object >(); responseBody = ( Map <String, Object>) JSON . deserializeUntyped ( res . getBody ()); String errorVal = (String) responseBody . get ( 'error' ); //リファレンス上、アクセストークンが無効(有効期限切れ)の場合、'unauthorized’となる if ( errorVal . equals ( 'unauthorized' )){ //クラウドサインのアクセストークンの再取得 authToken = getAuthToken (); //単純再帰で再実行する。 documentId = getDocumentId (authToken, title, message); } ・・・中略・・・ } ・・・以下省略・・・ } 実装を終えて 上記を実装した結果、稟議と入力内容が同じ、または、稟議から生成できる内容は全てシステム連携で自動生成するため、押印担当は稟議とクラウドサインの画面を並べて転記するような煩雑な作業を必要としない環境になりました。また、契約書の製本、郵送等の紙媒体であるが故の事務の削減ができるようになる等の、電子契約を導入することのそもそものメリットも併せて享受しています。 当社では 2021 年 4 月後半からクラウドサインを導入しましたが、2021 年 6 月時点ではすでに 月間で締結した契約書の「3 割以上」が電子契約を活用しており 、押印担当の展望として今後も利用を拡大していく予定です。 コーポレートエンジニア募集中 メドレーのコーポレート部門では、本稿のように、SaaS の導入ひとつとっても検討を尽くし、既存のシステムと有機的に結合させることで「徹底的に合理性を追求した組織基盤や、仕掛けづくり」を行っています。 面白そう!興味がある!と感じた方は、ぜひ当社採用ページからご応募お願いします! 最後までお読みいただきありがとうございました。 募集の一覧 | 株式会社メドレー メドレーの採用情報はこちらからご確認ください。 www.medley.jp