Articles tagged with: キントーン

Cybozu Days 2022のブース出展でkintoneにメディアプレーヤーを設置しました。


kintone Advent Calendar 2022の20日目の記事です。

  Topへ↓

今年もこの季節がやってまいりました。Advent Calendarです。
11月のkintone月間が終わると、間髪を入れずに師走のAdvent Calendarです。
そのkintone月間に行われたのがCybozu Days 2022でした。弊社は3年連続のブース出展をしました。

弊社メンバーが書いた振り返りブログはこちら。
Cybozu Days 2022の今年のテーマはDX(デジタルトランスフォーメーション)でした。
Cybozu Days 2022を終えて[ブースで交流]
Cybozu Days 2022を終えて[出展までの準備]
Cybozu Days 2022を終えて[まとめ]

今年の弊社ブースは、昨年に続いてIoTとkintoneの連携をテーマに打ち出しました。
しかも今年はSORACOMさんのデバイスを中心に据え、さらに業種を絞ってみました。農業!

今年の弊社ブースは全部で五つの出展物を出していました。他の四つは、SORACOM UGではおなじみの前嶋さん(@anysonica)にご協力をお願いしました。
前嶋さんはSORACOM UGの今年のMVPにも選ばれたすごい方です。前嶋さんにお手伝いいただいた四つの出展内容については前嶋さんのブログをご覧いただくのが良さげです。

本稿ではソラカメの動画をkintoneの画面に出す。つまり、メディアプレーヤーの出し方に絞っています。
その実装はシンボ技研の山崎さん(@ryoyamazakiwork)にお願いしました!


これはCybozu Days 2022の弊社ブースの一コマです。動くソラカメの動画がkintoneに映る様子を興味深げに見ているのは瀧村さん(@kinbozuu)のお子さんたちです。とてもかわいらしかった。


2.完成したコード

  Topへ↑

実は、この出展で苦労したのは、ソラカメの動画を連携させる部分ではなく、動画をkintone内でストリーミング再生を行う実装です。そっちのほうが苦労しました。
本稿ではその部分を取り上げてみようと思います。
ただし、苦労したといっても、結果として落ち着いたコードのステップ数はとてもシンプルです。

たったこれだけ。

ただし、以下のライブラリをCDNから呼んでいます。
https://cdn.dashjs.org/latest/dash.all.min.js


3.コードの説明

  Topへ↑

上に挙げたコードを要約すると、
スペースフィールドに対してinnerHTMLでhtmlとして描画し、videoタグで動画を表示する領域を確保しています。あとはdashjsに備わっているオブジェクトやメソッドを呼び出します。
たったこれだけです。

    kintone.app.record.getSpaceElement('dash').innerHTML =
     '<video data-dashjs-player="" autoplay="" width="800" height="600" src=" + url + " controls="true"> ';

続いて読み込んであるdashjsのMediaPlayerオブジェクトのcreateメソッドを呼びます。

 const player = dashjs.MediaPlayer().create();

すると、ストリーミングプレーヤーが使えるように内部で設定されます。
あとはinitializeメソッドで描画するvideoタグや対象となるurlを呼び出せば、ストリーミングが呼び出せます。

 player.initialize(video, url, false);

この時、video要素はdocument.querySelectorを使って変数に格納しておきましょう。

 const video = document.querySelector('video');

すると、こんな感じでkintone上でメディアプレーヤーが使えるようになるのです。

4.dashjsの説明

  Topへ↑

このdashjsはメディアプレーヤーとしての機能をほぼ備えています。たとえば早送りや巻き戻し、テキストの埋め込みなど。
このdashjsを使えば、簡単にkintoneの各項目の値を使ったさまざまな動画へのアプローチが可能になると思われます。
例えば、IoTデバイスで得た動画を基に、動的にkintoneでアクションをつなげることができます。
また、オンラインミーティングをしながら、画面上でkintoneの画面から動的にアクションを起こすこともできるはずです。

dashjsのライブラリの中身やドキュメントは
https://github.com/Dash-Industry-Forum/dash.js
をご覧いただくことをお勧めします。

また、dashjsはBSDライセンスなので、そのライセンスに従っていればほぼ自由に使えます。
kintone内に組み込むことも自由。

5.kintoneでメディアを再生できればさらなる可能性が!

  Topへ↑

動画を起点としたアクション。それはkintoneに限らず、まだ他のシステムやプラットフォームでもそれほど盛んではないようです。(長井意見)
ということは、kintone上で動画を使った業務改善の手法が確立できれば、新たな業務改善の手法として脚光を浴びるかもしれません。
例えば作業マニュアルの動画をkintoneにアップし、その動画内のテキストテロップでコメントを入れる機能をkintone内で行ってしまうなど。
それは、kintoneの業務改善ツールとしての優位性を示す機会になりそうです。

Cybozu Days 2022では弊社にさまざまな可能性を開いてくれました。
IoTとkintoneの可能性。さらには、動画とkintoneのコラボレーションにおいても、
この記事では動画を撮影するためのデバイスにSORACOM社のソラカメを使っています。
ソラカメで撮った動画をkintoneに表示する実装。実はこの実装はすでに案件として進んでおり、来年度に着手することになりそうです。

そのソラカメから動画を取得し、kintoneにUrlとして連携する部分。
その実装については、明日アップする予定のSORACOM Advent Calendar 2022の記事内で書く予定です。
あわせてご覧いただければと思います。


弊社のアドベントカレンダー歴


弊社代表の長井が参加してきたアドベントカレンダーもだいぶ年数がたちました。
何を書いたか忘れつつあるので、ここに残しておきます。

2022年
Adventar 「すごくない」kintone Advent Calendar 2022
・kintoneの機能をコミュニティに例えてみた
Qiita kintone Advent Calendar 2022
・Cybozu Days 2022のブース出展でkintoneにメディアプレーヤーを設置しました。
Qiita SORACOM Advent Calendar 2022
・Cybozu Days 2022のブース出展でソラカメとkintoneの連携にチャレンジしました。


2021年
Qiita kintone Advent Calendar 2021
・私、Mac方面については素人なのですが、、、


2020年
Qiita kintone Advent Calendar 2020
・kintoneにシステム移したいんや
Qiita kintone 2 Advent Calendar 2020
・パート2は痛恨の欠席。
Adventar カレンダーPlus Advent Calendar 2020
・Cybozu OfficeのスケジュールをCalendar Plusで(だいぶ)再現!


2019年
Qiita kintone Advent Calendar 2019
・ワーキングツリーにはkintoneとboxをお飾り!
Qiita kintone 2 Advent Calendar 2019
・コーチングのグラフってkintoneで出せるんやって!


2018年
Qiita kintone Advent Calendar 2018
・ライフログのkintone盛り alasql仕込みのGoogle Chart添え


2017年
Qiita kintone Advent Calendar 2017
・Save The Live!~サイボウズLiveを救え~


2016年
Qiita kintone Advent Calendar 2016
・kintoneとWordPressの連携ネタ三題噺


2015年
Qiita kintone Advent Calendar 2015
・年賀状の宛名書きをkintoneにお願いする。WordとAccessを和えて


2022年11月のまとめ(法人)


令和四年十一月。

世界の動きとは切り離されているかのように、弊社の忙しさは増す一方です。私たちの生活は円安によって脅かされつつあり、日本の低落傾向は悲観的な観測を呼んでいるというのに。

弊社に案件の引き合いが次々とやってくる今、私に時局を論じる時間はありません。今やるべきことをやらねば。
私たちに案件が来る。それはつまり、ここ数十年の日本の低落傾向からの復活の兆しであると希望を持ちながら。
弊社にはありがたいことに風が吹いています。が、その風の強さに翻弄され始めています。うまく操らなければ、スピードを制御できずに陸に激突してしまいます。
弊社には、まだまだ課題が山積みです。

昨年の反省を真摯に踏まえ、弊社がこれからも生き延びるにはどうすれば。
外から見ると弊社には案件が次々と来ており、順調に見えるのでしょう。ですが、油断してはなりません。案件が積み重なっており、見積もりの作成すら遅れ気味になっています。

達成度5割。達成感6割。満足感8割。それが今月の代表自身の自己採点です。

今月は今までにない手応えを感じるとともに、体制構築にあたっての光が見えてきました。着実に前に進みたいと思います。
弊社とご縁をいただいたすべての方々に感謝します。ありがとうございました。


●弊社の業績

§ 総括 十一月度の売上は目標額をぎりぎり超えました。最低限の目標ですが。

今月は検収が可能な案件がなかったからです。とはいえ、来月にまとめて複数案件の検収を控えており、そこは大丈夫です。すでに今年度の売り上げは前年度を突破していますし。

弊社メンバーもがんばってくれています。今の体制の中、少しずつメンバーの関係性は定着しつつあります。
ただし、社内の仕組みづくりには課題がまだ残っています。私も率先して範を示さねば。

メンバーの開発スキルは確実に上がっています。が、それ以上に案件の量が急激に増えています。
今期の売り上げの予想額も算出しました。計画の中にはパートナー技術者にも関わってもらう方向で計画を進めています。
弊社メンバーも必ず案件をやり遂げられるようなスキルを身に付けてもらいながら。

今月は弊社にとって以下のようなトピックがありました。
・Cybozu Days 2022でブースを出展しました。
・Cybozu Days後のコミュニティ打ち上げで代表が締めのあいさつを担当しました。
・スナックジョイゾー @オフライン vol.9 〜デイズナイト〜で代表がLT登壇しました。
・埼玉DXパートナーサイトに弊社の動画がアップされました。
・法政大学アメリカンフットボール部さんと一緒に登壇した人チーム応援カフェの動画が公開されました。


実績を出しつつ、日常も充実させる。ワークライフバランスの軸は堅持しつつ、成長もさせていきます。引き続きよろしくお願いいたします。

§ 業務パートナー 今月もまた、複数の企業様との間で機密保持契約を締結しました。

そして、何件もの新たな関係が生まれた今月でした。
そうした企業様との関係に依存せず、同時にお互いにとって良い関係を維持する。

この夏から、経理や財務諸表の見直しを行い、五年間の長期計画を考え、経費としての外注費をどう考えるかも検討しました。
外注に依存することはしません。が、弊社内だけでは案件がこなしきれない。そのバランスをとりながら、経営していきます。

今月もCybozu Days 2022の自社ブースの展示において、約四社の方との共同出展ができました。
来年も手伝って下さるとのことで、誠にありがたいことです。

なお、まだ詳細は明かせませんが、そこから次のステップに繋がる準備を始めています。今月から。

リモート体制でも指示がきちんと伝えられるよう、ツールの整備には引き続き取り組んでいきます。

§ 開発案件 今月は九割の開発案件がkintoneがらみでした。
ありがたいことに今までにいただいた多数の案件の実装作業が続いています。それに並行して、週一、二件の頻度で新たな案件のお話をいただけています。本当に感謝します。今月も複数の大きな案件の引き合いや、案件の開始が決まりました。正直案件が来すぎてかなりいっぱいいっぱいになってきています。
来月からも複数の大きな案件が始まります。

今、動いている実装案件が30件弱。他にも二ダースほど準備が進んでいる案件があります。それらをどのようにこなしていくか。それが今の課題です。
今、弊社が取り扱っているシステムはkintoneをベースにGmail、Google Calendar、Zoom、HubSpot、ChatWork、Slack、freee、freeeサイン、マネーフォワード、スマレジ、LINE、Benchmark、box、Stripe、サブスクペイ、AWS、各種CMS、その他です。
案件によってはパートナー社や技術者さんと一緒に構築し、私や弊社の技術顧問の方も実装に取り組みながら日々の開発に取り組んでいます。

来年の春に社内の体制を構築し直す予定です。
その時、代表は実装から手を引く予定です。もちろん、コーディングや設計を一切やらないという意味ではありません。今までとは違う種類のkintoneとの連携サービスや、新たな接続方式が求められる場合は率先して調査や作業を行わねばならないでしょう。新たな概念や実装についてはきちんと追わなければ経営の判断を誤るので。
ですが、弊社で実績のあるkintoneとそれ以外のSaaSとの連携や、kintoneアプリ間の連携やアプリ構築はメンバーに任せ、代表は手をひきます。
代表がkintoneを触るとすれば、商談の場においてkintoneの魅力を伝える際や、セミナーで登壇した壇上においてでしょうか。

経営者がどの案件にも手を突っ込み、ワーカーとして奔走しているようでは社の発展は望めません。
長期計画やビジョンを決め、真剣に経営に取り組みを始めた今だからこそ、経営にもリソースを割かねばなりません。代表が経営に時間を割けなくなると大所が見えなくなります。
弊社を発展させるためにも、なるべく代表が手を動かさずに案件を回す体制を作ります。
そして、トップセールスのための交流に時間を割かなければ。

来月も、引き続き弊社メンバーの教育も兼ねた案件に注力していきながら、体制構築に邁進します。


§ 財務基盤の堅牢化 財務をきっちりすること。前からの課題です。
弊社としては問題ないのに、家計が絡むととたんに脆弱になる。
この点は弊社の長年の課題でした。

また、東京都中小企業振興公社の人財アドバイザーの方に昨年末よりアドバイスを受けています。この方から五年後の弊社の目指すべき姿を作るように言われ、SWOT分析をはじめ、売上や経費の計画をきちんと作成しました。税理士の先生にも手伝っていただきながら。

経費もほぼ毎月計算しています。
長い間、弊社代表が苦手意識を持ち、遠ざけていたこれらの作業。これらの作業から目を背けては経営に取り組めない。そのように覚悟を決めました。

§ 社内体制 昨年の師走に、社是、企業理念、経営理念やスローガンを見直しました。メンバーが離任した理由に、肝心な部分の価値観のずれがあったためです。そこで年始にあたり、三人でもう一度忌憚のない意見を交わしながら、それらを練り直しました。以下に掲示します。

企業理念
「情報技術を生かして、
正直に、飾らずに、自分、家族、パートナー、お客様、地域に寄り添う」

経営理念
「一期一会の儲けよりお互いが継続して協業できる幸せを」

9つ(ナイン)の「ない」
「組織図はない」
「タイムカードない」
「ノルマは設けない」
「多数決で決めない」
「社長室は作らない」
「肩書きもいらない」
「皆が経営者の行い」
「定年は強制しない」
「雑談は惜しみない」

アクアビットに合う方
「家族を大切にする気持ちのある方」
「仲間を大切にできる方」
「笑顔のある方」
「まず肯定から入る方」
「夢を持ち続ける方」
「人の話を聴ける方」
「人間が好きな方」
「可能性を信じる方」
「自分が好きな方」
「自分で仕事を見つける方」
「会社に滅私奉公せず公私を大切にする方」

メンバーの募集を出した時と考え方の軸はぶれていません。

昨年の師走より、東京都中小企業振興公社の方に人財制度についてのアドバイスを受けています。
そのアドバイザーの方から作成するようにと指示された、5年後の弊社を見据えたビジョンや課題の解決策は、上記の各理念をより細かく詰めたものです。文章や財務上の数値、目標の形で具体的に表しています。
それらはメンバーにも見せ、説明もしています。また、メンバーからは課題も提出してもらいました。

5年後に向けた体制構築に向け、まずは代表が実装作業から手を引くことに決めました。営業とブランディングと総務その他の経営に注力します。
来年の春からは、メンバー各自にさまざまな業務の役割を持ってもらいます。各人に役割を持ってもらいながら、案件については教え合い、助け合いながら実装を進められるような体制を目指します。そして、Cybozu Daysのようなイベントでは役割を超えて協力するようなチームを目指します。

既に案件の増加数が弊社の体制の許容量を超過しており、このままだと破綻してしまいます。今年いっぱいは採用には手を出さないつもりでしたが、新たな採用に踏み切ります。

それと同時に教育について、ソース管理について、より効率的で堅牢な仕組みづくりを進めていきます。

リモートで出来るような案件の量でなくなりつつあり、新人メンバーに教える事は多いため、来年の春には拠点の開設も検討しています。

§ 人脈の構築 今月は外出や打ち合わせを何度も行い、リアル商談を数多く行いました。
お客様とのリアル商談は約8回です。オンライン商談はざっと数えたところ約37回ほどです。
今月、頂戴した名刺は104枚です。


今月はCybozu Days 2022の出展という弊社にとって大きなイベントがありました。30日の夜は某所でコミュニティー界隈の人々とCybozu Days 2022の打ち上げを行いました。もちろん弊社でも1日目の夜に飲み会を行い、皆さんをねぎらいました。
他にもスナックジョイゾー@オフライン vol.9 〜デイズナイト〜にも参加し、楽しい交流のひとときを過ごしました。
今年のCybozu Days 2022では昨年一回だけお願いされたサインを三回もお願いされました。私もサイボウズ商店で購入したジョイゾー四宮さんの著書をもってジョイゾーブースに行き、四宮さんにサインしていただきました。

日本中から来た人々とリアルで交流深める。まさにそれがCybozu Daysの素晴らしさです。11月は毎年kintone界隈で多様なイベントが催され、ご縁がつながります。まさに今年もそうでした。
これらを通じてまた面白いお仕事や次の展開につながっていきそうです。

皆さんとの熱いつながりが次の案件につながり、お互いが望むビジネスと人生につなげる。それこそ、まさに弊社が望むあり方です。

引き続きよろしくお願いいたします。

§  対外活動 今月はこちらのイベントに参加しました。Cybozu Days 2022(11/10)(11/11)。 コミュニティー界隈打ち上げ(11/11)。スナックジョイゾー@オフライン vol.9 デイズナイト(11/18)。kintone Café JAPAN 2022(11/19)。法政大学アメリカンフットボール部ORANGE vs 早稲田大学 BIG BEARS(11/23)。

今月は、Cybozu Daysを通してより広く弊社の名前が認知されたように思います。私も二箇所ほどで人前で話をする機会がありました。Cybozu Daysの展示ブースの前でもずっと喋っていました。
一方、今月はさらに忙しくなりました。それによって同時に並行する案件の数が私の処理能力を超えて溢れました。そのため月の後半は、イベントにもあまり出ずに、作業や打ち合わせや商談の日々でした。

来月以降、いろいろと予定が控えています。
今月はあっという間に突破した喜ばしい月でした。来月は今月にも増して忙しいかもしれません。ですが、何とか時間を捻出して対外活動にも精を出したいと思っています。


弊社のお仕事はこうした対外活動から生まれています。
代表自身による新たな交流を発信することは絶対に怠ってはならないと肝に銘じています。

コロナが小康状態になり、リアルのイベントも増えつつあります。また皆様と交流を深めたいと思っています。
まずは今月の弊社と関わっていただいた皆様、誠にありがとうございました。


§ 執筆活動 以前に連載していたCarry Meさんが運用する本音採用サイトの「アクアビット 航海記」の続きを弊社サイトにアップする作業ですが、今月はアップできませんでした。

今月、書いた本のレビューは0本()
今月、書いた映画のレビューは0本()。
今月、書いた抱負は0本() 。
今月、書いた旅日記は0本() 。
今月、書いた「物申す」は0本() 。
今月、書いた弊社の活動ブログは5本(
Cybozu Days 2022の今年のテーマはDX(デジタルトランスフォーメーション)でした。
Cybozu Days 2022を終えて[ブースで交流]
Cybozu Days 2022を終えて[出展までの準備]
スナックジョイゾーにて妻が覚醒~
Cybozu Days 2022を終えて[まとめ]
) 。
今月、書いた弊社の技術ブログは0本()。

なお、4月から毎営業日にnoteに書き始めた記事ですが、今月は以下の内容をアップしました。

10月28日 10月28日 Twitterはどこに向かうのか
10月31日 10月31日 プラスになる場合のみ集まる
11月2日 11月2日 マイナス×マイナスはプラス
11月4日 11月4日 長時間労働は自ら率先が前提
11月5日 11月5日 Cybozu Days 2022はすぐ
11月7日 11月7日 学ぶ事は有事の際に力となる
11月8日 11月8日 Cybozu Days 2022まで走
11月9日 11月9日 お仕事の服装を考えてみる
11月10日 11月10日 Cybozu Daysの開始所感
11月11日 11月11日 Cybozu Daysの出展効果
11月14日 11月14日 Cybozu Daysの振り返り1
11月15日 11月15日 ビジネスのご縁の連絡手段
11月16日 11月16日 kintoneに対し客観視する
11月17日 11月17日 会話に集中出来ているか?
11月18日 11月18日 Cybozu Daysの振り返り2
11月21日 11月21日 やり続けると見える景色の事
11月22日 11月22日 アドベントカレンダーの勧め
11月24日 11月24日 チーム力がなぜ必要なのか
11月25日 11月25日 お客様への聞取り出来てる?

§ 年表

・十一月お仕事

二俣川で作業・商談×4、キンコーズ 横浜駅西口店、大和市文化創造拠点シリウスで作業、タックルステーキで打ち合わせ、小野路公園グラウンドで視察・作業、水道橋で商談×2、幕張メッセでCybozu Days 2022×2、山傳丸で打ち上げ、HUBで打ち上げ二次会、アパホテル&リゾート東京ベイ幕張で宿泊×2、クラフトマーケット 海浜幕張店で打ち上げ、和風ダイニング季作で打ち上げ二次会、銀座で打ち合わせ、TOKYOテレワーク・モデルオフィス府中で作業、スナックジョイゾー参加、桜新町で商談、桜新町駅Tully’s Coffeeで作業、神田でオフィス移転立ち合い、神田で商談×2、アクアビット サテライトオフィス×1

§ ツイートまとめ
・十一月ツイート

https://togetter.com/li/1980010


2022年8月のまとめ(法人)


令和四年八月。

コロナウイルスが世を乱して早二年半。次々と新たな株が出現し、日本は今や、世界最多の感染者数になるほどの猛威をふるっています。続いて第八波のケンタウロスと呼ばれる株も来ているようです。
もう、今後コロナがなくなることはないでしょう。膨大な死者は出ていません。が、連日数百人の死者が出ているコロナをただの風邪だと軽んじることは軽率です。が、過度に恐れて経済を壊すには今の日本はあまりにも脆弱です。
来月早々にkintone Café 神奈川 Vol.13 & SORACOM UG 東京のイベントを予定しています。一時はオンライン開催を覚悟しましたが、リアル開催を決行します。

一方、ウクライナへのロシア侵攻は泥沼化しており、台湾海峡をはさんでの中華人民共和国と中華民国(台湾)の緊張も予断を許しません。
世界では干ばつや洪水がかつてない規模で頻発し、地球温暖化の影響は顕著に人類を脅かしています。
未来は不透明。これからどうなるか分からない。そんな激動の日々です。

そのような世情の中、仕事のご依頼が途切れずにいただけていることに感謝します。
が、自社だけの成長を追い求めても、社会が滅びてしまっては何にもなりません。社会へ貢献するためにやるべきことは多いのです。
常に外の世界に目を向け、現状に安住しないよう、常に気を引きしめなければ。

弊社もセキュリティ対策やBCP対策は考え始めています。それと同時に生産性を上げ、新たな技術や考え方を常に取り入れていかなければなりません。

昨年の反省を真摯に踏まえ、弊社は未来に生き延びていけるよう、日々努力と試行錯誤を続けています。
が、油断してはなりません。案件が積み重なっており、それが逆にリスクにもなっています。

達成度5割。達成感6割。満足感6割。それが今月の代表自身の自己採点です。

今の好況に甘んじず、足元を固めます。危機感を持ち、油断せず。
弊社とご縁をいただいたすべての方々に感謝します。ありがとうございました。

●弊社の業績

§ 総括 八月度の売上も目標額を超えることができました。

また、今月もいくつかの引き合いを頂きました。本当にありがたいことです。
弊社メンバーもがんばってくれています。四人体制で乗り切れるかは、これからの頑張りです。

あるお客様で四月から取り組み始めた半常駐の案件もメンバーが頑張ってくれています。そのメンバーは別の複数の案件も頑張ってくれています。
今月はついにkintoneアソシエイト試験にも受かってくれました。これで弊社常勤メンバーでアソシエイトを持っているのは代表を含めて三人です。

今月は一件、大きな案件で提示していた見積が失注してしまいました。さらに途中で終了してしまった案件が一件ありました。特に後者は弊社のミスであり、反省点は多々あります。
ですが、代わりに別案件の引き合いを頂いたため、今期の売上目標は過去最高を見込めています。あとは利益を出さなければなりません。これもきちんと経理をチェックしており、黒字を達成できそうです。

弊社のこれからをどうしたいのか。ただ案件をこなすだけでなく、どういう会社に向かいたいのか。メンバーでも打ち合わせを行いました。
kintoneだけで回せるような案件はどちらかというと小規模です。弊社としては、大きな案件も引き受けていかなければなりません。
弊社の価値とはkintoneを他のPaaS/SaaSと連携させることにあります。その一つの到達点として、弊社独自のサービスを立ち上げる事を着想しました。
それが実り、春先にスマレジアプリコンテストで佳作をいただきました。
ですが、なかなかそれがサービスとして結び付けられていません。複数の案件を並行してこなすことに精一杯です。

今月は弊社にとって以下のようなトピックがありました。
・弊社メンバーがkintone アソシエイト試験に合格(8/8)し、合格までのロードマップ記事をアップしてくれました(8/30)。
・サイボウズパートナーネットワーク上の弊社ページが更改されました(8/15)。
・代表宛にkintoneエバンジェリストパーカーを届けていただきました(8/18)。
・代表がサイボウズOfficeのコミュニティ「オフコミ」に参加しました(8/18)。
・代表がサイボウズユーザーフェスティバルのkintoneコミュニティ紹介動画に登場しました(8/18)。
・代表がkintoneカイゼンマネジメントエキスパート試験を受験しました(合否発表は9月)(8/20)。
・弊社がSORACOM社の登録SPSパートナーに登録されました(8/4)。
・Road社と業務パートナーを締結しました(8/10)。
・Yoom株式会社と紹介パートナー・販売パートナー・OEMパートナーの契約を締結しました(8/28)(8/30)。
・東京テレワーク・モデルオフィスに応募し、多摩地区の三拠点のテレワークオフィスが使えるようになりました。
・11月10-11に幕張メッセで行われるCybozu Days 2022の出展を公表しました。

実績を出しつつ、日常も充実させるための工夫。それらは全て、代表の求めるワークライフバランスの実現につながってゆくと信じています。引き続きよろしくお願いいたします。

§ 業務パートナー 今月は、先月のM-SOLUTIONS様に続き、複数の会社様とパートナー契約を結ぶことができました。SORACOM社様とは登録パートナー。Road様とは業務パートナー。Yoom社様との間では紹介・販売・OEMパートナー契約を結びました。

先々月に経理や財務諸表の見直しを行いました。そして、五年間の長期計画を策定しました。その中で経費として外注費をどう考えるかも検討を重ねました。
かつて、外注費をSES業界の割合で設定した失敗。それを繰り返さぬよう、パートナーさんとの外注費を見直しています。同時に業務の丸投げはしないように戒めました。

リモート体制でも指示がきちんと伝えられるよう、ツールの整備には引き続き取り組んでいきます。

§ 開発案件 今月は九割の開発案件がkintoneがらみでした。
ありがたいことに2,3,4月に頂いたたくさんの案件の実装で忙しい日々を過ごしています。今月もその実装作業や準備に注力しました。今も週一、二件の頻度で新たな案件のお話をいただけており、感謝しています。

今、動いている案件が20件強。他にも10件ほど準備が進んでいる案件があります。それらをどのようにこなしていくか。それが今の課題です。
今、弊社が取り扱っているシステムはkintoneをベースにGmail、Google Calendar、Zoom、HubSpot、ChatWork、Slack、freee、freeeサイン、マネーフォワード、スマレジ、LINE、Benchmark、box、Stripe、サブスクペイ、AWS、各種CMS、その他です。
今はIoTにも力を入れようと、SORACOM社、obniz社のデバイスとつなぐことを検討しています。
案件によってはパートナー社や技術者さんと一緒に構築し、私や弊社の技術顧問の方も実装に取り組みながら日々の開発に取り組んでいます。

それらの開発スキルを上げるため、言語のプログラミング効率を上げるためにpaizaを試しています。また言語に頼らぬノーコード連携を実現するため、Zapier、IFTTT、Yoomなどをつなぐ開発にも注力しています。
なるべく代表が手を動かすことなく案件を回す体制を築いています。

来月も、引き続き弊社メンバーの教育も兼ねた案件に注力していく予定です。

今月はメタバースを何度か試し、また、MidjourneyやStable DiffusionといったAIによるイラスト生成ツールを試してみました。



§ 財務基盤の堅牢化 財務をきっちりすること。前からの課題です。
弊社としては問題ないのに、家計が絡むととたんに脆弱になる。
この点は弊社の長年の課題でした。

ようやく、昨年の夏のコロナ感染がきっかけに苦しんだ状況に解決の光が見えてきました。
健全な経営は財務をきっちりしてこそです。今年度から毎月の費用をきちんと把握できるよう、毎月の領収書をスキャナで取り込み、それをfreeeに渡して収支を月次で出せるようにしています。

また、東京都中小企業振興公社の人財アドバイザーの方に昨年末よりアドバイスを受けています。この方から五年後の弊社の目指すべき姿を作るように言われ、SWOT分析をはじめ、売上や経費の計画をきちんと作成しました。税理士の先生にも手伝っていただきながら。
弊社代表が苦手意識を持ち、長年遠ざけていたこれらの作業。これらの作業から目を背けては経営に取り組めない。そのように覚悟を決めました。

§ 社内体制 昨年の師走の初めに、一年前に作成した社是、企業理念、経営理念やスローガンを見直しました。メンバーが離任した理由に、肝心な部分の価値観のずれがあったためです。そこで年始にあたり、三人でもう一度忌憚のない意見を交わしながら、それらを練り直しました。以下に掲示します。

企業理念
「情報技術を生かして、
正直に、飾らずに、自分、家族、パートナー、お客様、地域に寄り添う」

経営理念
「一期一会の儲けよりお互いが継続して協業できる幸せを」

9つ(ナイン)の「ない」
「組織図はない」
「タイムカードない」
「ノルマは設けない」
「多数決で決めない」
「社長室は作らない」
「肩書きもいらない」
「皆が経営者の行い」
「定年は強制しない」
「雑談は惜しみない」

アクアビットに合う方
「家族を大切にする気持ちのある方」
「仲間を大切にできる方」
「笑顔のある方」
「まず肯定から入る方」
「夢を持ち続ける方」
「人の話を聴ける方」
「人間が好きな方」
「可能性を信じる方」
「自分が好きな方」
「自分で仕事を見つける方」
「会社に滅私奉公せず公私を大切にする方」

一昨年の暮れに求人募集を出した時と考え方の軸はぶれていません。

昨年の師走より、東京都中小企業振興公社の方に人財制度についてのアドバイスを受けています。
人財アドバイザーの方から作成を指示された、五年後の弊社を見据えたビジョンや課題の解決策は、上記の各理念をより細かく詰めたものです。
文章や財務上の数値、目標の形で具体的に表し、それらは弊社のメンバーにも説明しています。
全員に当事者意識を持ってもらうため、各メンバーに課題を出してもらった結果が芳しくなかったので再提出を指示しました。目の前の課題や案件を解決するための視点ではなく、弊社が今後も生き残るために何をすればよいか。

今月は何回も不合格の壁に阻まれてきた弊社メンバーが、何度目か数えられないほどの挑戦の末、kintoneアソシエイト試験の合格を勝ち取ってくれました。また、その合格までのロードマップを記事にしてくれました。
受かった事よりも、諦めずに受け続けたその根性を私は評価したいと思います。

今月はメンバー全員、パートナー技術者、パートナー会社、メンバーの家族、代表の家族も集って暑気払い会を開催しました(8/5)。メンバーのお子さんの事も考えて府中のシズラーで。おかげでよい親睦が深められました。
また、今月はメンバーのプライベートにトラブルが発生し、Zoomで参加できなくなるなどの変化が生じています。
これは、昨年に犯した失敗の反省を踏まえ、代表に課された試練だと思っています。それもあって来月からはパートナー企業の方も含め、7名体制になります。慎重に、かつ大胆に果断に経営していきたいと思います。

来年にもメンバーに加わってもらう想定で体制づくりを進めています。

§ 人脈の構築 今月は夏休みもあって、リアル商談は半常駐先で定例会と打ち合わせで一度だけ。あとは全てオンライン商談でした。
オンライン商談はざっと数えたところ約31回ほどです。
今月、頂戴した名刺は1枚です。

とはいえ、今月も様々なオンラインイベントに顔を出しました。業務も忙しい中でしたが、オンライン上での知見は増やせたと思います。
また、来月以降のイベントの準備や種まきも。これらのご縁を通じ、次の面白いお仕事や次の展開に繋がっていきそうです。

こうしたご縁がつながり、弊社も弊社に関係する皆さんに福が舞い込む状況を喜ばしく思います。
引き続きよろしくお願いいたします。

§  対外活動 今月はこちらのイベントに参加しました。DevRel/Japan CONFERENCE 2022(8/6)。サイボウズ ユーザーフェスティバル 2022(8/17)。


DevRel/Japan CONFERENCE 2022は私にとって新たな学びの場となったDevRel Jpさんのイベントです。今回もさまざまな気付きを頂きました。また、サイボウズユーザーフェスティバルではkintoneコミュニティの紹介セッションにおいて、以前のkintone Café 神奈川での場面や事前に収録した紹介録画も含め、四回ほど登場しました。サイボウズOfficeのコミュニティが発表されるなど、とても貴重な学びの場となりました。

来月以降も複数のイベントに登壇予定で、今月もその準備や打ち合わせが何度かありました。

今年に入ってから、発信の機会を増やすため、noteの投稿を始めています。これも毎営業日に記事をアップしています。

弊社の開発や記事執筆のお仕事はこうした対外活動から生まれています。代表自身による新たな交流を発信することは絶対に怠ってはならないと肝に銘じています。

コロナがまだ落ち着く様子を見せませんが、リアルのイベントを中止にしている場合ではなくなってきたのも事実。また皆様と交流を深めたいと思っています。
まずは今月の弊社と関わっていただいた皆様、誠にありがとうございました。


§ 執筆活動 以前に連載していたCarry Meさんが運用する本音採用サイトの「アクアビット 航海記」を弊社サイトにアップし直す作業ですが、今月は1本アップしました。
(「アクアビット航海記 vol.46〜航海記 その30」)
私の人生でももっとも苦しみ、もっとも鉄火場の力を発揮したのが家の売却交渉。その交渉が始まる前年を描いています。

今月、書いた本のレビューは4本(
住友銀行秘史
選別主義を超えて―「個の時代」への組織革命
全一冊 小説 立花宗茂
図解・首都高速の科学

今月、書いた映画のレビューは0本()。
今月、書いた抱負は0本() 。
今月、書いた旅日記は0本() 。
今月、書いた「物申す」は0本() 。
今月、書いた弊社の活動ブログは1本(
kintoneアソシエイト試験合格ロードマップ
) 。
今月、書いた弊社の技術ブログは0本()。

なお、先月から毎営業日にnoteに書き始めた記事ですが、今月は以下の内容をアップしました。

7月28日 7月28日 現実と仮想の情報のあり方
7月29日 7月29日 データ入力のあり方が変わる
8月1日  8月1日 弊社の理念は世の平和のために
8月2日  8月2日 組織・団体の恐いところとは何か
8月3日  8月3日 環境変動に危機感を持たないと
8月4日  8月4日 kintoneのデザイン更改に備え
8月5日  8月5日 組織の指標を資格に託す試み
8月8日  8月8日 DevRelの活動をお勧めします
8月9日  8月9日 過去の教訓から学べる事は多い
8月10日 8月10日 汚された地球もまだ救える筈
8月12日 8月12日 技術への盲信は自戒したい
8月15日 8月15日 二度目の復興はきっとできる!
8月16日 8月16日 ユーザー体験と顧客の満足度
8月17日 8月17日 オンラインイベントを肯定
8月18日 8月18日 kintoneも適材適所に提案
8月19日 8月19日 常識への批判心を常に持つ
8月22日 8月22日 読解力と文章力こそが助け
8月23日 8月23日 リーダーシップは常に進行形で
8月24日 8月24日 正確な指示と伝え方の奥深さ
8月25日 8月25日 デモ行進の熱意の使い道とは
8月26日 8月26日 自主の学びを放棄するなかれ
8月29日 8月29日 担う役割は変転・進化する

§ 年表

・八月お仕事

タリーズコーヒー、二俣川で作業・打ち合わせ×4、Sizzler府中店で懇親会、Tully’s Coffeeで作業、Jeim 町田テストセンター、アリオ橋本で作業、アクアビットサテライトオフィスで作業

§ ツイートまとめ
・八月ツイート

https://togetter.com/li/1938004


kintoneアソシエイト試験合格ロードマップ


弊社メンバーが先ごろ、kintone アソシエイト試験に合格しました。
IT業界が未経験で面談時にkintoneの名前すら知らなかった状態で弊社にジョインしたメンバーが、何度も不合格になりながら、頑張ってkintoneアソシエイト試験の合格を勝ち取ってくれたロードマップを書いてくれました。

kintoneアソシエイト試験合格ロードマップ

  Topへ↓

kintoneの知識ゼロからアソシエイト試験取得までのロードマップを自分なりにまとめました。

まず試験の全体像を理解する必要があります。(テスト範囲)
問題の構成は9ヶのポイントに分かれているので、一つづつ点で覚えていき、最終的に線で理解する必要が出てきます。
覚える項目がおおすぎるので、必要最低限合格に必要な箇所のみに絞り込みます。

ステップ1

  Topへ↑

ではまず、全体像の把握のためのステップ1です。

ステップ1
まずは公式サンプル問題に挑戦します。
どのような問題がどんな風に出題されるかを把握します。

こちら(https://cybozu.co.jp/kintone-certification/aa_practice/questions.html)から3パターンで挑戦できます。

おそらくある程度kintoneを触ったことがある方でも半分も取れないと思います。
ステップ1で足りない知識をステップ2で補います。

ステップ2

  Topへ↑

ステップ2は基本的に暗記です。
大きく分けると9ケに分かれています。

ステップ2
1、kintoneの契約プランの特徴
2、Cybozu.com
3、kintoneシステム管理
4、ポータル
5、スペース
6、ゲストスペース
7、通知
8、アプリ
9、モバイル版での挙動

基本的にCybozu.com共通管理者、kintoneシステム管理者、アプリ管理者、スペース管理者、一般ユーザー、ゲストの目線で考えます。

Cybozu.com共通管理者は何ができて何ができないか、
kintoneシステム管理者は何ができて何ができないか、
といった目線で覚えていきます。

1、kintoneの契約プランの特徴

  Topへ↑

1、kintoneの契約プランの特徴
スタンダードコースとライトコースの違い。
ゲストユーザーの料金などを全て覚えます。
こちらのページ(https://kintone.cybozu.co.jp/price/)を丸暗記します。

2、Cybozu.com

  Topへ↑

2、Cybozu.com
ここは覚えることが多いですが、要点をまとめておきます。
最低限下記の5つのやり方は覚えます。
①、ユーザーの登録方法(表示名、ログイン名は必須)
②、一括操作(一括操作でできること、できないこと)
③、アクセス制限(有料なアクセス制限と無料のアクセス制限の種類)
④、監査ログ(監査ログで確認できる内容)
⑤、カスタマイズ(カスタマイズできる範囲)

参考ページ(https://jp.cybozu.help/general/ja/)

3、kintoneシステム管理

  Topへ↑

3、kintoneシステム管理
ここも必ず出題されますので、全て覚えます

①アプリ
アプリ管理、アプリテンプレート
②スペース
スペース管理、スペーステンプレート、スレッドのアクション
③ゲストユーザー
ゲストユーザーの管理、ゲストユーザーの認証
④権限
アクセス権、アプリグループ
⑤カスタマイズ
JavaScript / CSSでカスタマイズ
ヘッダーの色
⑥その他
アップデートアプション
利用する機能の選択
プラグイン
スマートフォンでの表示

参考ページ(https://jp.cybozu.help/k/ja/admin/permission_admin/permissions/set_admin.html)

4、ポータル

  Topへ↑

4、ポータル
ここではポータルでできる機能が重要になります。
お気に入り機能、検索機能、モバイルでの挙動。
この3つは確実に出題されます。
特にkintoneの検索機能はクセがあります。
添付ファイルの内容まで検索対象に入っているなど、
細かい箇所まで出題されますので、しっかり把握しておきます。

参考ページ(https://jp.cybozu.help/k/ja/user/portal.html)

5、スペース

  Topへ↑

5、スペース
スペースも問題数が多いのでしっかり覚えておきます。
スペースの作り方、スペースペンプレート、スレッドアクション、スレッドのフォロー
特にスペースの参加/退会、スレッドのフォロー/フォロー解除を禁止する箇所は確実に覚えます。

参考ページ(https://jp.cybozu.help/k/ja/admin/space_admin.html)

6、ゲストスペース

  Topへ↑

6、ゲストスペース
ゲストスペースの招待の仕方、ユーザーがゲストスペースでできる操作、ゲストができる操作
kintoneアカウントと共通化する箇所は応用問題でよく出題されます。

参考ページ(https://jp.cybozu.help/k/ja/user/guest.html)

7、通知

  Topへ↑

7、通知
どのタイミングで誰に通知されるか、通知の特徴
自分宛の通知と全ての通知の違いを把握しておけばここはそこまで重くはありません。

参考ページ(https://jp.cybozu.help/k/ja/user/notifications.html)

8、アプリ

  Topへ↑

8、アプリ
ここが一番覚えることが多いです
出題箇所も多いので、実際に操作して体で覚えることをお勧めします。

①アプリの作り方
ファイルを読み込んで作る方法(CSVやExcel)
拡張子まで覚えておきます。
②フォーム
フィールドの種類と特性
特にルックアップ、関連レコード、リンク、ラベル、はよく出題されます。
CSVに書き出したときのデータの動きなど、実際に見ておいた方が良いです。
③一覧
アプリ管理者と一般ユーザーで操作の違い
④グラフ
アプリ管理者と一般ユーザーで操作の違い
⑤設定
ここも項目が多いので、よく出る箇所をまとめます。
・プロセス管理
・通知
・アクセス権
・カテゴリー
・高度な設定
・アクション

カスタマイズ・サービス連携は深いところまでは出題されません。
カスタマイズ・サービス連携がアプリの設定でできて、プラグイン、JavaScript/CSSでカスタマイズ、APIトークン、Webhookで何ができるかだけ把握しておけば大丈夫です。

参考ページ(https://jp.cybozu.help/k/ja/user/app_settings.html)

9、モバイル版での挙動

  Topへ↑

9、モバイル版での挙動
モバイル版を使わない方もいらっしゃると思いますが、必ず出題されますので
モバイル版ではできないこと、できることを把握しておきます。
モバイル版に関する出題は項目も少ないので覚えやすいと思います。

参考ページ(https://jp.cybozu.help/k/ja/user/mobile_access/smartphone.html)

繰り返す

  Topへ↑

ステップ2が終わりましたら、ステップ1に戻りサンプル問題を受けます。
この流れを何度か繰り返し頭に定着させます。

何回か繰り返し、自信がついてきたら本番に挑戦することをお勧めします。
受験料金も安くはないので自信を持った状態で受けたいですね。
幸いにもサンプル問題の再現度はかなり高いので、実際の試験とのギャップに苦しむことはないと思います。


2022年7月のまとめ(法人)


令和四年七月。

一度はこの世から消えていくと思われたコロナウイルス。また新たな株が出現し、日本では世界最多の感染者数になるほどの猛威をふるっています。続いて第八波のケンタウロスと呼ばれる株も来ているようです。
先月、あれほど楽しんだ高知でのkintone Café 高知 & SORACOM UG SHIKOKUのイベントも、来月に開催を予定しているのですがオンライン開催も視野に入れなければならなくなりそうです。実際に、私が知る限り複数のイベントがオンラインに切り替わるようです。

一方でウクライナへのロシア侵攻は泥沼化しており、天才や自然災害のリスクもまだ予断を許しません。
まだまだ未来は不透明です。今は良くてもこれからどうなるか分からない。そんな激動の日々です。
そのような世情の中、仕事のご依頼が途切れずにいただけていることに感謝します。
とはいえ、その現状に安住するのは危険です。
常に外の世界に目を向け、現状に安心しないよう常に気を引きしめなければ。

それにはどうしなければならないのか。
まだまだ課題は山積みです。
尼崎市ではUSBが紛失した騒ぎが持ち上がり、KDDIさんの大規模な接続障害もありました。
この事件は弊社にとっても人ごとではありません。これからも常にセキュリティ対策やBCP対策は考えておかなければ。

昨年の反省を真摯に踏まえ、弊社はこれからも生き延びていけるよう、努力を続けます。
一見すると成果は出ており、順調に見えます。が、油断してはなりません。案件が積み重なっており、それが逆にリスクにもなっています。

達成度4割。達成感5割。満足感6割。それが今月の代表自身の自己採点です。

幸先がよかったとはいえ、その状況に甘んじず、足元を固めて進みたいと思います。
弊社とご縁をいただいたすべての方々に感謝します。ありがとうございました。

●弊社の業績

§ 総括 七月度の売上も目標額を超えることができました。

また、今月もいくつかの引き合いを頂きました。本当にありがたいことです。

弊社メンバーもがんばってくれています。四人体制で乗り切れるかは、これからの頑張りです。

あるお客様で四月から取り組み始めた半常駐の案件も、メンバーが頑張ってくれているため、今月から私が行く日数を大きく減らせました。そのメンバーは別の複数の案件も頑張ってくれています。
おかげでkintoneの中で完結できる案件については、うちのメンバーだけで回りつつあります。代表が打ち合わせに出なくても、うちのメンバーだけでお客様と打ち合わせて進めてくれています。とても助かっています。

ただ、今の案件はうちのメンバーだけではこなせない量になりつつあります。メンバーの開発スキルは上がっていますが、それ以上に案件の量が増えています。
今期の売り上げの予想額も算出し、場合によってはパートナー技術者にも関わってもらう方向です。もちろん弊社メンバーにも必ず案件をやり遂げられるようなスキルを身に付けてもらわねばなりません。

kintoneだけで回せるような案件はどちらかというと小規模です。弊社としては、大きな案件も引き受けていかなければなりません。
弊社の価値とはkintoneを他のPaaS/SaaSと連携させることにあります。その一つの到達点として、弊社独自のサービスを立ち上げる事を着想しました。
それが実り、先々々月にスマレジアプリコンテストで佳作をいただきました。
ただ、案件をこなすことに精一杯で、まだサービス化に至るレベルには達せられていません。

今月は弊社にとって以下のようなトピックがありました。
・弊社にてOculus Quest 2を購入しました。
・kintone Café メタバース支部の立ち上げに関わりました。
・公益財団法人埼玉県産業振興公社様が実施されている埼玉DXパートナーに弊社が登録されました。
・kintoneのプラグインを販売されているM-SOLUTIONS様と販売店契約を締結しました。
・今年初めから代表がついていた某お客様のセキュリティ講師の予定講義が終わりました。
実績を出しつつ、日常も充実させるための工夫。それらは全て、代表の求めるワークライフバランスの実現につながってゆくと信じています。引き続きよろしくお願いいたします。



§ 業務パートナー 今月は、新たなSaaS様との間でパートナー契約を結ぶ事が決まりました。来月早々に締結が出来そうです。

また、kintone界隈でも著名なM-SOLUTIONS様と販売店契約を締結しました。

先月から、経理や財務諸表の見直しを行い、五年間の長期計画を考えました。その中で経費として外注費をどう考えるかも検討しました。
かつて、外注費をSES業界の割合で設定していた失敗がありました。それを含めてパートナーさんとの外注費も見直し、業務の丸投げはしないように考えました。
上記の件を踏まえた打ち合わせを6/29に行い、今後の体制を固めています。

リモート体制でも指示がきちんと伝えられるよう、ツールの整備には引き続き取り組んでいきます。

§ 開発案件 今月は九割の開発案件がkintoneがらみでした。
ありがたいことに2,3,4月に頂いたたくさんの案件のお引き合いがあり、今月もその実装作業や準備に注力しました。週一、二件の頻度で新たな案件のお話をいただけており、感謝しています。

今、動いている案件が30件ほど。他にも二ダースほど準備が進んでいる案件があります。それらをどのようにこなしていくか。それが今の課題です。
今、弊社が取り扱っているシステムはkintoneをベースにGmail、Google Calendar、Zoom、HubSpot、ChatWork、Slack、freee、freeeサイン、マネーフォワード、スマレジ、LINE、Benchmark、box、Stripe、サブスクペイ、AWS、各種CMS、その他です。
案件によってはパートナー社や技術者さんと一緒に構築し、私や弊社の技術顧問の方も実装に取り組みながら日々の開発に取り組んでいます。

経営者がワーカーとして奔走しているようでは社の発展は望めません。長期計画やビジョンを決め、経営に真摯に取り組み始めた今だからこそ、強くそのことを感じます。
代表が経営に時間を割けなくなると大所が見えなくなります。それは経営上のリスクとなってしまいます。
弊社を発展させるためにも、なるべく代表が手を動かすことなく案件を回すようにしなければ。

今月は代表が携わっていた某お客様のセキュリティ講師の講義も全八回が一段落つきました。

来月も、引き続き弊社メンバーの教育も兼ねた案件に注力していく予定です。

§ 財務基盤の堅牢化 財務をきっちりすること。前からの課題です。
弊社としては問題ないのに、家計が絡むととたんに脆弱になる。
この点は弊社の長年の課題でした。

ようやく、昨年の夏のコロナ感染がきっかけに苦しんだ状況に解決の光が見えてきました。
健全な経営は財務をきっちりしてこそです。今年度から毎月の費用をきちんと把握できるよう、毎月の領収書をスキャナで取り込み、それをfreeeに渡して収支を月次で出せるようにしています。

また、東京都中小企業振興公社の人財アドバイザーの方に昨年末よりアドバイスを受けています。この方から五年後の弊社の目指すべき姿を作るように言われ、SWOT分析をはじめ、売上や経費の計画をきちんと作成しました。税理士の先生にも手伝っていただきながら。
弊社代表が苦手意識を持ち、長年遠ざけていたこれらの作業。これらの作業から目を背けては経営に取り組めない。そのように覚悟を決めました。

§ 社内体制 昨年の師走の初めに、一年前に作成した社是、企業理念、経営理念やスローガンを見直しました。メンバーが離任した理由に、肝心な部分の価値観のずれがあったためです。そこで年始にあたり、三人でもう一度忌憚のない意見を交わしながら、それらを練り直しました。以下に掲示します。

企業理念
「情報技術を生かして、
正直に、飾らずに、自分、家族、パートナー、お客様、地域に寄り添う」

経営理念
「一期一会の儲けよりお互いが継続して協業できる幸せを」

9つ(ナイン)の「ない」
「組織図はない」
「タイムカードない」
「ノルマは設けない」
「多数決で決めない」
「社長室は作らない」
「肩書きもいらない」
「皆が経営者の行い」
「定年は強制しない」
「雑談は惜しみない」

アクアビットに合う方
「家族を大切にする気持ちのある方」
「仲間を大切にできる方」
「笑顔のある方」
「まず肯定から入る方」
「夢を持ち続ける方」
「人の話を聴ける方」
「人間が好きな方」
「可能性を信じる方」
「自分が好きな方」
「自分で仕事を見つける方」
「会社に滅私奉公せず公私を大切にする方」

メンバーの募集を出した時と考え方の軸はぶれていません。

昨年の師走より、東京都中小企業振興公社の方に人財制度についてのアドバイスを受けています。
そのアドバイザーの方から作成するようにと指示された、5年後の弊社を見据えたビジョンや課題の解決策は、上記の各理念をより細かく詰めたものです。文章や財務上の数値、目標の形で具体的に表しています。
それらはメンバーにも見せ、説明もしています。
全員に当事者意識を持ってもらおうと、各自に課題も出してもらったのですが、残念ながら目の前の課題や案件を解決するための視点にとどまっています。

弊社メンバーは、まだ技術的なスキルの習得に目一杯の状況です。経営者の視点までは気が回らないのも仕方がないのかもしれません。
ただし、少しずつ進歩はしてくれています。焦りは禁物と肝に銘じ、教えていくつもりです。
まずはスキル面の評価と人事面の評価を別に行う体制を作りながら、弊社の体制を良いものに整えていこうと思います。

3月の半ばよりメンバーに一人、SES形態で参加してもらっています。紹介予定派遣なので、いずれは正社員登用も視野においています。
また、パートナー会社の方にも今月より専任で関与してもらっています。それを踏まえ、今月からは6名体制でやっています。
毎週の月曜日の朝に行っているオンラインの打ち合わせは継続し、顔を合わせての打ち合わせも再開したいと思います。
昨年に犯した失敗の反省を踏まえ、慎重に、かつ大胆に果断に経営していきたいと思います。

来年にもまたメンバーに加わってもらう想定で進めています。

§ 人脈の構築 今月は外出や打ち合わせを何度も行い、リアル商談を数多く行いました。
お客様とのリアル商談は約15回です。オンライン商談はざっと数えたところ約34回ほどです。他にも半SESのお客様での打ち合わせもあります。
今月、頂戴した名刺は20枚です。

先月はkintone Café 高知 Vol.16とSORACOM UG SHIKOKUへの参加を通じて、SORACOM界隈の皆さんに知己が作れました。
それによってSORACOIM UGには二回。SORACOMさんが主催するIoT Solution Day 2022にも参加しました。
これらを通じてまた面白いお仕事や次の展開に繋がっていきそうです。

また、kintone Café 神奈川 Vol.13 と SORACOM UG 東京の共催イベントの準備を通して、またまた面白いご縁を多数いただいています。
こうしたご縁がつながり、弊社も弊社に関係する皆さんに福が舞い込む状況を喜ばしく思います。
引き続きよろしくお願いいたします。

§  対外活動 今月はこちらのイベントに参加しました。SORACOM Discovery 2022 Online(7/6)(7/7)。SORACOM UG Online(7/7)。kintone Café メタバース支部準備会(7/14)。kintone hive Tokyo Vol.15(7/20)。SORACOM UG Online #13(7/25)。IoT Solution Day(7/28)。

今月は、SORACOMさんがらみのイベントが多く、界隈の方々の熱量やコミュニティの雰囲気を学べました。
また、メタバース支部の準備会は、届いたばかりのOculus QUEST 2に四苦八苦しながらの参加がとても印象的にでした。
kintone hive Tokyo Vol.15は皆さん素晴らしい登壇で、hiveの地区予選を見るのが初めてだった私にはとても刺激になりました。

来月も複数のイベントで登場する予定でしたが、9月にずれ込みそうです。
今年に入ってから、発信の機会を増やすため、noteの投稿を始めています。これも毎営業日に記事をアップしています。

弊社の開発や記事執筆のお仕事はこうした対外活動から生まれています。代表自身による新たな交流を発信することは絶対に怠ってはならないと肝に銘じています。

コロナが小康状態になり、リアルのイベントも増えつつあります。また皆様と交流を深めたいと思っています。
まずは今月の弊社と関わっていただいた皆様、誠にありがとうございました。



§ 執筆活動 以前に連載していたCarry Meさんが運用する本音採用サイトの「アクアビット 航海記」を弊社サイトにアップし直す作業ですが、今月は1本アップしました。
(「アクアビット航海記 vol.45〜航海記 その29」)
私が今まででもっとも技術者として経験を積んだ時期の充実を書いています。今から思うとまぶしい時期でした。

今月、書いた本のレビューは3本(
翳りゆく夏
立花三将伝
昭和天皇の終戦史

今月、書いた映画のレビューは0本()。
今月、書いた抱負は0本() 。
今月、書いた旅日記は0本() 。
今月、書いた「物申す」は1本(
この国の未来に私や弊社ができること
) 。
今月、書いた弊社の活動ブログは0本() 。
今月、書いた弊社の技術ブログは0本()。

なお、先月から毎営業日にnoteに書き始めた記事ですが、今月は以下の内容をアップしました。

6月29日 メタバース上の会議の可能性
6月30日 纏めを書いて時間を貯める
7月1日 年の後半に入って気を配る事
7月4日 有事対応にこそ会社の真価が。
7月5日 鎌倉幕府に組織の困難を見る
7月6日 政治に関心は持ち続けましょう
7月7日 コミュニティで人生が変わる
7月8日 SORACOMさんの世界の広さ
7月11日 情報技術で変えていかねば
7月12日 SNSにも読解力は必要です。
7月13日 孤独を考えぬ働き方改革は無
7月14日 コミュニケーション手段の長短
7月15日 メタバース体験とその可能性
7月19日 人工知能に意識の芽生え?
7月20日 kintoneとSORACOMの相性
7月21日 資格試験を目標にする意味
7月22日 お客様との伝達路を整えたい
7月25日 場の繋がりを体験するきっかけ
7月26日 技術情報の伝達者に大切な事
7月25日 研修資料から学ぶ事は多い

§ 年表

・七月お仕事

二俣川で作業・打ち合わせ×5、EKITERIA PRONTOで作業・打ち合わせ、銀座で商談、快活CLUB 王子駅前店で作業、八王子労働基準署 町田支署で手続き、町田森野郵便局で手続き、江戸川区で商談・作業、丸の内で商談、鶴川で手続き、八王子年金事務所で手続き、セブンイレブン 八王子八日町5丁目店で手続き、
コワーキングスペース八王子 8Beatで作業、Tully’s Coffee Prime Fiveで作業、日比谷公園で作業、Starbucksで作業、Tully’s Coffeeで作業、東京都中小企業振興公社で打ち合わせ、鎌倉・旅する仕事場で作業、安国論寺で視察、鎌倉ちゃんぷるで親睦、魚がし日本一で親睦、bizstation新橋で打ち合わせ、ビーフキッチンスタンドで懇親、本郷で商談、ドトールコーヒーショップ 本郷三丁目東店で作業、大崎ブライトコアホールでIot Solution Day 2022、アクアビットサテライトオフィスで作業×5

§ ツイートまとめ
・七月ツイート

https://togetter.com/li/1923605


Cybozu Days 2020に三日間出展しました


2020年11月11日から11月13日まで幕張メッセで催されたCybozu Days 2020に弊社も出展いたしました。https://cybozuconf.com/

今まで、弊社はCybozu Daysも含め、あらゆる展示会に出展した経験がありませんでした。
そうした経験の不足に加え、今回は世の中がコロナウィルスに振り回されています。そんな世相の中、そもそもCybozu  Days開催されるかすら定かではありません。
さらに、多数のkintone案件を受注し、開発に従事していた弊社にとって、並行してCybozu Daysの出展準備などとても無理。そう思っていました。

そう思っていた一方で、昨年のDaysの後、サイボウズオフィシャルパートナーとして任命いただきました。また、長らくエバンジェリストとして任命されていながら、ステージにも立ったことがなければブース出展の経験もなかった私。いつもオーディエンスのままだったことに自分でも飽きたらないものを感じていました。
パートナーになってブース出展の資格を得たところで、一つブース出展をやってみようか。
ところが弊社は、kintone連携ソリューションを持っていません。ブースに出展しても、出せるのはパートナーとしての開発実績か、エバンジェリストとしての立場のみ。そんな手持ちの札が少ない状態でブースを出展したところで、会場の隙間を埋めるだけに終わってしまうのでは、と懸念していました。

そんな私に心強い助っ人が。
Polaris Infotech社の情報親方こと東野さんです。
https://www.polarit.co/

kintone界隈の方々には、サイボウズ社が発行しているkintone導入ガイドブックはおなじみですが、そのコンテンツは東野さんによって作られています。
さらに東野さんは、新規技術やガジェットを導入するのが好きで、その進取の気性は、私をはるかに上回っています。
弊社はサイボウズオフィシャルパートナーであり、出展資格を持っていますが、Polaris Infotech社はパートナーでないため出展できません。

二社が共同出展することにより、互いに足りないところを補い合い、なおかつ二社にとってWin-Winの結果が見込まれる。
そんなわけで、共同出展に踏み込みました。

二社で共同出展することにしたとはいえ、来場者の目を引くにはもう一つ決め手が薄いことは否めません。なにしろ、kintone界隈で著名な連携サービスを持っていないのですから。そのような状態で出展するには決め手が足りない。もう一つ柱が欲しい。

そこで東野さんが提案したのが、アバターによる音声応答のソリューションです。
名古屋のRAKUDOさんが運営するAI Interfaceというサービスがあります。
https://www.ai-interface.com/
http://rakudo.io/homepage/
AI Interfaceが備える音声応答のソリューションの背後にkintoneを組み合わせれば、音声による応答を実現しつつ、背後のデータベースも備えられる。しかもkintoneの場合、簡単にデータベースが構築できる。より良いソリューションになるはず!

そんなわけで、他の案件との並行に疲弊しながらでしたが、アプリの連携に成功しました。

あとはブースの展示パネルやちらしや名刺の手配です。ここは出展経験を持つPolaris Infotechさんの経験に全面に依存しながら、無事に当日に間に合わせることができました。東野さんには感謝です。
スタッフも両社の関係会社やビジネスパートナーさんにご協力を仰ぎ、三日間で延べ11名のスタッフに手伝っていただきました。皆さんにも感謝です。
また、二日目の最後のほうにこうした素敵な動画を撮影し、すぐにYouTubeにアップしてくださったキンスキの松井さんにも感謝です。

こうして三日間の出展を無事終え、感じることは来場者の皆さんへの感謝です。
沢山の方が訪れてくださいました。埋め草ブースどころか、私たちの思った以上に。
二社の知己の方はもちろん。弊社が過去に手掛けさせていただいたお客様や、私が前月に登壇したセミナーで私に興味を持ってく出さった方や、サイボウズ社員の方など。
もちろん、初めてお会いする方も多くが私たちのブースに立ち寄ってくださいました。

もちろん、無名の私たちのブースは、ほとんどの方にとっては興味を引く対象になりえず、一瞥して通り過ぎてしまう方も多かったです。
それでも、私たちのブースに目を止めて下さった方がいたことは、とてもありがたく思いました。

なぜ目にとめてくださったのか。kintone連携ツールのパネルが掲げられてもおらず、ノベルティで客引きできるほど資金力もない我々のブースに。
それはおそらく、アバターの画面がブースの中で異彩を放っていたからでしょう。
アバターの女の子にならんで検索の応答結果として東野さんの顔が大きく映し出されたディスプレイ。それはなかなかのインパクトでした。東野さんの打った手が見事に功を奏しました。

今回のCybozu Days 2020は、EGO & PEACEというテーマでした。
わがままと平和。
その一見すると相反する二つのテーマは、今の多様化が求められる社会において、誰もが両立に悩んでいます。

私にとっても同じです。今までの私の生涯は、まさにEGOを追い求めてきたものでした。
ラッシュがいやだ、毎朝毎夕の通勤の繰り返しがいやだ。それは間違いなくEGOです。
では、そのEGOをどうやって実現し、人に迷惑をかけずに自分にとって平和な毎日を呼び入れるか。

そもそも私がなぜkintoneのエバンジェリストになっているか。
その主な理由は、こうしたサイボウズ社の理念に共感していることです。
毎年のCybozu Days で打ち出されるテーマが、私にとって刺さります。
今年もそれは変わりませんでした。

ただ、今年はブースに三日間のほとんどを立つことに決めており、セッション聴講はあきらめていました。
ですが、三日目のセッションが聞けたのはうれしかったです。(初日のkintone Hackもほぼ見ることができました)

まさにそこで展開されていた議論は、私と弊社の抱えるテーマに即しており、そうした場に出展社として参加できたことがとても誇らしかったし、得るものが多かったです。

弊社は大阪のCybozu Days 2020には出展しませんが、私は登壇のために初めて参加する予定です。今から楽しみでなりません。

最後になりましたが、来場者の皆さん・サイボウズの皆さん・弊社ブースのスタッフの皆さん・東野さん。ありがとうございました!


ワーキングツリーにはkintoneとboxをお飾り!


kintone Advent Calendar 2019の24日目の記事です。

  Topへ↓

ん?この著者、この間もAdvent Calendarでみたで? はい。二度目の登場です。

kintoneは優れたツールですが完全ではない?
なーんてディスられても動じず、欠点を正直に認めるのもkintoneの憎めないところ。そう思ってやまない著者です。

kintoneの欠点のいくつかはすぐに挙げられます。
例えばブラウザーベースで動いているので、ファイルアップロードの作業が面倒、とか。
添付ファイルフィールドに画像データを放り込みまくると、一ユーザーあたり5GBの容量の制限が足かせになってくる、とか。

そんな限界を解消するためのささやかなクリスマスプレゼントを皆様にお届けしたいと思います。
なに、ちょっとした贈り物です。クリスマスツリーによくぶら下がっている箱のような。
箱・・・つまりboxです。
今回の記事では私の2019年の失敗事例も公開しているので、ひょっとしたら皆さまのご参考になるかもしれません。

box for kintoneのご紹介

  Topへ↑

世の中にオンラインストレージ製品はたくさんありますよね。その中でもboxが存在感を出しているのはご存じでしょうか。

kintoneとboxの連携はbox for kintoneというプラグインとして公開されています。
それを使えばブラウザー上でアップロードなどせず、ドラッグ&ドロップでブラウザー上から操作できちゃうのです。

クライアントツールのbox Driveをインストールすれば、Windowsのエクスプローラと同じ操作でbox上にファイルをアップロードできてしまう。なんて優れもの。

boxを使うと無尽蔵(契約プランによる)を誇る容量にファイルを置きまくり。
それをkintoneの画面上からに自由に呼び出せる。素晴らしい!
上に書いたkintoneの弱点を周囲のツールが補ってくれるいい例です。
本稿もそうしたkintoneを補ってくれる一つの例としてお役に立てれば幸いです。お日柄もよいので。

box for kintoneの使い方は、Cybozu developer networkにも出ています。
https://developer.cybozu.io/hc/ja/articles/205070124-Box-for-kintone その記事に従えば、簡単にbox for kintoneを導入できるはずです。

ところが、この記事で書かれているのは、一つのレコードに一つのフォルダーを対応させるところまで。
kintoneでちょっとしたシステムを作ろうとすれば複数アプリにまたがった構成が必要です。それに応じてboxのフォルダー構成も複数の階層にまたがってしまいます。

合同会社アクアビットダム設計なる会社

  Topへ↑

たとえば、大阪と東京に支店がある合同会社アクアビットダム設計があったとします。
この会社はダムを独自の技術で製造し、お客様にお納めしている設定です。

受注システムをkintoneで構築するにあたり、
大阪支店[組織]の
長井何某[個人]が担当する
担当案件[案件]の
施行状況[施工]と
湛水状況[湛水]を管理すると仮にしましょう(工程はしょりすぎ。ちなみに最後の工程は水を貯める工程です)。

他に顧客マスタがあるでしょうがここは割愛。また、[組織]と[個人]はアプリではなく、kintoneのアカウントを使用する想定です。
この場合3アプリですね。

ここでご注文からの流れをkintoneで管理したとしましょう。
各アプリの連動はkintoneのアクション機能を使ったとします。

ダム完成までにはさまざまな状況を報告していかねばなりません。するとダムの進捗に合わせて写真が大量に溜まっていきます。
kintoneの添付ファイルフィールドにファイルをアップしていると、すぐに容量が危うくなりかねません。
ここでboxの出番です。

ここでboxで写真を管理しようとした場合、box内のフォルダー構成はこのようになると思います。

さて、先ほどご紹介したbox for kintoneを思い出してみましょう。
プラグイン設定画面にルートフォルダーのIDを設定していましたね。

つまり、アプリ自体にルートフォルダーのみを作る仕様。
それって、どのレコードであろうと共通で1つのフォルダーだけ、、、
いやいや多層boxと多層アプリでは対応できないのはちょっと、、、
結論! box for kintoneだとちょっとキツイかも。

合同会社アクアビットダム設計にboxを

  Topへ↑

ということで、本稿では多段階にわたるboxの連動例をお伝えしたいと思います。また、その時にしでかしてしまった失敗と、そのリカバリ例もお伝えしたいと思います。

まず、話を簡単にするため、合同会社アクアビットダム設計としてのルートフォルダーを設定しておきましょう。

さらに、支店ごとにフォルダーを設定し、支店の配下に担当ごとのフォルダーも生成しておくと話が早いですね。

実際のboxのフォルダー構成はこんな感じ。

ここでルートフォルダーのフォルダーIDを取得しておきます。boxの画面から取れます。

その状態で、案件アプリに新規レコードを登録します。
案件アプリの項目には案件の主管支店と、案件の主担当を指定するフィールドも忘れずに。もちろん必須項目として。

なぜ必須項目にするのでしょう。
その理由は、レコードが保存成功後、案件フォルダーを作る際にどこのフォルダーの配下に作成するか決めなければならないためです。

boxのフォルダー生成APIについて

  Topへ↑

ここでboxの仕様を押さえておきましょうか。
サービスの仕様を確認するには、APIから逆引きしたほうが理解しやすい。いわゆる技術者あるあるです。
boxのAPIはこちらのサイトをご覧になると良いでしょう。
https://ja.developer.box.com ・・developerサイトトップ
https://ja.developer.box.com/reference ・・APIレファレンス

boxのフォルダー作成の項を読むと、親であるフォルダーのIDがパラメーターとして必須のようです。
編集時には親フォルダーのIDは必須ではなくなりますが、もしフォルダーの場所を移動する際は親フォルダーのIDは指定しなければなりません。

案件フォルダーを作る際は、親となる担当者フォルダーのIDを把握しておかねばなりません。
そしてその上の支店フォルダーも。

つまり、案件レコードの保存のタイミングで行うべきことは、まず、そのレコードの支店フィールドの値に等しい支店フォルダーを検索することです。
その際、基準となるのはルートフォルダーです。

ルートフォルダーの下にある支店フォルダーを検索し、そのIDを特定します。
間髪入れずに支店フォルダーの配下にある担当者フォルダーを検索します。
これは同一担当者が複数支店にフォルダーを持っている場合など、運用も考慮していますが、支店フォルダーのIDを内部で保持できるのであれば、いきなり担当者フォルダーから検索してもよいです。
重要なのは案件フォルダーを作成するにはその親フォルダーのIDを事前に必ず保持しておくことです。

先ほど、kintoneの案件アプリの支店と担当者の両フィールドは必須でなければならないとしたのには、そういう理由があったのです。
これら二つのフィールドの値がないと、案件を保存する際に生成されるべき案件フォルダーの保存先が迷子になってしまうので。

JavaScriptで実装してみた

  Topへ↑

続いてはいよいよboxの操作を行います。

その前に本稿ではboxの権限周りには踏み込まないことを言っておきます。
OAuthについては、もともとbox  for  kintoneで用意されていたclient IDを使用します。本当はbox内でアプリを作成し、そのアプリ内で設定した権限を認証しなければならないのですが。
box for kintoneに甘えてしまいましょう。

ついでにpromise処理が考慮されたAPI実行部分もbox for kintoneの処理を流用させていただきましょう。

処理の大まかな順序としては以下の通りです。
ただ、のちに述べますが、このコードは動きません。なのでコードは画像として参考程度に載せます。

まず、イベントが動くタイミングはapp.record.create.submit.successです。新規作成処理成功後ですね。

処理の都合上、この中で別のアプリに更新を行い、その結果が成功した場合にboxフォルダー生成処理を呼び出しています。

boxフォルダー生成処理では、まずルートフォルダーから支店フォルダーを検索します。
続いて支店フォルダーから担当者フォルダーを検索します。

ここでboxの検索の仕様が立ちふさがってきます。
boxの検索仕様として、対象の種類、生成時刻、オーナーIDや親フォルダーIDなどは指定できるのですが、肝心の文字列を完全一致で検索できないのです。queryというパラメータがあるにもかかわらず、そこに指定した文字列は曖昧検索として処理されてしまうのです。
APIレファレンス

つまり、親フォルダーに属する検索対象が複数ありうる場合、検索文字列に工夫が必要です。例えば姓名の間にスペースが入る場合など。
「長井 権兵衛」と「長井 主水」が対象のフォルダー配下にあって「長井 権兵衛」を検索したい場合、queryに「長井 権兵衛」を設定してもマッチしません。
ではどうやればよいか。
スペースの前後の文字列で検索するのです。
この場合、「長井」または「権兵衛」で検索します。すると前者は二件がヒットし、後者は一件がヒットします。
その結果を再度ループして回し、一件ごとにname属性の値が検索文字列に一致するかを確認する。
そのような面倒な処理がboxの検索には必要です。

このコードも実際は使っていませんが、軽く提示します。

これで、担当者フォルダーIDまで求められました。

boxのフォルダー生成と検索にまつわる問題

  Topへ↑

続いてはフォルダーの生成に移りましょう!
APIレファレンス

生成にあたっては、名前と親フォルダーのIDを指定するだけです。

これで、案件レコードが保存されたら案件フォルダーを作成するところまでができました。

ここで当初想定していたboxの構成を見てみましょう。
案件フォルダーの配下に「提案状況」「施工状況」「湛水状況」の三フォルダーがあります。
このフォルダーの生成にも実は厄介な問題が潜んでいます。

例えば、案件のレコードが保存されました。そして案件フォルダーが生成されました。
そしたら、アクション機能によって施工状況アプリにレコードをコピーし、施工状況アプリでもレコードが保存された瞬間、案件レコードの時と同じように施工状況フォルダーを生成すればええんちゃうの?と思ったでしょう。

ところが、フォルダーを生成するには親のフォルダーの指定が必須です。
親フォルダー、つまり案件フォルダーをフォルダー生成処理の直前で検索してフォルダーIDを取得しなければなりません。
ところがこの親フォルダーの検索取得にはひとつハードルが控えています。そのハードルとは、コンテンツが生成されてから検索可能となるまでに時間がかかる、というものです。
boxはなんらかのコンテンツが作成されてから、それが検索可能となるまでにbox内部でindexを構築しており、それに時間が掛かるのです。
boxの APIレファレンスには以下のように書かれています。

つまり、案件フォルダーの生成からすぐ、施工状況アプリのレコードを保存した場合、親となるべきフォルダーが検索できないため、親フォルダーの指定ができないのです。

boxを多層構造でkintoneと連動させる場合、この仕様上の制限は現状では避けられません。

この制限を回避するため、発想を切り替えました。
つまり、案件フォルダーが生成された後、同時に配下のフォルダーも作ってしまうのです。

box APIでは、フォルダー生成が成功した時点で返り値として生成されたフォルダーのIDが得られます。このIDを使えば配下のフォルダーも即時に生成できます。

このコードも実際は使っていませんが、軽く提示します。

この下の処理ではさらに生成した案件フォルダーのURLを取得し、そのURLやフォルダーIDをkintoneの2アプリに更新して設定しています。

ここまででkintoneのapp.record.create.submit.successイベントを見てきました。その結果、実装ができそうです。
テストでもフォルダーが意図通りに生成されました。開発用のPCでも、お客様のご担当者様のPCでも。

バグ大魔王降臨!

  Topへ↑

ところが! やったと思った安心のかげに潜むのが落とし穴。バグが出てしまったのです。
テストではうまく動いていたのに、いざ本番になるとうまくいかない。なんということでしょう!

実は、その根本的な原因は今もなお究明できていません。
事象としてはboxにAPIリクエストを投げた後、何も戻ってこないのにプログラムが終了してしまいます。httpレスポンスすら帰ってこずに。
それも終了する場所がまちまちなのが始末が悪い。複雑なPromiseの構造に加え、referredを混在させたことにも問題があったのかもしれません。
この不具合がやっかいなのは、boxからのレスポンスを待つ間、app.record.create.submit.successの結果が完了できないことにあります。その間、ブラウザーは固まってしまい、kintoneを利用されている皆様にはただ困惑が。

そして、この不具合の原因がブラウザーにあるのか、box側にあるのか、kintone内部にあるのか、それともPCのスペックにあるのか。はたまたネットワーク環境によるものなのか。いまだに分っていません。
ただ、ブラウザー上でレスポンスを待つ運用はまずい、という悔いだけは骨身に沁みました。
私はその原因を追究するよりもお客様の運用を円滑に進めることを優先しました。
その決断として、ブラウザーに依存する実装を止めました。

AWSへ処理を移管

  Topへ↑

では、どうすればよいか。

幸いなことにkintoneにはWebhookという機能が備わっています。Webhookには、レコード保存時にWebhookのリクエストを受け付けてくれるWebhook URLを設定できます。

私がWebhook URLとして設定したのはAWSのAPI Gatewayで設定したURLでした。
API Gatewayについての説明は割愛しますが、kintoneから受け取ったWebhookのリクエストに含まれるJSONを読み取り、それを後続の処理に渡すことができます。
後続の処理にはAWS Lambdaを選びましたので、同じAWS上で処理が連携できます。

AWS LambdaではNode.jsを使い、ほぼkintoneのkintone.app.create.submit.successで実装したのに近いコーディングを行いました。
box Node SDKがAWS Lambdaから簡単に使用できるので、それを使えば似たような実装ができるのです。
ただし、boxのアプリは一から作る必要があります。設定もあれこれ行う必要が生じました。
最初、こちらのブログの力も借りました。ありがとうございました。
Lambda関数のコードを以下に掲示します。なお、このコードは動いているものを基にいろいろといじっているので参考になると思います。

/**
 * This sample demonstrates how to call Box APIs from a Lambda function using the Box Node SDK.
 *
 * For step-by-step instructions on how to create and authorize a Box application,
 * see https://github.com/box/samples/tree/master/box-node-lambda-sample.
 */
const BoxSDK = require('box-node-sdk');                                                // Node.jsのbox-node-sdkモジュールを呼び出す
const request = require('request');                                                    // Node.jsのrequestモジュールを呼び出す
const boxConfig = JSON.parse(process.env.BOX_CONFIG);                                  // AWS Lambdaの環境変数のBOX_CONFIGの値をJSONで扱えるように

boxConfig.boxAppSettings.appAuth.keyID = boxConfig.boxAppSettings.appAuth.publicKeyID; // 9行目で取り出したkeyIDにpublicKeyIDを代入

const sdk = new BoxSDK(boxConfig.boxAppSettings);                                      // 9行目で取り出したboxAppSettingsをsdkに代入

/**
 * Create a service account client that performs actions in the context of the specified
 * enterprise.  The app has a unique service account in each enterprise that authorizes the app.
 * The service account contains any app-specific content for that enterprise.
 * Depending on the scopes selected, it can also create and manage app users or managed users
 * in that enterprise.
 *
 * The client will automatically create and refresh the service account access token, as needed.
 */
const client = sdk.getAppAuthClient('enterprise', boxConfig.enterpriseID);             // boxアプリが適用できるアカウントのグローバル設定を管理

var DOMAIN = '*********.cybozu.com'; //kintone環境のドメイン                            // *****はご使用のkintoneのサブドメインを
var APP_ID_1287 = 1287;   //案件管理アプリのアプリID
var BASE_URL = "https://" + DOMAIN + '/k/v1/';
var APITOKEN_1287 =  "kintoonkaramottekitatookunwokokoniiretene";                      // kintoneの案件アプリのAPIトークン
var headers_1287 = {'X-Cybozu-API-Token': APITOKEN_1287};                              // リクエストで使用するヘッダ
var FolderId;
var updaterecordid_1287;

exports.handler = (event, context, callback) => {                                      // eventはkintoneのWebhookからAPI Gatewayを経由したレコード情報
                                                                                       // contextはLambda関数に関する情報
    const API_BASE_PATH = 'https://api.box.com/2.0';                                   // box Node SDKの文法に準拠

    // targetnameはコンテンツの文字列
    // typeはコンテンツの対象。本稿の場合はfolder
    // content_typesは検索対象とするプロパティ。本稿の場合はname
    // limitは検索結果として戻す件数。
    // idsは親フォルダーのフォルダーID
    // methodは本稿では全てGETなので使用していない
    // dataは本稿の場合検索対象(支店,担当者,案件No)のうち、担当者の場合["担当名"]で渡ってくる。
    function searchFolder(targetname, type, content_types, limit, ids, method, data, success, error) {  //157,159,161行目から呼び出されて検索処理を実施
        if (data !== undefined) {                                 // dataが指定されている場合
            if (data[0] === "担当名") {                            // dataの配列の最初の要素が"担当名"の場合
                targetname = targetname.split(' ')[0];           // 受け取るtargetnameは「長井 権兵衛」の様に全角スペースで区切られた姓名なので姓を取得
            }
        }

        return new Promise(function (resolve, reject) {           // Promiseを設定
            client.search.query(                                  // 24行目でclientとして承認されたbox Node SDKのsearchクラスのquery関数を呼び出し
                "\"" + targetname + "\"",                         // 最初のパラメーターは検索対象文字列。文字列なのでエスケープした""で囲む。でも曖昧検索
                {                                                 
                    fields: 'id,name,modified_at,extension,permissions,collections',  //検索結果として返すコンテンツのプロパティ
                    type: type,                                                       //folder
                    content_types: content_types,                                     //検索対象はnameプロパティ
                    limit: limit,                                                     //結果として返す件数
                    ancestor_folder_ids: ids,                                         //親フォルダーID 
                    offset: 0                                                         //オフセットしないので0
                })
                .then(function(results){                                              //結果が取得されたのでこのPromiseチェーンへ
                    if (data !== undefined) {                                         //dataが指定されている場合
                        if (data[0] === "担当名") {                                   //dataの配列の最初の要素が"担当名"の場合
                            for (var i = 0; i < results.entries.length; i++) {        //戻り値の件数分(limitで指定した件数分)
                                if (results.entries[i].name === data[1]) {            //戻り値のnameプロパティがdataの2番目の要素(担当名)か
                                    resolve(results.entries[i].id);                   //Promiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                                }
                            }
                        }
                    } else {
                        resolve(results.entries[0].id);                               //Promiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                    }
                })
                .catch(function(error){ // エラーの場合
                    reject(error);
                });
        });
    }

    // createParamは生成フォルダー名と親フォルダーIDが含まれたJSONオブジェクト
    function postFolder(createParam) {                     //176行目から呼び出されてフォルダー生成処理を実施
        return new Promise(function (resolve, reject) {    // Promiseを設定
            client.folders.create(createParam.parent.id, createParam.name)  //24行目でclientとして承認されたbox Node SDKのfolderクラスのcreate関数を呼出
                                                                            //1つ目は親フォルダーID、2つ目は生成するフォルダーの名称 
                .then(function(results){                                    //85行目の処理が成功したのでこのPromiseチェーンへ
                    var ankenid = results.id;                               //生成したフォルダーIDを以下のforeach内で使うためにankenidに代入 
                    var subfolders = [                                      //生成した案件フォルダーの配下に作成する三つのフォルダー名を配列にしています
                        "提案資料",
                        "施工状況",
                        "湛水状況"
                    ];

                    var promiseset = [];                                    //三つのフォルダーの生成が終わるまで待つPromiseを三つ作るので配列を設定
                    subfolders.forEach(function(val,index,ar){              //89行目で生成した配列の各要素をループします
                        promiseset[index] = new Promise( function( resolve, reject ) {  //95行目で生成した配列にPromiseを設定します。
                            client.folders.create(ankenid, val)             //24行目でclientとして承認されたbox Node SDKのfolderクラスのcreate関数を呼出 
                                                                            //1つ目は親フォルダーID(案件フォルダー)、2つ目は生成するサブフォルダーの名称
                                .then(function(results){                    //98行目の処理が成功したのでこのPromiseチェーンへ
                                    resolve(results.id);                    //97行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                                }).catch(function(error){                   //98行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                          //97行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                        });
                    });
                    Promise.all( promiseset )                               //97行目で設定した三つのPromiseが全て完了したらここに来る
                        .then( function ( message ) {
                        resolve(ankenid);                                   //84行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                    })
                        .catch( function ( reason ) {                       //97行目で設定した三つのPromiseのどれかが失敗したらここに来る
                            console.log( reason ) ; // "失敗!!"
                        reject(false);                                      //84行目のPromiseは失敗したとエラーオブジェクトを返す
                    });
                })
                .catch(function(error){                                     //85行目の処理は失敗したらここに来る
                    reject(error);                                          //84行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }

    // boxフォルダーIDは更新対象となるフォルダーID
    // createParamは更新フォルダー名と親フォルダーIDが含まれたJSONオブジェクト
    function putFolder(boxフォルダーID, updateParam) {             //165行目から呼び出されてフォルダー更新処理を実施
        return new Promise(function (resolve, reject) {         // Promiseを設定
            client.folders.update(boxフォルダーID, updateParam)    //24行目でclientとして承認されたbox Node SDKのfolderクラスのupdate関数を呼出
                                                                //1つ目は対象となるフォルダーID、2つ目は更新するフォルダー情報の含まれたJSONオブジェクト 
                .then(function(results){                        //126行目の処理が成功したのでこのPromiseチェーンへ
                    resolve(results.id);                        //125行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                })
                .catch(function(error){                         //126行目の処理が失敗したのでこのPromiseチェーンへ
                    reject(error);                              //125行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }

    function getFolderURL(createdid) {                          //178行目から呼び出されてフォルダーの共有処理を実施
        return new Promise(function (resolve, reject) {         // Promiseを設定
            client.folders.update(createdid, {shared_link: client.accessLevels.OPEN}) 
                                                                //24行目でclientとして承認されたbox Node SDKのfolderクラスのupdate関数を呼出
                                                                //1つ目は対象となるフォルダーID、2つ目は更新するフォルダーのプロパティ(共有設定)
                .then(function(results){                        //139行目の処理が成功したのでこのPromiseチェーンへ
                    resolve(results.shared_link.url);           //138行目のPromiseは完了したと戻り値の共有URLプロパティ(リンクURL)を返す
                })
                .catch(function(error){                         //139行目の処理が失敗したのでこのPromiseチェーンへ
                    reject(error);                              //138行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }
    function createBoxFolder(支店, 担当者, 案件No, boxフォルダーID) {  //212行目から呼び出されてフォルダーの共有処理を実施
        return new Promise(function (resolve,reject) {             // Promiseを設定
            var rootfolder = "12345678910";                        //boxのフォルダー制御のルートとなるフォルダーのフォルダーIDを静的に代入
            var ownerbranchfolder;                                 //支店フォルダーのフォルダーID
            var personinchargefolder;                              //担当者フォルダーのフォルダーID
            var createParam;                                       //searchFolder関数へはダミーオブジェクト。putfolderとpostfolderへはJSONオブジェクト

            searchFolder(支店, "folder", "name", 10, rootfolder, 'GET', createParam).then(function (branchfolderid) {  //45行目へ
                ownerbranchfolder = branchfolderid;                           //searchFolderからの返り値を上位スコープのownerbranchfolderへ代入
                searchFolder(担当者, "folder", "name", 10, ownerbranchfolder, 'GET', ["担当名",担当者]).then(function (personfolderid) {  //45行目へ
                    personinchargefolder = personfolderid;                    //searchFolderからの返り値を上位スコープのpersoninchargefolderへ代入
                    searchFolder(案件No, "folder", "name", 10, personinchargefolder, 'GET', createParam).then(function (projectfolderid) {  //45行目へ
                        var name = "案件No" + " " + 案件No;                    //生成/更新する案件フォルダーの名称を設定する
                        createParam = {name: name, parent: {id: personinchargefolder}};  //案件フォルダーの設定情報をJSONオブジェクトに組み立てる
                        if (projectfolderid.length > 0) {                     //161行目で案件フォルダーが存在した場合(フォルダー情報更新)
                            putFolder(projectfolderid, createParam).then(function (updatedid) {  //124行目へ
                                FolderId = updatedid;                         //161行目の処理で得た更新したフォルダーIDをスコープ外の168行で使うため
                                getFolderURL(updatedid).then(function (updatedurl) {             //137行目へ
                                    resolve(FolderId+"****"+updatedurl);      //151行目のPromise完了をフォルダーIDと共有URLプロパティ(リンクURL)で返す
                                }).catch(function(error){                     //167行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                            //151行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                            }).catch(function(error){                         //165行目の処理が失敗したのでこのPromiseチェーンへ
                                reject(error);                                //151行目のPromiseは失敗したとエラーオブジェクトを返す
                            });
                        } else {
                            postFolder(createParam).then(function (createdid) {       //83行目へ
                                FolderId = createdid;                                 //生成したフォルダーIDを以下の179行目で使うためにFolderIdに代入
                                getFolderURL(createdid).then(function (createdurl) {  //137行目へ
                                    resolve(FolderId+"****"+createdurl);      //151行目のPromise完了をフォルダーIDと共有URLプロパティ(リンクURL)で返す
                                }).catch(function(error){                             //178行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                                    //151行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                            }, function(res) {                                        //176行目のフォルダー生成処理でrejectレスポンスが返った場合
                                if (res.status && res.status === 409) {               //176行目のフォルダー生成処理でrejectレスポンスが409返った場合
                                    if (res.context_info                              //176行目のフォルダー生成処理でrejectレスポンスが競合を示した場合
                                        && res.context_info.conflicts
                                        && res.context_info.conflicts.length > 0) {
                                        return;                                       //150行目のcreateBoxFolder関数を終える
                                    }
                                }
                            }).catch(function(error){                                 //176行目のフォルダー生成処理でエラーが帰った場合
                                reject(error);                                        //151行目のPromiseは失敗したとエラーオブジェクトを返す
                            });
                        }
                    }).catch(function (error) {                                       //161行目のフォルダー検索処理でエラーが帰った場合
                        // 非同期処理失敗。呼ばれない
                        console.log(error);
                    });
                }).catch(function (error) {                                           //159行目のフォルダー検索処理でエラーが帰った場合
                    // 非同期処理失敗。呼ばれない
                    console.log(error);
                });
            }).catch(function (error) {                                               //157行目のフォルダー検索処理でエラーが帰った場合
                // 非同期処理失敗。呼ばれない
                console.log(error);
            });
        });
    }

    var recordjson = JSON.parse(event.body);                                 //34行目で受け取ったkintoneのWebhookのレコード情報をJSON形式で扱えるように
    updaterecordid_1287 = recordjson.record.レコード番号.value;               //210行目のレコードデータの「レコード番号」フィールドの値を代入
    createBoxFolder(recordjson.record.支店.value[0].code,                    //150行目へ
                    recordjson.record.担当者.value, 
                    recordjson.record.案件No.value, 
                    recordjson.record.boxフォルダーID.value).then(function(idurl) {
        if (idurl) {                               //212行目のcreateBoxFolderの戻り値(168、179行目で値設定)
            var targetrecordids = [updaterecordid_1287+"**"+APP_ID_1287];   //211行目で設定したレコード番号と27行目で設定したアプリID
            var kintonepromiseset = [];                                     //kintoneのレコードアップデートが終わるまで待つPromiseの配列を設定
            targetrecordids.forEach(function(val,index,ar){                 //217行目で生成した配列の各要素(本稿では1つ)をループします
                kintonepromiseset[index] = new Promise( function( resolve, reject ) {  //218行目で生成した配列にPromiseを設定します。
                    var body_post = {                                                  //kintoneの既存案件アプリを更新するレコードを組み立てます。
                        app: val.split("**")[1],                                       //217行目で設定した配列の**で区切られた右側(アプリID)
                        id: val.split("**")[0],                                        //217行目で設定した配列の**で区切られた左側(レコード番号)
                        record: {
                            boxフォルダーID: {
                                value: idurl.split("****")[0]                       //212行目のcreateBoxFolderの戻り値の****で区切られた左のフォルダーID
                            },
                            表示: {
                                value: idurl.split("****")[1].replace("*******.box.com","app.box.com")
                                         //212行目のcreateBoxFolderの戻り値の****で区切られた右のURL(契約のboxのサブドメインをapp.box.comに置換の必要あり)
                            }
                        }
                    };
                    var options_getsalesamount = {                            //リクエストのbody部分を組み立てます。
                        url: BASE_URL + 'record.json',                        //28行目で設定したURLのルートと一行レコードの更新なのでrecord.jsonを連結
                        method: 'PUT',                                        //更新なのでPUT
                        headers: headers_1287,                                //30行目で設定したAPIトークン
                        'Content-Type': 'application/json',                   //リクエストのボディ部分のタイプ
                        json: body_post                                       //221行目で設定したボディ部分
                    }
                    //レコードを取得
                    request(options_getsalesamount, function (error, response, body) {    //Node.jsのrequestモジュールで234行のリクエストを送信
                        if (error) {                                                      //242行目の値がerrorだったら
                            console.log('Error: ' + error.message);
                            reject();                                                     //220行目のPromiseは失敗したとエラーオブジェクトを返す
                        }
                        console.log("kintone recordput:succcess"+val);
                        resolve();                                                        //220行目のPromise完了を返す
                    });
                });
            });
            Promise.all( kintonepromiseset )                                              //220行目で設定したPromiseが全て完了したらここに来る(本稿は1つ)
                .then( function ( message ) {                                             //252行目の処理が成功したのでこのPromiseチェーンへ
                    context.done(null, {text: "kintone POST and Box Folder Create success!"});  //Lambdaの処理結果をログとして残す
            })
                .catch( function ( reason ) {
                    context.done(null, {text: "Box Folder Create failed!"});              //Lambdaの処理結果としてエラーログ
                return;
            });
        } else {
            context.done(null, {text: "Box Folder Create failed!"});                      //Lambdaの処理結果としてエラーログ
        }
    }, function(res) {                                                                    //212行目の返り値がrejectで戻ってきた場合
        context.done(null, {text: "Box Folder Create failed!"});                          //Lambdaの処理結果としてエラーログ
        return false;                                                                     //212行目の結果としてfalseを返す
    });
};

 

なんとか実装

  Topへ↑

いずれにせよ、私が2019年に出した唯一の大きなバグがこれでした。
結局、バグが出てから実運用にこぎつけるまでにさらに二カ月ほどの時間をいただきました。お客様にも多大なご迷惑をおかけしてしまいました。

これが実装できたことで、案件アプリにレコードを登録した時点で、Webhookが発動し、AWS API GatewayからAWS Lambdaを介してboxへのフォルダー生成と、レコードに対応するboxのURLとフォルダー番号をkintoneの該当レコードに登録することができました。

kintoneの画面上にboxのフォルダーを出す部分はbox for kintoneの内部にも書かれている通りです。
実際それを使わせていただいています。ありがとうございます。
以下にコードを載せていますが、疲れてきたのでコード内のコメントは割愛します。ごめんなさい。

(function() {
    'use strict';

    var BOX_CLIENT_ID = 'wkgp4k64whsha8mwvg7k5k63cim82mmv';   //sample_plugin_default
    // localStorage
    var LOCAL_STORAGE_PREFIX = 'kintone.plugin.' + BOX_CLIENT_ID;
    var LOCAL_STORAGE_JUDGED_ALLOW_ACCESS = LOCAL_STORAGE_PREFIX + '.judgedAllowAccess';

    var config = [];

    var BOX_EMBED_WIDTH = 840;
    var BOX_EMBED_HEIGHT = 420;

    var getUrl = function(path) {
        var matchedGuestSpacePath = location.pathname.match(/^\/k\/(guest\/\d+\/)/);
        var guestSpacePath = '';
        if (matchedGuestSpacePath !== null && matchedGuestSpacePath.length === 2) {
            guestSpacePath = matchedGuestSpacePath[1]; // "guest//"
        }
        var apiPath = '/k/' + guestSpacePath + path;
        return apiPath;
    };

    var boxApi = {
        clientInfo: {'provider': 'box', 'client': BOX_CLIENT_ID},

        getAccessToken: function() {
            // add a hash parameter for distinguishing OAuth redirect
            var delimiter = (location.hash.indexOf('#') === 0) ? '&' : '#';
            location.hash += delimiter + BOX_CLIENT_ID + '.oauth_redirect=true';
            kintone.oauth.redirectToAuthenticate(this.clientInfo, location.href);
        },
        hasAccessToken: function() {
            return kintone.oauth.hasAccessToken(boxApi.clientInfo);
        }
    };

    var validateConfig = function(record) {
        config['folderId'] = '0';//Box親フォルダーID
        config['keyFld'] = '顧客名';//kintoneキーフィールド
        config['boxUrl'] = '表示';//Box共有リンクの格納先
        config['boxFolderId'] = "boxフォルダーID";
        config['access'] = 'Open';//Box共有リンクのアクセス権[Collaborator/Company/Open]
        config['prohibitToDownload'] = 'false';//コラボレータにのみダウンロードを許可する

        if (!config) {return false; }
        return true;
    };

    var decorateBoxLinkField = function(boxUrl) {

        var boxLinkPattern = /^https:\/\/([a-zA-Z0-9]+).box.(com|net)(\/s\/[a-z0-9]+)$/;
        var match = boxUrl.match(boxLinkPattern);
        if (!match) {
            return;
        }
        var iframeSrc =
            'https://app.box.com/embed_widget/000000000000' +
            match[3] +
            '?theme=gray' +
            '&show_parent_path=no' +
            '&show_item_feed_actions=no' +
            '&partner_id=233';

        var elEmbed = kintone.app.record.getFieldElement(config.boxUrl);
        if (elEmbed === null) {return; }
        $(elEmbed).empty();

        var width = BOX_EMBED_WIDTH;
        var height = BOX_EMBED_HEIGHT;

        $(elEmbed).parent().css({
            'width': (width + 100) + 'px',
            'height': 'auto',
            'background-color': 'rgba( 255, 255, 255, 0 )'
        });
        var embedIframe = $('', {
            src: iframeSrc,
            width: width,
            height: height,
            frameborder: '0',
            allowfullscreen: 'true',
            allowscriptaccess: 'always'
        });
        $(elEmbed).append(embedIframe);
    };

    var judgedAllowAccessFlag = {
        isSet: function() {
            return (localStorage.getItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS) !== null);
        },

        set: function() {
            localStorage.setItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS, 'true');
        },

        remove: function() {
            localStorage.removeItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS);
        }
    };

    kintone.events.on('app.record.detail.show', function(e) {
        if (validateConfig(e.record)) {
            var boxUrl = e.record[config.boxUrl].value;
            if (!e.record[config.boxUrl].value) {

                var elEmbed = kintone.app.record.getFieldElement(config.boxUrl);
                if (elEmbed === null) {return null; }
                $(elEmbed).empty();

            } else {
                decorateBoxLinkField(boxUrl);
            }
        }

        return e;
    });

    var checkAccessToken = function() {
        var oauth_redirect_param = BOX_CLIENT_ID + '.oauth_redirect=true';
        if (location.hash.indexOf(oauth_redirect_param) !== -1) {
            judgedAllowAccessFlag.set();

            // remove a hash parameter
            location.hash = location.hash.replace(oauth_redirect_param, '');

            var t = setInterval(function() {
                if (location.hash.indexOf(oauth_redirect_param) !== -1) {
                    // cancel button was clicked
                    clearInterval(t);
                    location.href = getUrl(kintone.app.getId() + '/');
                }
            }, 500);
        } else if (!judgedAllowAccessFlag.isSet() || !boxApi.hasAccessToken()) {
            kintone.oauth.clearAccessToken(boxApi.clientInfo, function(body, status, headers) {
                boxApi.getAccessToken();
                return null;
            });
        }
    };

    kintone.events.on('app.record.create.show', function(e) {
        if (validateConfig(e.record)) {
            checkAccessToken();
            e.record[config.boxUrl]['disabled'] = true;
            e.record[config.boxFolderId]['disabled'] = true;
        }

        return e;
    });

    kintone.events.on('app.record.edit.show', function(e) {
        if (validateConfig(e.record)) {
            if (!e.record[config.boxUrl].value) {
                checkAccessToken();
            } else {
//                e.record[config.keyFld]['disabled'] = true;
                e.record[config.boxFolderId]['disabled'] = true;
            }
            e.record[config.boxUrl]['disabled'] = true;
        }
        return e;
    });

    kintone.events.on('app.record.index.edit.show', function(e) {
        if (validateConfig(e.record)) {
            e.record[config.boxUrl]['disabled'] = true;
            e.record[config.keyFld]['disabled'] = true;
            e.record[config.boxFolderId]['disabled'] = true;
        }
        return e;
    });
})();

 

 

まとめ

  Topへ↑

実案件ではさらに凝った実装(フォルダー数も階層も本稿の例よりさらに多い)が施されています。
そして、古くboxが設定されていないレコードには手作業がたまに発生しているものの、実運用に乗っています。
この記事ではそれ以上の情報を出すことはお客様の業務に関わるのでここまでにしとうございます。

本稿がkintoneを運用している皆様にとって少しの手助けになれば幸せです。

kintone上で大量の添付ファイルに困っていらっしゃる方や、社内ファイルサーバーからの移行でお困りの方。他のPaaSからkintoneへ移行する作業があって、添付ファイルの扱いにお困りの方。
弊社では本稿のようなboxとkintoneの連動事例を何例も手掛けております。お困りの際はおっしゃってくださいませ。

最後に蛇足ですが、boxの案件で例に挙げた三つのダムは、私が実際に訪れてダムカードを入手した場所です。


当エントリーの参考にさせていただいたブログ

  Topへ↑

最後になりましたが、このエントリー作成にあたり、以下の2サイトからの情報を参考にさせていただきました。ありがとうございました。

 box APIレファレンス
 AWS Lambda上でBox Node SDKを利用する-九龍堂雑録


コーチングのグラフってkintoneで出せるんやって!


kintone2 Advent Calendar 2019の5日目の記事です。

  Topへ↓

突然ですが皆さん、コーチングって聞いたことがありますか?
あっ! そこのあなた、ページはそのままに!
これは間違いなくkintone Advent Calendarの記事ですから。
ほら!

きとみちゃん楽しいですよね!
https://kintone.cybozu.co.jp/jp/kitomi/

日々、お仕事に励むきとみちゃん。
きとみちゃんとお仕事をする仲間はとっても個性が豊か。

ちょっぴりあわてんぼうでドジっ子のきとみちゃんがkintoneに救われる姿は微笑ましいです。
ちなみに私は巻物で見積書を出してくださる麻呂な方が好きです。この方のお名前はなんでおじゃる?

さて、きとみちゃんがお仕事をする上で助けになる手法はkintoneの他にもさまざまなものがあります。

その中の一つが冒頭に書いたコーチングなのです。

コーチングを一言で言い表すなら、
・相手の学習や成長、変化を促し、相手の潜在能力を解放させ、最大限に力を発揮させる。
でしょうか。

詳しくはWikipediaの「コーチング」
をご覧くださいませ。

ビジネスにフォーカスを当てたコーチングの歴史はまだまだ浅いです。
ここでお伝えしておかなければならないのは、自己啓発セミナーとは違う、ということです。

と、偉そうにウンチクを述べる私ですが、コーチングを受けた経験は人生で1,2回だけ。
では、そんな私がkintone Advent Calendarで何を語るというのでしょうか。

結論を先に書いちゃうと、kintoneでこんなグラフを作ってみましょう!
ということなんですね。

グラフとデータのご説明

  Topへ↑

上に登場したのは四つの傾向を円グラフにしたものです。
それぞれの傾向の文字列にマウスを合わせると、事前に登録しておいたキーワードが出てくる。
これ、実は以前、お客様に依頼されて作ったkintoneにChart.jsを組み込んだグラフ生成の仕掛けです。

私はコーチングには無知です。
ですから、kintoneに入力画面を作り、その結果を集計することで、設問に応じた四つの傾向が算出できる、ということを知ったときは新鮮でした。

お客様によれば、
相手をほめる場合の、個人に響くキーワードは4つの傾向に分けられる
だそうです。

それに合わせて、こんな入力画面を作ってみました。

仮に20問の設問としています。それぞれの4つの傾向ごとに5問を設問しました。
それぞれの問いごとに
・よく当てはまる
・当てはまる
・当てはまらない
・まったく当てはまらない
の4種類の答えをラジオボタンで設定しています。

もちろん、さらに設問数を増やすことも可能ですし、設問数を自在に増減させたい、というご要望もあるでしょうね。
その場合はサブテーブルを使えばよさそうです。
この記事ではサブテーブルではなく、20問に固定したバージョンでお届けしてみます!

実際の内容

  Topへ↑

はい。ではアプリの設定画面です。フォームはこんな感じ。

一番左の文字列フィールドは設問の文字列を入力します。
フィールドコードは上から順にquestion_1からquestion_20としています。

真ん中のドロップダウンフィールドは4つの傾向を選びます。
フィールドコードは上から順にtrend_1からtrend_20としています。

右のラジオボタンフィールドはそれぞれの答えを入力する欄です。
フィールドコードは上から順にanswer_1からanswer_20としています。

で、続いてはグラフを表示するカスタマイズビューを設定してみましょう。

こんな感じですね。

続いてはロジックです。
実は、このグラフを作るには以下の二つのJavaScriptファイルを設定するだけ。

上に設定したのは、Chart.jsです。
Cybozu Developer Network
からCDNのページに移動してもらえれば。

そこのChart.jsに書かれているURLをコピーし、上の画面の
から

に貼って保存するだけ! きとみちゃんでもできますよね?
htttps://がダブらないようにだけ気を付けて!

続いてグラフ表示のロジックです

  Topへ↑

では続いてきとみちゃんとグラフ.jsの内容を。
ここからはVisual Studio Codeの画面にコメントを入れています。




ちょっと見にくいので、直に貼ったコードも提供します。右にスクロールしてくださいね。

(function () {
  "use strict";

  // 一覧ページ
  kintone.events.on('app.record.index.show', function(event) {                       //一覧画面表示時の定型文です
    var record = event.records[0];
    var itemcount = 20;
    var 1_Score = 0;
    var 2_Score = 0;
    var 3_Score = 0;
    var 4_Score = 0;
    var selectedScore = 0;
    var dataLabelPlugin = {                                                          //ここは以下の162行目で呼び出されるチャートのプラグインコンフィグで呼び出される部分です。
      afterDatasetsDraw: function (Chart, easing) {                                  //afterDatasetsDrawとはプラグインコアAPIとして呼び出されるChart.js内部のフックです。要は描画後です。
        var ctx = Chart.ctx;                                                         //チャートが描画されている対象のDOM要素です。157行目で定義され、159行目でChartオブジェクトに渡されます。
        Chart.data.datasets.forEach(function (dataset, i) {                          //対象チャートをループしています。データは77行目で一種類で指定していますのでループは一回のみです。               
          var meta = Chart.getDatasetMeta(i);                                        //チャートのメタデータを取得しています。データやラベルも含まれています。
          if (!meta.hidden) {                                                        //チャートのhiddenプロパティがTrueの場合そもそもチャートが描画されません。
            meta.data.forEach(function (element, index) {                            //メタ要素のデータをループします。今回は4種類ですね。
              ctx.fillStyle = 'rgb(0, 0, 0)';                                        //円グラフの中の文字の色です。rgb(0, 0, 0)は黒を表しています。
              var fontSize = 16;                                                    //36-37行目で文字の場所を設定するためのフォントサイズを16pxで設定しています。表示フォントのサイズとは別に。
              ctx.font = "24px \"Helvetica Neue\", Helvetica, Arial, sans-serif";    //これが実際に描画される文字のフォント情報です

              var sum  = function(arr) {                                             //ここでは対象となるデータの合計値を返します。4種類のデータの合計です。
                  return arr.reduce(function(prev, current, i, arr) {
                      return prev+current;
                  });
              };
              var percentString = ((dataset.data[index] / sum(dataset.data))*100).toFixed(1) + "%";  //それぞれのデータの値を全体の合計で割り、パーセントの文字列を構築します。
              var dataString = Chart.data.labels[index];                                             //それぞれのデータのラベルです。79行目で定義した4つの傾向のラベルですね。 
              ctx.textAlign = 'center';
              ctx.textBaseline = 'middle';

              var padding = 5;
              var position = element.tooltipPosition();
              ctx.fillText(dataString, position.x, position.y - (fontSize / 2) - padding);           //30行目で設定したラベルの値を計算した位置に表示します。
              ctx.fillText(percentString, position.x, position.y - (fontSize / 2) - padding + 35);   //29行目で設定した値のパーセントの文字列を計算した位置に表示します。
            });
          }
        });
      }
    };
    for (var i=1 ; i<=itemcount ; i++){                                                              //ここから76行目までは大人の事情でいろいろとあいまいですがお許しを
      switch( record['answer_' + i]['value'] ) {                                                     //要するに20レコードの設問の答えを基に四つの傾向に加算しているのです
        case 'よく当てはまる':
          selectedScore = 係数は内緒よ♪;
          break;
        case '当てはまる':
          selectedScore = 係数は内緒よ♪;
          break;
        case '当てはまらない':
          selectedScore = 係数は内緒よ♪;
          break;
        case 'まったく当てはまらない':
          selectedScore = 係数は内緒よ♪;
          break;
      }
      switch( record['trend_' + i]['value'] ) {
        case '一つ目の傾向':
          1_Score = 1_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;           //要するに20レコードの設問の答えを基に四つの傾向に重みづけしているのです
          break;
        case '二つ目の傾向':
          2_Score = 2_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
        case '三つ目の傾向':
          3_Score = 3_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
        case '四つ目の傾向':
          4_Score = 4_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
      }
    }
    1_Score = ロジック関数は内緒よ♪(1_Score);
    2_Score = ロジック関数は内緒よ♪(2_Score);
    3_Score = ロジック関数は内緒よ♪(3_Score);
    4_Score = ロジック関数は内緒よ♪(4_Score);                                          //さらに四つの傾向に値を秘密ロジックで精緻化しています。この辺も大人の事情が絡んでいます。
    var pieChartData = {                                                             //161行目でChartオブジェクトに渡されるデータとラベルと背景色のホバー色や枠の組み合わせです。四要素です。   
      labels : ["リーダー合理系","アイディア活動系","ヘルプ支援系","クール分析系"],       //ラベルですね。四つの要素に分かれています。
      datasets : [                                                                   //四つの要素のそれぞれの色の指定です。
        {
          backgroundColor: [
            '#ff6384',
            '#36a2eb',
            '#cc65fe',
            '#ffce56'
          ],
          hoverBackgroundColor: [
              "#FF2384",
              "#3662EB",
              "#cc25fe",
              "#FF8E56"
          ],
          hoverBorderColor: [
              "#000000",
              "#000000",
              "#000000",
              "#000000"
          ],
          hoverBorderWidth: [
              2,
              2,
              2,
              2
          ],
          data : [1_Score,2_Score,3_Score,4_Score]                                   //四つの要素の値です。大人の事情で実際の回答から複雑に計算された結果が格納されます。
        }
      ]
    }
    var tooltipkeyword = {                                                           //ここは四つの傾向ごとに176行目で乱数を設定し、任意のキーワードを表示するようにしています。
      type : [
        {
          word : [
            '同業者もあの人を噂している',
            '他の部署でも話題になっている',
            '○○さんしかできない',
            '自分で判断し、動ける人',
            '部署のメンバーに信頼されている',
            'あのひとには任せられる'
          ],
          title : "任せる、難題、未知の分野、他に頼めない、誰にもできない"
        },
        {
          word : [
            '発想がおもしろい!!',
            '一緒にいるだけで楽しい!!',
            'さすがアイデアマン!!',
            'すばらしいサービス精神!!',
            'うちの部署のムードメーカー!!',
            'その自由な発想がうらやましい!!'
          ],
          title : "自由にして、思いっきり、楽しく、面白く、みんなでいっしょ"
        },
        {
          word : [
            'みんなが働きに感謝している',
            '縁の下の力持ち',
            '一緒にいて落ち着く',
            '丁寧で親切で信頼できる',
            '細かいところによく気が付く',
            '相手の気持ちを分かってくれる'
          ],
          title : "感謝、ありがとう、仲良く、話し合い、相手の気持ち"
        },
        {
          word : [
            '詳しく業務を理解している',
            '商品のことをよく知っている',
            'わが社のことになんでも詳しい',
            'うちの課の歩く辞書',
            'あの人に聞けば間違いない',
            'このデータ量は大したもの'
          ],
          title : "情報、正確、正しく分析、予定通り、計画通り"
        }
      ]
    }
    var canvas = document.getElementById('canvas').getContext("2d");                 //Chartが描画されるDOM要素を指定するChart.jsの定型文です。id="canvas"はカスタマイズビューで指定しました。
    canvas.canvas.height = 256;                                                      //描画される領域の高さを指定しています。
    var test_chart = new Chart(canvas, {                                             //ここでChartオブジェクトをインスタンスとして実体化させています。
      type: 'pie',                                                                   //type: 'pie'はグラフの種類ですね。円グラフです。
      data: pieChartData,                                                            //77行目で定義したデータの実態です。
      plugins: [dataLabelPlugin],                                                    //プラグインコンフィグで関数を呼び出すことができます。その関数は13行目をご参照ください。
      options: {                                                                     //ここからはオプション情報です。
        animation: {
          animateRotate: true,
          animateScale: true
        },
        tooltips: {
          titleFontSize: 48,
          bodyFontSize: 36,
          callbacks: {
            label: function (tooltipItem, data){                                     //ここは描画後にマウスカーソルが乗った時の事前に内部でtooltipItemに定義された情報を基に値を返します。
                return pieChartData["datasets"][0]['data'][tooltipItem['index']] + "ポイント"       //77行目で定義されたデータから該当するデータを表示し
                  + "  キーワード → " + tooltipkeyword["type"][tooltipItem['index']]["title"];      //さらにキーワードとして109行目で定義された四つの傾向のタイトルを表示します。
            },
            afterLabel: function (tooltipItem, data){                                //172行目のラベルの後に別の情報を表示させるにはafterLabelツールチップコールバックが呼び出せます。
                  return "「" + tooltipkeyword["type"][tooltipItem['index']]["word"][Math.floor(Math.random()*(6-0)+0)] + "」";
            }                                                                        //さらにテキストとして109行目で定義された四つの傾向の文言のオブジェクトから乱数で選ばれた文言を表示します。
          }
        },
      }
    });
  });
})();

あとはこのJavaScriptファイルを

にのようにアップロードしていただければ。

どうでしょう。kintoneのデータにChart.jsを組み合わせるだけで、
kintoneのデータを分析することができてしまうのです。

Chart.jsにはさまざまなグラフが用意されているので、
kintoneの標準グラフでは表現できないことも可能です。

コーチング用の分析ツールとしても使えてしまうkintoneの奥深さを楽しんでいただけたらきとみちゃんも喜ぶはずです!
よかったら以下にChart.jsの公式サイトのリンクも貼っているのでご参考になさってくださいね。

当エントリの参考にさせていただいたブログ

  Topへ↑

最後になりましたが、このエントリ作成にあたり、以下の2サイトおよび、コーチングについて教えて頂いたお客様からの情報を参考にさせていただきました。ありがとうございました。

 Chart.jsドキュメント翻訳
 Chart.js公式サイト


kintoneのレコード詳細ページからAmazon商品データを参照するテクニック(3/3)


3.kintoneからのJavaScript呼び出しとJavaScript内でのAjaxの記載について

 ここでは、kintoneへのJavaScriptの適用方法その他の説明は割愛します。はじめよう kintone JavaScript API の記述が大変参考になります。私も最初はここから勉強しました^^

 ただし、本稿でもまた、前書きを書かねばなりません。恐縮ですm(_ _)m

 それは、kintoneからAmazon API呼び出しのタイミングについてです。冒頭でも挙げたように、本稿ではレコードの詳細画面でAmazon情報を呼び出すのが目的です。しかし、レコード詳細画面の編集時画面での実装については、本稿では触れていません。できなくもないのですが、現在の2014年9月時点のkintone JavaScript APIでは、レコード詳細画面のテキストボックスの値変更時にJavaScriptを呼び出すことができません。そのため、編集時画面でAmazonからの情報を表示させる意味がないと判断し、記載から外させて頂きました。ご了承下さい。ただし、レコード編集画面のフィールド値変更時イベントについては、2015/07/12の定期メンテナンスにおけるkintone API更新で実装されました。が、本稿ではまだ検証に至っておらず、掲載しておりません。ご了承ください。

 また、Amazonからの呼び出し自体にも、全く問題がない訳ではありません。Amazon側にリクエストを続けて投げると、Http/1.1 Service Unavailableのエラーが返ってくる場合があります。なので、リクエストはほどほどに・・・ということなのでしょう。このエラー対策については、jQueryのAjaxで、Errorハンドラから再帰呼び出しを試したのですが、calleeとuse strictが両立できていません。なので、時間をおいて再実行ということでご了承ください。

 最後にクロスドメイン制約についてです。本稿の目指す実装は、*****.cybozu.com から ****.jp/ または *****.co.jpといった違ったドメインのサーバーにAjaxリクエストを発行します。が、そのような別ドメインへのリクエストは、セキュリティ上脆弱になりかねないため、環境によっては簡単に通信ができないようになっています。本稿ではphpからkintoneへ渡すデータ形式をXMLにしました。JSONPを使えばクロスドメイン制約についても解決できるようです。が、今回はXMLを選択しました。ご了承頂ければと思います。随所にそのあたりについて

 あと、jQueryの呼び出しも必要となりますので、kintoneのアプリ設定画面から、JavaScript登録を忘れずに。

 さて、気を取り直して実装開始^^

 以下は、AmazonCallBookInfoDetail.jsの内容です。200行あるすべてのコードを開示しながら、適宜解説を加えていきたいと思います。

  4行目     jQuery.support.cors = true; はクロスドメイン制約をかいくぐるための呪文の一つです。

  5行目     kintone.events.on('app.record.detail.show', function(event){
は詳細レコード呼び出し時に書く呪文です。

  6行目     実行しているブラウザの種別をここで取得しています。呼び出し先のURLがhttpsから始まっても、画像はhttpが返されることがあり、それを防ぐために一部ブラウザによって画像のアドレスを変更しています。

  8行目     表示したkintone詳細レコードのISBNフィールド(フィールド名はF_Isbn)の値を取得しています。

  9行目     8行目で取得したIsbnから – ハイフンを除去しています。

  10行目     8行目で取得したIsbnから  ISBN を除去しています。

  11行目     kintoneフォーム上で事前に作成しておいたスペースをここで取得しています。このスペース内にAmazon からの情報を書き込んでいきます。

  12行目     kintoneフォーム上で事前に作成しておいたスペースをここで取得しています。このスペース内にAmazon からの大きな画像を表示させます。

  15-17行目 本稿で実装する内容では、ユーザ入力データからのサニタイズ処理はあまりありません。唯一この場所で実施しています。数値がどうかをチェックしていますが、ISBNの末尾一桁のチェックディジット結果が X となる場合があり、その場合のみ許可しています。

  19行目     12行目で取得した画像表示用イメージの横幅を指定しています。

  20行目     11行目で取得したAmazonデータ表示用スペースにユーザを飽きさせないために表示しております。

 

 

  24行目     先に紹介しましたが、codeとIsbnという2つのパラメーター以外に、nowというパラメーターも追加しています。実はnowパラメーターは以下の処理では使われていません。投げるリクエストURLを常に変化させないとInternet Explorer上でうまく値が戻ってこないという情報から、このような記載を設けています。

  25行目     ここは xml を指定しています。おそらくはjsonpでもjsonでもうまくいくことでしょう。

  26行目     キャッシュをOFFにしないとInternet Explorerで正常に戻ってこないという情報を基に追加しました。

  27行目     非同期処理をONにしています。ここをOFFまたは記載なしにすると、何らかのエラーがAjaxで発生した場合、ブラウザがフリーズしてしまいます。

  28行目     10000ミリ秒、つまり10秒間だけphpからの戻り値を待ちましょう。という指定です。

  29行目     正常にデータが戻ってきた場合の処理を30行目以下に記載します。関数の引数dataが、成功した場合に受け取るデータが格納された変数です。

 

 

  30-79行目     商品情報に関するすべてのAmazonからの戻り値を変数として宣言してあります。正直、ここまでやる必要はありません。ありませんとも。

  82行目以下は、実際のXMLの内容を基に、DOMの森を探索することになります。なので、以下ではXMLを実際に表示させてみます。

  この中で実際に表示させるのに必要なのは、32行目の<Item>タグ以下のデータとなります。

  53-101行目までは各種商品イメージの情報が記載されています。この中で53行目から67行目までのSmallImageとMediumImageとLargeImageは。その下の68行目から101行目までの間に同一の情報があります。なので、この部分は取得を省略してもよいでしょう。

  102行目以下は主要な情報が並んでいます。これらはほぼ取得すべきところです。ただし、データの種類によってはタグ自体がないものもあります。以下の例でいうと、104行目の<Creator>タグは小説のように著者がある本には登場しません。著者がある本は<Author>タグが替わりをつとめます。また、112行から117行の<Languages>タグや135行目の<ReleaseDate>タグがないデータがあることも確認しています。

 もう一件、AmazonからのXMLデータで気を付けるべき点があります。それは書籍とkindle書籍の場合です。kindle書籍はISBN項目を持っていないのですが、Amazonの仕様でISBNに対してデータ検索結果にkindleデータも含まれます。XML上では、2つの<Item>タグが紐付いています。その場合は、JavaScript側で<Item>タグ内をループさせるのですが、<ISBN>タグの有無で判別しています。

 大体のXMLデータの構造がおわかり頂けましたでしょうか。先に紹介したAmazonのガイドですが、項目の位置はこちらでガイドされています。ただし、各項目がどのように商品データに関連付いているかはわかりません。なので、上に挙げた項目以外にも存在することもあるかと思います。ご容赦ください。

  82行目     jQueryの文法に則り、XMLのデータが格納された変数dataに対し、<item>タグの要素を検索しています。その結果をループしたのが82行目の記述となります。

  83行目     条件文が真の場合、84行以下の処理を実行しています。条件文の頭に$(this)とありますが、これは 82行目でループを開始した<item>タグの要素データを指します。<item>タグ内でさらに<ItemAttributes>タグを検索し、さらにその中で<ISBN>タグを検索しています。この構造については、3ページ前のXMLをご参照ください。<ISBN>タグの要素内のテキストを取得しているのが text()です。
つまり、ISBNの要素の有無によって判定を分けています。これは先にXMLの構造を説明する際にも書きましたが、kindle書籍データが含まれている場合の退避処理となります。

  84-85行目     <item>タグの要素データ内の<ASIN>タグや<DetailPageURL>タグの要素内データを取得しています。

  86行目     82行目で始めた<item>タグ内ループの中で、さらに<ItemLink>タグを検索し、さらに<Description>タグを検索し、それをループしています。

  87行目     86行目で始めた<Description>タグ内ループの中で、要素内の文字列が Add to Wish Listに等しいかを尋ねています。

  88行目     87行目で条件に合致した場合、<Description>タグと同じレベルの隣のタグ<URL>タグの要素内のデータを取得しています。

  90-98行目 87、88行目と同様です。それぞれの<URL>タグの内容を取得しています。

  99行目     86行目で始めたループを閉じています。

 

  100行目     82行目で始めた<item>タグ内ループの中で、さらに<ImageSet>タグを検索し、それをループしています。

 

  101-118行目    100行目で始めた<ImageSet>タグ内ループの中で、それぞれの要素タグの内容を取得しています。

  119行目     100行目で始めたループを閉じています。

 ここまで来たら大分要領がつかめてきたのではないでしょうか。あともう少しで終わりです。お疲れのことでしょう。
私も大分疲れてきました・・・(^^;)

 120-146行目     それぞれの要素内データを取得しています。もう説明は不要ですよね。ただ、121行目ですが、Attrという見慣れない記述があります。この部分に対応しているのは XMLの104行目に当ります。<Creator>タグ内に Role="監修"というRole属性の値を取得するために、Attrという命令を発行しています。これを末尾につなげることで、 サイボウズ式編集部 監修 という文字列を組み立てているわけです。

 

  147行目     83行目の<ISBN>タグの有無を判定したif文の終わりです。

 

  148行目     82行目で始めたループを閉じています。

  149行目     aws_ASINの値が真かどうかを判定しています。cybozu.com developer networkのTipsの中に(小技)undefined と空文字のスマートな if 文判定がありますが、そちらの記述通り、文字列の取得結果がnull または undefined以外のデータの場合、150行目以下の処理を実行します。

  150-173行目   11行目で取得したスペースに対し、innerHTMLを使用して書き込むべき文字列を生成しています。今回の実装では、一つのスペース内に改行で区切ってAmazonからの情報を羅列する手法を採りました。冒頭に記載しましたが、編集時画面に実装し、Isbnの値の変更時に各フィールドに自動入力するほうが望ましいという考えもあります。なお、169行目から173行目は取得したURL文字列を<a>タグの中に含めています。

  174行目     6行目でブラウザの種別を取得しました。取得したブラウザ文字列にfirefoxが含まれているかどうか、要は実行しているブラウザの種類を問うているのがこの行です。

  175行目     使用ブラウザがfirefoxの場合、12行目で取得したスペースのinnerHTMLに対して書き込む画像の http: を 空白文字 にしています。Firefoxだけが、https:// から https:// の画像を読み込もうとするとエラーが出るため、このような処置を取っています。

  176-178行目     使用ブラウザがFirefox以外の場合、12行目で取得したスペースのinnerHTMLに対して書き込む画像のURLをそのまま使用しています

  179行目     149行目で判定したaws_ASINの値が偽だった場合に、180~184行目の処理が動作します。なお、Amazon側に渡したISBNコードが不正な値だった場合、XMLは違う種類のメッセージを返します。ほとんどの部分は一緒なのですが、<Items>タグの中の<Item>タグが<Errors>タグに置き換えられています。以下にエラー時のXMLを提示します。

 

  180-184行目     11行目で取得したスペースにエラー情報を含めます。上の図にあるとおりのエラー構造に基づいて、適宜整形して頂ければと思います。

 

  185行目     149行目で判定した条件式の終わりの部分です。

  186行目     29行目で判定したAmazonから正常な値が戻ってきたかどうかの判定式の終わりの括弧です。

  187行目     Ajax通信に何らかのエラーが生じた場合、処理はこちらのステートメント内で行われます。関数の引数として、XMLHttpRequest、textStatus、errorThrownといった値が返されますので、その値に応じてメッセージを整形されるとよいでしょう。

  193-195行目     errorThrownは場所の数だけ配列として戻ってきますので、193~195行では配列の要素数だけ処理を行っています。

  196行目     ここでは取得したエラー情報をアラートとしてポップアップ表示しています。Amazon情報を表示するスペースに表示するのもよいでしょう。

  197行目     187行目からのエラー時に実行される関数の終わり部分です。

  198行目     22行目からの$.ajaxの関数の終わりの括弧です。

  200行目     5行目からのkintone詳細レコード表示時に実行される関数を終えるにあたり、戻り値を返しています。

  201行目     5行目からのkintone詳細レコード表示時に実行される関数の終わりの括弧です。

  203行目     本JavaScript全体の無名関数の終わりの括弧です。

これでJavaScriptの実装は終了です。お疲れ様でした。お互い・・・

 さて、JavaScriptを完成させ、あとはこれをkintoneのアプリ管理画面からアップすれば完成!

 のはずですが・・・・・実は先ほどのJavaScriptの仕組みでは動かないブラウザがあります。それは。。。。Internet Explorerです。jQueryのAjax関数では、Internet Explorer 9より前のバージョンに対応しておらず、別の仕組みを経由してAjaxを実装する必要があります。なんてこった!またまたクロスドメインではまることになりました。

 Googleで検索を行うと、いくつかInternet Explorer 9以前のバージョンでのクロスドメイン実装についてのJavaScriptが存在します。私もいくつか試しましたが、私の環境ではそのうち動いたのが以下の1つだけでした。

 MoonScript/jQuery-ajaxTransport-XDomainRequest

 なので、今回はこちらのJavaScriptも使わせて頂きました。ありがとうございます。

 結果として、kintone上にアップするJavaScriptファイルは3種類となりました。最初にjQuery本体。次にIE9以前対応のクロスドメイン制約対応のJavaScript。最後に今回作成したJavaScriptです。

 これで完成です。

 と。このように表示が出来ましたでしょうか。レコードを移動すると新たなレコード先でAmazonから呼び出されたデータが表示されます。ただし、あまり短い時間で呼び出すとAmazonからの応答が途絶えてしまうことがあります。その場合は F5でリロードを行って頂ければと思います。

 外部認証が必要な複雑なAPIを呼び出す場合、このような方法があるよ、ということを理解頂ければ、さまざまなケースで実装が可能になり、kintoneをより一層ご活用頂くことができることでしょう。ぜひ、お試しください。ただしくれぐれも過度なAmazonへのリクエストはお控えください。1時間当たりのリクエスト回数も上限が定められているようですし・・・・

 本件について、文責は全て私、合同会社アクアビットの長井にあります。もし内容についての御質問などございましたら、お寄せいただければ、時間の許す限り、回答させて頂くようにいたします。


kintoneのレコード詳細ページからAmazon商品データを参照するテクニック(2/3)


2.phpを利用したAmazonへのパラメーターの生成方法について

 最初に一言申し添えておかねばなりません。なぜkintoneからAmazonの情報を取得するのにphpを介さなければならないか、について。

 cybozu.com developer networkをご覧の皆様は、すでに有識者の皆様が執筆された、秀逸なTipsの数々にも目を通されたことでしょう。Tipsの中には

  外部APIの同期処理をつかってみよう!

  他サービスのAPIからデータを取得してkintoneに表示してみよう

 という2つのkintoneから外部APIに接続するTipsも挙げられています。私も本稿を書くにあたって参考にしました。ありがとうございますm(_ _)m。

 これらのTipsはkintoneのJavaScript APIを使用しています。JavaScript内に外部APIのURLを直接書き込み、その結果を表示させる方法です。一方、Amazon APIを呼び出す際のURLですが、その中には全章で取得したAmazonの「Access Key ID」「Secret Access Key」「トラッキングID」の3種類から生成した署名文字列を含める必要があります。つまり、署名文字列を生成するにあたっては、JavaScript内に「Access Key ID」「Secret Access Key」「トラッキングID」という機密情報を埋め込まねばなりません。それを避けるために、外部のphpに署名文字列の生成や機密情報の扱いを任せようというのが狙いです。

 能書きが長くなりました。それでは実装に移らせて頂きます。

 まず、kintoneに埋め込むJavaScript内で呼び出すAPIのURLを記します。

パラメーターは ?code=ISBN&no='+Isbn としています。Isbnとあるのは、kintoneのフィールドに入力された本のIsbnを変数化したものです。Amazon認証情報に関する情報の一切はJavaScriptからは省かれています。

 次にパラメーターを受け取ったAmazonBookInfo.phpの中身を示します。

 JavaScriptに値を返すための各種ヘッダの記述と、Amazonから情報を取得する本体のphpであるSetBookByAmazon.phpの呼び出しが書かれています。ヘッダについてはGoogle ChromeとFirefox、そしてInternet Explorer 9で正常にデータが渡せることが確認できています。これは環境によって違いますので、工夫してみてください。特に4番目のヘッダで application/xml となっています。これはJavaScriptに対して渡すデータの種類を xmlとして規定する部分ですから、重要です。

 最後にパラメーターを受け取ったSetBookByAmazon.phpの中身を示します。

 これで動くはずです。あ、Access Key IDとSecret Access Key IDとアソシエイトTagはこんなふざけたものではなく、きちんとAmazonで取得した文字列を使って下さいね^^。

  define('AssociateTag','anatanoiddesu-99'); の赤字の部分を前章で取得した「トラッキングID」で置き換えてください。

  define('AccessKeyId','ICHIBANSAISHINNOAKUSESUKII');  の赤字の部分を前章で取得した「Access Key ID」で置き換えてください。

  define('SecretAccessKey','DAIJINADAIJINASIIKURETTOAKUSESUKIIDESUNENN');   の赤字の部分を前章で取得した「Secret Access Key」で置き換えてください。

 それ以外は、この内容でデータが取得できるはずです。このphpをサーバーにアップし、URLをアドレス欄に打ち込んでみてください。例えば以下のように。

 どうでしょう。XML形式でデータが表示されたのではないでしょうか。そう、あの本の情報が\(^o^)/

 なお、渡すパラメータや取得できる情報の種類はかなり多岐に亘っています。本稿ではISBNのみに焦点を当て、ISBNから取得可能な書籍情報のみについて紹介したいと思います。上に挙げたphpの内容もISBNによる取得に対応しています。Amazon Products Advertising APIの単一商品取得の ItemLookUpはこちらのリンクが公式情報となっています。

 上のphpの内容に ・・・・・・あ といった記述があります。くれぐれもこの部分はphpファイルからは除いてくださいね。動かなくなってしまいますので^^。

 以下は、その あ~か についての説明を記します。

 ・・・・あ その上の行と共にJavaScriptから渡ってきた変数が格納される場所です。

 ・・・・い Amazonから取得する情報の種類を指定します。ItemAttributesは商品詳細、Imagesは画像情報。このように複数の種類を指定する場合 , カンマでつなげます。

 ・・・・う 指定した各種パラメーターをそのパラメーター名称で並び替えます。署名作成時に必要な処理です。

 ・・・・え 指定した各種パラメーターをエンコードします。RFC3986で定められた方法に準じています。

 ・・・・お 署名文字列の生成部分です。ここで初めてSecret Access Keyの活躍の場が与えられます。ハッシュ関数によって、容易には結果から元々の文字列が推測できないような文字列に変換されます。

 ・・・・か 組み立てられたUrlから取得した文字列を取得します。結果文字列はXML形式で帰ってきています。

 ・・・・き 帰ってきた文字列を表示します。この表示処理によって、呼び出し元へ文字列が戻されます。

 これで、Access Key ID と Secret Access Key、トラッキングIDの3種類の情報とISBNからAmazonの書籍情報がデータ取得できました。あとはこれをJavaScriptで加工するだけです。あと一息です!もうしばらくご辛抱ください!!

 最後に、こちらで作成した SetBookByAmazon.php と AmazonBookInfo.php ですが、サーバーにアップする際、https://の領域ではなく、https:// の方にアップされたほうが良いかと存じます。

なぜかというと、呼び出し元となるkintoneのURLは https://******.cybozu.com/k/12/ とhttps://から始まります。これに対し、上記2つのphpファイルが https:// から始まる領域から始まると、ブラウザによっては好ましからざる動きをするからです。


kintoneのレコード詳細ページからAmazon商品データを参照するテクニック(1/3)


はじめに

kintone アプリで資産管理を行いたい、という要望はよく聞きます。その際、既存サイトのデータが参照できたら便利ですよね。例えば商品データを参照したい場合、Amazon APIを通して取得したいといった要件は十分考えられます。本稿では、kintoneの詳細レコード上に、Amazon APIから取得したデータを表示する方法について紹介します。

少々長くなってしまうので、簡単に本稿の構成を書いておきます。

1.AmazonのAccess Key ID と Secret Access Key、トラッキングIDの取得方法について

2.phpを利用したAmazonへのパラメーターの生成方法について

3.kintoneからのJavaScript呼び出しとJavaScript内でのAjaxの記載について

その前に・・・本件を実現するために必要なものがあります。それは例えば・・・

  1. パソコン。ノートでもデスクトップでもよいですが、スマートフォンや俗にいうガラパゴス携帯、またタブレットからの利用は考慮していません。
  2. パソコンのブラウザ。本件ではGoogle Chrome、Firefox、Internet Explorer9、11で検証しています。その他のブラウザでは予測不可能な動作に遭遇するかもしれません。もちろん、他のブラウザでも動けばそれに越したことはありません。ノウハウはぜひ教えて頂ければ^^;)・・・。とりあえずこの4種でお願いします。
  3. phpの動作するサーバー。今回の検証で使用したレンタルサーバーは、php.iniも変更できず、pearなどのライブラリも利用できません。SSLも専用でははく共用SSLを使用しています。phpネイティブだけを頼りに構築しているので、サーバー間の動作差異はあまりないと思えます。なお、レンタルサーバのOSはUnix系で、Apache 2.2で動いているそうです。phpのバージョンは5.3.2です。
  4. kintoneの画面ですが、最低限3つのフィールドが必要です。Isbnを入力するためのフィールド(名称:F_Isbn)を追加し、Amazon情報表示用のスペース(名称:Space_Image)と画像表示用のスペース(名称:Large_Image)を追加してください。
    表にまとめると以下のような感じとなります。

    フィールドタイプ フィールド名 フィールドコード 説明
    文字列(1行) ISBNコード F_Isbn ISBNコード用
    スペース Space_Image Amazon情報表示用スペース
    スペース Large_Image 画像表示用スペース

    (※以下の結果画面は違うフィールドも多数入っていますが)

    それらを満たし、かつ、kintoneにカスタマイズを加えると、以下のように詳細画面にAmazonからの呼び出し結果が表示できるようになります♪

最後になりますが、本稿は2014/10月に書かれました。その時期のkintoneとAWSの仕様に合わせています。cybozu.com developers networkへの掲載に合わせ、サイボウズ社のご担当者のチェックを経て、2015/2月のkintoneのバージョンアップ内容と本稿の記載を照合させた上で投稿しています。2015/7現在、まだ同じ仕組みで動作しているので、本稿の内容に問題ないとは思われますが、今後のkintoneおよびAWSのバージョンアップによっては本稿の内容が正常に動作しない恐れもあります。ご承知の上、ご参考になさって下さい。

1.AmazonのAccess Key ID と Secret Access Key、トラッキングIDの取得方法について

 https://www.amazon.co.jpにアクセスしましょう。まずはここから。

 次にアカウントを既にお持ちの方はログインし、まだの方はアカウントの新規作成をお願いします。

 アカウントでログインすると、下の図の赤枠が こんにちは ○○○さん という表示になっています。

 その赤枠部分をクリックすると、アカウントの管理画面に移動します。

管理画面では、アフィリエイトの報酬結果を見たい気持ちをこらえ、ページの下端まで移動してください。すると下図のような記載が見つかると思います。その中のアソシエイト(アフィリエイト)をクリックしてください。

 下図は、アソシエイト・セントラルの画面です。左上にIDのようなものが書かれています(赤枠囲み)。これがkintoneからAmazonへアクセスする際に必要な情報の一つ「トラッキングID」です。是非φ(..)メモしておいてください。

 続いて、上図のメニューバナーの中に Product Advertising APIというのがあります。そのバナーをクリックしてください。Product Advertising APIのページに移動します。

 Product Advertising API のページ内にはAmazonへのAPIアクセスの奥義が沢山書かれています。是非 お読み頂ければと思います。が、ここは先を急ぎましょう。

 ページの中ほどに、下図のようなリソースというコーナーが設けられており、その中にアカウント設定というリンクがあります。これをクリックします。

 アカウントサービスのページに移動しました(下図)。アクセスキー情報という枠の中に、こちらのリンクというリンクがあります(下図の赤枠です)。これをクリックしてください。

 ダッシュボードのページに移ったと同時に、ダイアログボックスが登場します(下図)。Amazonでは、アカウント管理についてはIAMというサービスを推奨しています。きめ細やかな各種権限設定が可能になっているようです。しかし、今回のAmazonへのAPIに当っては、最初にAmazonにログインした際に使用したアカウント(IAMではルートIDと呼んでいるようです)の認証情報を使用する必要があります。なので、先にContinue to Security Credentialsの方をクリックして下さい。

 下図のようなYour Security Credentialsの画面が表示されました。右側の + Access Keys(Access Key ID and Secret Access Key)をクリックして下さい。

 下図のようにメニューが開きました。Important Changeとメッセージがあります。これはSecret Access Keyの取扱いが変更になり、以前から持っていたAmazonアカウントに紐付いたAccess Key IDでは、再取得を行わないとSecret Access Keyが使用できないという意味です。なので、新たなAccess Key IDを作成しました。新たなAccess Key IDの作成は下図のCreate New Access Keyをクリックすると発行できます。発行が終われば、前のAccess Key IDはDeleteしてください。

新たなAccess Key IDを発行した画面が下図です。Show Access Keyをクリックすると、今回kintoneからAmazon APIへアクセスする際に必要な情報の二つ「Access Key ID」「Secret Access Key」が表示されます。是非φ(..)メモしておいてください。Download Key Fileをクリックすると、csvファイルで2つのIDが保存できます。

 これで、Access Key ID と Secret Access Key、トラッキングIDの3種類の情報が取得できました。3種類とも是非φ(..)メモしておいてください。 図が多くて見にくかったことでしょう。お疲れ様でした。