• 車種別
  • パーツ
  • 整備手帳
  • ブログ
  • みんカラ+

ヒデノリのブログ一覧

2025年09月14日 イイね!

語弊ある言い方かもしれませんがエンジニアに失敗は無い

語弊ある言い方かもしれませんがエンジニアに失敗は無いちょっと前にブログを投稿しましたが、
新プロジェクトXのH3ロケット打ち上げを見返して見て思いが高ぶってしまい連続投稿失礼しますm(_ _)m

語弊ある言い方かもしれませんがエンジニアという職業は割り切れてしまえば、
結果を見ると私からすると一番楽な職業なのかもしれません。
ただし、その途中では最も過酷な職業に思います。

我々は日本人で失敗を恐れることに対して、
何故そうなったのかは興味を持って調べていないのでわかりませんが、
何故か失敗することがネガティブな評価を自信からも周りからも行われるということを
日本人は何よりも恐れているように思います。
確かに、共通テスト(私が受けた当時はセンター試験)というマークシートを元に、
例えば東大では足切りと言われるセンター試験の結果を持って
2次試験を受ける資格が与えられないという状況がありました。

私の部下も失敗を恐れるような働き方を思っているようですが、
私の伝え方が足らないのをとても部下に対して申し訳なく思います。

エンジニアという立場の方にはプロジェクトを成功する事が望まれています。
ですが、今までの実績のあるやり方がわかっている作業であれば
そもそもプロジェクトが組まれないと思っています。

プロジェクトが組まれるというのは、
今まででは駄目とトップが思ってブレークスルーを求めるために組まれるものだと思っています。

つまり、エンジニアには会社(プロジェクト)がどうしようもない状況に陥って
それをなんとかしたいと思ってすがってきたものだと思っています。

これまでの実績が通用しない状況で、
新しいことを実現してくれるであろう期待値からエンジニアが呼ばれるのだと思います。

ここまでが導入です。
--------------------------------------------
プロジェクトに組み入れられたエンジニアはどうするのか?
今までの実績を調べるのは当然のこと、
今まで無かった技術を開発することを求められるわけです。

今まで無かった技術を開発するということなので、
前例のない事を実現することが求められるわけです。

無いものを作れ!

令和の今どきであればふさわしくない言葉かもしれませんが、
どうしようも無くなってエンジニアに求める能力というのはそういうものです。
--------------------------------------------
それに対して、いゃ俺に言っても・・・
前例のないことに対して何を求めているん?
とお思いの方が大多数だと思います。

私も50歳手前まで、ごく最近(数年前まで)そう思っていました。

ですが、大きなプロジェクトをいくつか任されて、
ある日突然振り返ったときに思い立ちました。
--------------------------------------------
そういう大きなプロジェクトって興味を持つことが重要なのでは?

プロジェクトを任されたときに最初に思うことは、
自分の手に余るか余らないか?
ということだと思います。

自分の手に余ることであれば今までの経験で出来ることですし、
自分の手に余らないことであれば・・・
言い方を変えると今の自分にできないことを求められるチャレンジングな目標ということになると思います。
--------------------------------------------
とある、ネットワークエンジニアリングの教科書で以下の様に表現されていました。
エンジニアとして一番成長していると実感できるのは顧客先で四苦八苦してるときだと・・・

つまり、顧客先で膨大な失敗を繰り返しながら
成功に導こうと延々と繰り返している時がエンジニアとして成長できる機会であると言っていました。
--------------------------------------------
また、バックプロパゲーション(誤差逆伝播法)とは、
ニューラルネットワークの学習において、ネットワークの出力と正解データとの差(誤差)を、出力層から入力層に向かって逆方向に伝播させ、各層の重みとバイアスを調整することで、ネットワークの性能を向上させるアルゴリズムです。
この手法は、ディープラーニングの基盤技術として広く普及しています。
--------------------------------------------
つまり、失敗経験を上手くいく方法を教えることなくスパルタ的に教えているわけです。

AIと人間の脳に本質的な違いは無いということから、
学習の仕方に最短距離というのはなく、
結局のところバックプロパゲーション(誤差逆伝播法)を行うしか無いというわけです。
--------------------------------------------
後半をご覧になっていくと失敗は行っはいけないものなのか?
失敗することもあるのでは?
という事を思われるかもしれません。

私は自分でエンジニアとして過ごしてきて、
認知できる大きなプロジェクトとしては
受注システム(Order Manegement System=OMS)の導入です。

OMSの導入は会社としては相当大きなプロジェクトだったのですが、
最初は担当者で合ったものから最終的に責任者にまで上り詰めました。

OMS導入と言っても我が社には知見がない状態での導入になりますので、
試行錯誤を繰り返すことになります。
しかも先方に導入担当というのがいない状況です。

ならば、自分たちでどうにかするしか無いという状況になります。

プロジェクトを日々進める上で沢山アイデアが出てきます。
仕事中に集中しているので、
休日にそのアイデアが出てくることも止めることが出来ませんが、
日本の世の中では1日8時間、1週で40時間以上働くことは法律で認められていなので自重していましたが、
興味を持ったことに対してその欲求が湧き上がってくるのは止めようがありません。

なので、ふとした時にアイデアが出てくるのを無理やり押さえつけるという日々を過ごしていました。

アイデアは興味を持って取り組むことで無限に湧き上がってくるのですが、
それを見つめ直したときに有用なアイデアと言うのは
100件中1~2件あれば良い方
という状況でした。
ですが、初めてのことを行うのでそれに対して悲観するものではなく、
新しいプロジェクトを行うにはそういうものだと何となく理解していました。
--------------------------------------------
思いついたアイデアの1~2%しか採用されないの?
とお思いの方もいらっしゃるかもしれませんが、
逆に言うと98~99%は失敗してもその失敗が会社に認識されないということになります。

アイデアの全てをプロジェクトに報告することは無いと思いまして、
業務時間中にそのアイデアを自身で可能な範囲で検証るというのを繰り返していくと思います。

という事は、エンジニアが行う成果に対して、
失敗は99~90%近くが失敗していたとしても認識されないということになりますし、
その失敗経験はそのエンジニアにとって貴重な資産になるというわけです。
--------------------------------------------
私の持論ですが、
----------------------------------------
あらゆる興味に対して勉強する姿勢で失敗は決して無い。
失敗はやり遂げられなかったという事実によって発生する。
----------------------------------------
間違いを恐れない。
諦めたことこそが失敗であり、
やり遂げたことこそが人生の成功体験である。
----------------------------------------
というのがあります。

何度か登場しているのでご存じの方もいらっしゃると思います。

新しいことを行うことに対してエンジニアは人間なので
必ず失敗します。
ですが、その失敗を糧に前に進むことが出来ます。
その失敗を繰り返しながら前に進んで成功まで導いた結果は、
そのエンジニアはプロジェクトを成功させたという評価になります。

このことから
----------------------------------------
あらゆる興味に対して勉強する姿勢で失敗は決して無い。
失敗はやり遂げられなかったという事実によって発生する。
----------------------------------------
というのが言えるかと思います。

興味を持ってやりたいことに対して、
業務時間外にそれを知りたい欲求を抑えることは出来ませんし、
失敗を糧に進むのを日常にすることで、
成功まで導いたという事実に対して
プロジェクトはそのエンジニアは完遂させたと評価します。

全く新しいことをすることに対して、
いわゆる教科書の内容が通じない状況でも、
新しいことを生み出すことを求められる状況に対して、
すでに述べましたが
興味を持つ
というのが一番大切だと思います。

ごく最近、意識的に行っている開発手法なのですが、
新しい開発を行う際にまず興味を持つことからはじめまして、
業務時間にそのことに対して興味を持ってやり遂げたいという気持ちを持って一生懸命考える。

業務時間中に解決できる問題ならば簡単に出来る問題だったと認識し、
業務時間で解決できない問題はチャレンジングな目標と認知する。

チャレンジングな目標は引き続き、
業務時間内で繰り返し一生懸命考える。

それでも解決できない問題はいったん据え置く。

え?時間をかけて業務時間外でも考えるんじゃないの?
とお思いの方もいらっしゃると思いますが、
考えているときの神経回路の電気の流れ方は、
行わないといけない回路周辺しか流れません。
----------------------------------------
一方、脳にはデフォルト・モード・ネットワークという状況が存在します。

何かに対して一生懸命考えている時は、
それを解決するためにそのためだけの周辺の回路が活性化している状況です。

ですが、脳の回路と言うのは想像を絶する神経回路が存在しているので、
なにかに集中している状態と言うのは回路の一部しか働いていない状況となります。

話がそれますが、
いわゆる精神疾患でうつ病と言われる方々の気持ちが落ち込む状況は、
症状を発生していないどなたにも発生する危険性をはらんでいますが、
それに対して改善する方法はマインドフルネスという方法があります。
これは、自身の息を何秒間吸って何秒間吐くというのを繰り返すというのも一つの方法とされています。
また、前に向かって進むウォーキングもいい方法とされています。
さらに、ハイキングも眼の前の坂を登ることに集中するのでいい方法とされています。

うつ病などの気分が落ち込む状態と言うのは、
なにかに集中できない状態で脳が暴走していることから発生していることが言われてます。

そのために、呼吸やウォーキング、ハイキングを日常的に行うことで脳の暴走を抑えることで脳を休めるということが言われています。

この事は、脳科学者の茂木健一郎さんが
脳を休める方法はそれまでとは全く別のことに集中する
と言っていることから伺えます。
----------------------------------------
一生懸命考えていることで、
脳の神経回路の動作する場所を意識的に狭めているのであれば、
うつ病などの神経疾患を患っている方が多く落ちっている状況のデフォルト・モード・ネットワークに意識的に投入させることで、
新しいアイデアが出てくるのではないか?
と思った次第です。

これが、私の提唱する
今の自分に解決できない問題を解決する最短方法
と思っています。

問題に対して一生懸命考える。
それで解決できる問題であれば良し。

そうでない問題であれば更に業務時間中に考える。

そうすると、業務時間外にそのもんだいについて頭に浮かぶ時が来ます。
それは知りたい欲求なので考えたくなりますが、
その欲求を意識的に抑えることをします。

時間外は趣味に使える時間だから業務の問題を知りたい欲求をあえて考えないようにします。

ですが、その欲求を抑えることは出来ず、
抑えても一定時間後に出てくるのですがそれでも抑えることをします。

これを繰り返すことで、
無意識下で問題解決を行うための回路が出来ていくのではないかと思っています。

そうする中で、業務時間中にふと時間が空いたときであったり
ハイキングを行っている中で坂の傾斜が穏やかになったときであったり、
お風呂を洗っている中で泡をシャワーで流そうとした瞬間であったり
何も考えない時間というのが日常生活で存在する中で、
脳はデフォルト・モード・ネットワークに一瞬で突入すると思っています。

デフォルト・モード・ネットワークと言うのは
脳の神経回路の暴走状態でうつ病などの神経疾患をお持ちの方にはデメリットでしか無いのですが、
今までの経験では実現不可能な目標を解決するにはデフォルト・モード・ネットワークに投入させることを何度も繰り返すことでしか出来ないのではないかともいます。

確かに、脳科学者の茂木健一郎さんを始めとした様々な功績を残した方の言葉を振り返ると、
ふとした瞬間にひらめいた
というのがあります。

という事は、一生懸命に問題解決する時間を繰り返しながら
デフォルト・モード・ネットワークに意識的に残す時間を作ることから、
今まで出来なかった問題を解決することを高速で行うのではなかろうかと思っています。
----------------------------------------
エンジニアという人種は日常のあらゆることに対して興味をいただきます。
それに対して知りたいという欲求が高まってきます。

それに対して突き詰めるのは研究者であって、
エンジニアはその道具を使って他のものと接続する方法を探すというのを
行っているのだと思います。

突き詰めるのではなく、
それに対して興味を持って真理や設計思想を知りたいと思って
それが他のものと繋がる方法を無意識下で探して、
設計者が当時では意図しない
設計を超えた方法で他のものと接続する方法を探す事を
史上の喜びのように思っています。
----------------------------------------
エンジニアに対して求められる能力・・・
今まで無かったものを開発す能力・・・

バックプロパゲーション(誤差逆伝播法)・・・

あらゆることに対して興味を持つ姿勢・・・

その興味に対して業務時間中に真剣に考える姿勢・・・

その興味に対して休日に湧き上がる興味とそれをあえて押さえつける姿勢・・・

うつ病などの神経疾患でとても悩んでいる
デフォルト・モード・ネットワークを意識的に作ることでの問題解決・・・
----------------------------------------
100個のアイデアが出る中で1~2個のアイデアが採用されるというのを日頃体感していたら、
失敗は日常というのが嫌でもわかってくると思います。

という事は、諦めずに失敗し続けるという姿勢を貫く限り
エンジニアに対して失敗は無いのだなと思いました・・・
----------------------------------------
タイトルに戻りますが、
エンジニアに失敗は無い
というのは、
エンジニアの失敗は日常であって、
その日常に対して評価できるのは本人以外にはおらず、
失敗を繰り返してその人の糧を得ることを繰り返すことで
成功まで導いたという事実だけでプロジェクトはそのエンジニアを評価するのだと思います。
----------------------------------------
エンジニアというのはエンジニア語を発するので、
一般の方にとってはエンジニア語での失敗例を理解できません。

そもそもエンジニアの失敗を失敗として認識できません。

それにあぐらを書いているエンジニアは取り残されるので置いといて、
前に進もうとしているエンジニアの方々は、
前例の無いことを行っているのだから
正解までの道しるべを示していない会社のほうが悪いと割り切って
前例のないことに対して興味を持って日常業務をこなして貰えればと思います。
----------------------------------------
エンジニアになるのは簡単です。
日常目にするものに対して興味を持つことから始めます。

それに対して他と繋がる方法を探します。

繋げようとして失敗は有ると思います。
ですが、その失敗はその貴方しか認識できないことです

貴方は貴方の興味を持って前に進むことを行うだけで、
全ての失敗は貴方が前に進む糧として蓄積されていくのです。
----------------------------------------
日常業務に対して興味を持つだけで・・・

失敗を恐れず前に進み続けるだけで・・・

いつでも貴方はエンジニアに成ることが出来ます。
----------------------------------------
年齢など関係ありません。

令和の今どきであれば貴方に合うAIに出会うことで
50歳でも60歳でもエンジニアに成ることは出来ます。
Posted at 2025/09/15 00:16:31 | コメント(0) | トラックバック(0) | AI | 日記
2025年09月14日 イイね!

デジタル一眼レフカメラの保存場所

デジタル一眼レフカメラの保存場所しょっちゅう持ち出していたら、
しょっちゅうズームを使っていたら、
カメラ内外の空気が動くのでカビが生えにくいそうです。

勿体がらずに、しょっちゅう使えってことですよね^^;

と言うものの、
梅雨の時期など雨が降っていて湿度が高い状態はカビが生えやすいという事で、
保管場所を整えることにしました。

予算は可能な限り安く^^;

なにせ、
ボディ:Canon EOS 7D
レンズ:Canon EF-S 18-55mm F3.5-5.6 IS STM
の組み合わせで2.5万円(税込み・送料込み)という状態ですので、
(ネックストラップとCFカードアダプターとSDカードで3,608円)
という低予算で揃えましたので、
ドライボックスに予算を避けることも出来ずに低予算で探さざるを得なくなりました。

良さそうの無いかな~と思って、
ニトリなら有るかな?と思っていって店内をウロウロしていたら・・・

5kgの米びつは?
持っていって車内に置いといたEOS 7Dとサイズを比較すると・・・
シンデレラフィットか!?

米びつは害虫や品質の劣化を防ぐために密封されているようです。

その足でホームセンターに行って
除湿剤にしようか?水とりぞうさんにしようか?
と悩んでいたので
Geminiに水とりぞうさんの除湿能力を聞いたところ・・・
--------------------------------
水取りぞうさんを密封容器で使用する場合、湿度は約40%まで下げられます。

仕組み
「水取りぞうさん」などの除湿剤は、主成分である塩化カルシウム(CaCl
2

)が空気中の水分を吸収することで除湿を行います。塩化カルシウムは潮解性(空気中の水分を吸収して液体になる性質)を持つため、密封された空間の水分を効率よく吸着します。

除湿の限界
理論上、塩化カルシウムは湿度が約35%になるまで水分を吸収し続けることができます。しかし、製品の使用環境や容器の気密性、空気の流れなどの要因により、実際の除湿能力はやや低下します。そのため、一般的に除湿剤が下げられる湿度の目安は40%程度とされています。

この湿度は、日本の高温多湿な気候で、カビやダニの繁殖を防ぐのに有効なレベルです。
--------------------------------
カメラやレンズの保管に最適な湿度は40%~50%前後ということですので、
密封容器ならば水とりぞうさんを組み合わせることで十分ですね。

という事で、令和の今どきで言い方に配慮した表現にすると
コスパに優れる一眼レフカメラのドライボックスは米びつ&水とりぞうさん
ということになりました^^;

令和の今どきであれば決して◯ンボーとか言ってはいけません^^;
差◯だとかいう人が何処から出てくるかわかりませんからね(-_-;)
まぁ、製造から25年も経過した車を持っていたら、
いきなり修理する場所が出てくるので◯ンボーですけどね^^;
良いんです。
チェイサーが良いんです。
ARC presents 西日本スポーツカーフェス2025で、
帰ろうとして車道に向かってゆっくりと動いていたら、
左前の方を車道に向かって走っていく若者二人がいて、
なんで走ってるん?と思ったらどうやら私のチェイサーが出庫するシーンを撮りたかったようで・・・
お前たち、そんなにチェイサーのことをかっこいいと思ってくれるんか!?
という事で、手を降ったりしてちょっとサービスしてしまいました^^;

閑話休題
--------------------------------
ついでに買ったDAISOの温湿度計では30%に張り付いていますが、
この湿度では10%程低く出るようで、
40%という湿度の下限まで下がってくれています。

これならばカビの心配なく保存してくれそうですね。

ただし、湿度の推移をちょくちょく見る必要はあります^^;
Posted at 2025/09/14 21:09:04 | コメント(1) | トラックバック(0) | 日記
2025年09月12日 イイね!

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:在庫情報取得_一括処理版編

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:在庫情報取得_一括処理版編今日はGPTとコーディングをしていました。
というブログを書いたのが2025年8月17日でしたので、
Google Apps Script(=GAS)の殆ど初心者の私が3週間ほどでここまで来たとは・・・
我ながら感慨深く思います・・・

コーディングが出来る生成AIをどうやって使ってきたのか?
コーディングを行いだしてから2週間の記憶はあやふやですが・・・
・Gemini 2.5 Pro
・GPT-5
を使っていたように思います。

ですが、無料枠では
・Gemini 2.5 Pro
の上限が早々に来てしまったために、そのコードを持って
・GPT-5
にコーディングを依頼するというのを行っていたように思います。

途中でうまく行ったように思いますが、
バージョン管理を行っていなかったために
どうにもならなくなった記憶があります。

コーディングを始めてから2週間が経過した頃に学習したGitHubのブランチと言う知識もありませんでした。

その後、
・GitHub Copilot
を使うためにVisual Studio Code(VS Code)を使いだしました。
タイトルに書かれているのが、
The open source AI code editor(オープンソースのAIと友にコード作成)
ソースは資産という考え方もありますが、
オープンソース化することで開発の速度とコストが向上するというのが有るのでしょうね。
パブリックかプライベートかとう大きな違いはありますが、
かなり有名な会社がGitHubを使用しています。
GitHubとGitHub Copilotは異なるのですが気になったらお気に入りのAIに聞いてみてください。

結構良かったです。
コードの右側にチャットを表示させて、
指示に従ってコードの書き換えを行ってくれました。

ですが、うまく実行するまでに至らず・・・
制限に接触してしまってからAIチャットが使えない・・・(-_-;)

という事で、GeminiだったかGPTだったかは覚えていませんが、
Claude
をオススメされて使いだしたら開発が一気に進みました。

これが、コーディングを始めてから2週間後の出来事です。

2025年9月1日の午前中までVS Code&GitHub Copilotでのコーディングを行っていたものの、
動作するに至らない中で、
無料制限枠に接触してコーディングができなくなったので、
午後からClaudeを使い始めると、
2週間経っても中々進まなかったコーディングが、
わずか半日で認証を終えるところまで進んだのを覚えています。
その間、私のプロンプトエンジニアリング能力も向上したのではないかと思います。
49歳の私が・・・(・・;)

それを検証を終えた状態で公開したブログが
Claudeでのコーディング:ネクストエンジンAPI認証編
となります。

ですが、Claudeも無料枠で使用していたので度々上限に接触してきました。
これが有料枠を契約する理由か(-_-;)

ウチの会社は金が無いから有料枠の契約は出来ないんだろうな^^;

制限にかかりながら進めていって在庫情報の取得が出来るようになったのが、
ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:在庫情報取得編
です。

ようやく・・・
ようやくここまで来たか・・・(-_-;)

ネクストエンジンとGoogleスプレッドシートを接続したいと思い、
Tepsというアプリを使ってみたり、
SFTPサーバーを立ち上げてCSV連携を自動で行ってみたり、
Yoomというアプリを軽く使ってみたりしましたが、
私の求めるコストと動作をしてくれなくて、
ネクストエンジンとGoogleスプレッドシートをAPI接続するために、
一からGoogle Apps Script(GAS)を生成AIと開発を行ってきて、
GASのプロジェクト内に作成した認証が出来た、
その次にプロジェクト内に新しく作成したスクリプトで200行の在庫情報の取得が出来た!

2週間半でここまで来たか・・・
とても感慨深いです。

ここで、この2週間の間で失敗した経験を元に、
バージョン管理の重要性に気がついて振り返ってみたのが
Visual Studio Code(VS Code)とGitHubの組み合わせです。
-------------------------------------------
Gitは、プログラムのソースコードなどの変更履歴を記録・管理するツールです。たとえるなら、ファイルの「タイムマシン」のようなもので、いつでも過去の状態に戻ったり、誰がいつどこを変更したかを確認したりできます。
-------------------------------------------
GitHubは、Gitの機能を使って世界中の開発者と共同作業を行うためのウェブサービスです。Gitで管理しているプロジェクトをインターネット上で公開・共有したり、他の人と一緒に開発を進めたりする「プロジェクト共有サイト」のような役割を果たします。
-------------------------------------------
Gitは個人のパソコンで動くツール、GitHubはインターネット上のサービスと覚えておくと良いでしょう。
-------------------------------------------
そのGitHubをさらにGUIで使いやすくしたのがVisual Studio Code(VS Code)というわけです。

Claudeで修正したコード全文をVisual Studio Code(VS Code)内で全て貼り付けて保存する(それに加えてコミットとプッシュ?)だけで、
その差分を表示させることも出来ます。

という事で、開発を中断してVisual Studio Code(VS Code)&GitHubの使い方を学ぶことにしました。
開発を進めたほうが前に進むかもしれませんが、
バージョン管理を行わないともとに戻ることが出来ず、
コーディングを始めてから2週間の前に進まない悪夢を再び味わうことになりますので、
開発を中断してでもバージョン管理を行おうとした次第です。
-------------------------------------------
うまく説明できるほどVisual Studio Code(VS Code)を使いこなしていないのですが、
それでもClaudeとコーディングを行っていって、
修正されたコード全文をVisual Studio Code(VS Code)に貼り付けて、
保存・コミット・プッシュすることで、
バージョン管理を行うことが出来るようになりました。

認証が出来た、
200行の在庫情報の取得ができた、
バージョン管理が行うように出来た。

プロの方からすると素人が!と思われるかもしれませんが、
一歩前に進めた瞬間のエンドルフィンが出るのを反芻するために
このコーディングをやっているのだと思います。
-------------------------------------------
認証まで出来て、テストの在庫情報取得まで出来れば、
APIのエンドポイントさえ知っていれば
APIを使って出来る範囲内でどんな情報でも行き来させることが出来るようになると思います。
-------------------------------------------
次に行ったのが1回で取得できる在庫情報の増加なのですが、
そのステップはQiitaでの公開を行うために、
端折って説明すると、
1回で200行の取得(言い換えれば200行までしか取得できない)
だったのを高速化することで1回で300行超の在庫情報が取得できて、
しかも1回の取得可能範囲を超えた在庫情報を取得したい場合、
次に取得する在庫行情報をスクリプトプロパティに残すことで、
1回目の続きから在庫情報を取得できるようになりました。
これで、1000行ほどの在庫情報であれば
1日何回か更新することが出来るようになりました。
これがClaudeで引き続き開発を行うことでわずか4時間(実働時間はもっと短い)
で実装できたことになります。
-------------------------------------------
ん~
なんかモヤモヤ・・
モヤモヤ・・・

とりあえずお昼になったからうどん食べてこよ(笑)

昼から帰ってきて運送会社からアポが合ったので対応して、
コーディングやろうかな~
-------------------------------------------
モヤモヤしてから2時間の間でぼ~っとする時間も合ったことから
脳がデフォルト・モード・ネットワークに入ったことから・・・

ネクストエンジンに対して配列のような形で情報を渡してもらうことで
数十行の在庫情報取得が
APIの1コールで出来るのでは?

と思ってClaudeに
-------------------------------------------
ネクストエンジンとGoogleスプレッドシートを接続して在庫情報を取得するGASの開発を行っていますが、取得する商品コードの数が数千件ある状態です。
今作成しているスクリプトでは1行1行取得していてGASの実行可能時間の上限にどうしても接触している状態です。
これを・・・例えば、数十の商品コードをネクストエンジンに渡して、一気に在庫情報を取得するというようなスクリプトを書くことは可能ですか?
-------------------------------------------
とプロンプトエンジニアリングを行うと
-------------------------------------------
Google Apps Scriptでバッチ処理による効率化は十分可能です。
ネクストエンジンのAPIは複数商品コードを一度に処理する機能を提供していますので、それを活用することで大幅な実行時間短縮が期待できます。
ネクストエンジンAPIには以下のような方法でバッチ処理が可能です:
商品マスタAPI(/api_v1_master_goods/search)を使った方法:
商品コードを配列で渡すことで、複数商品の情報を一度に取得できます。一回のAPIコールで最大100件程度まで処理可能です。
-------------------------------------------
出来るんか~い!
確かに、今までのプロンプトエンジニアリングでは数千行の在庫情報を取得したいとClaudeに伝えたことがないかもしれません(-_-;)

その後、制限にかかりながら出来たコードを公開します。
ハードコーディングしていますが、
テストスプレッドシートですので大丈夫です。

在庫情報取得_一括処理版.gs
-------------------------------------------
/**
* ネクストエンジン在庫情報取得スクリプト(一括処理版)修正版
*
* 【修正内容】
* - async/await構文をGoogle Apps Script対応の同期処理に変更
* - エラーハンドリングを改善
* - デバッグ出力を追加
*/

/**
* スクリプトプロパティの初期設定
* 初回実行時に使用してください
*/
function setupBatchProperties() {
const properties = PropertiesService.getScriptProperties();

// 既存の認証情報は保持して、新しい設定のみ追加
const newProperties = {
'SPREADSHEET_ID': '1noQTPM0EMlyBNDdX4JDPZcBvh-3RT1VtWzNDA85SIkM',
'SHEET_NAME': 'GAS',
'BATCH_SIZE': '100',
'API_WAIT_TIME': '500'
};

console.log('=== スクリプトプロパティ設定 ===');

for (const [key, value] of Object.entries(newProperties)) {
const currentValue = properties.getProperty(key);
if (currentValue) {
console.log(`${key}: ${currentValue} (既存値を保持)`);
} else {
properties.setProperty(key, value);
console.log(`${key}: ${value} (新規設定)`);
}
}

console.log('');
console.log('設定完了!以下の関数でテストを開始できます:');
console.log('- testBatchProcessing(10)');
console.log('- comparePerformance(20)');
console.log('- updateInventoryDataBatch()');
}

/**
* 現在のスクリプトプロパティ設定を表示
*/
function showCurrentProperties() {
const properties = PropertiesService.getScriptProperties();

console.log('=== 現在のスクリプトプロパティ設定 ===');
console.log(`SPREADSHEET_ID: ${properties.getProperty('SPREADSHEET_ID') || '未設定'}`);
console.log(`SHEET_NAME: ${properties.getProperty('SHEET_NAME') || '未設定'}`);
console.log(`BATCH_SIZE: ${properties.getProperty('BATCH_SIZE') || '未設定'}`);
console.log(`API_WAIT_TIME: ${properties.getProperty('API_WAIT_TIME') || '未設定'}`);
console.log('');
console.log('認証情報:');
console.log(`ACCESS_TOKEN: ${properties.getProperty('ACCESS_TOKEN') ? '設定済み' : '未設定'}`);
console.log(`REFRESH_TOKEN: ${properties.getProperty('REFRESH_TOKEN') ? '設定済み' : '未設定'}`);
}

/**
* メイン関数:一括処理による在庫情報更新
*/
function updateInventoryDataBatch() {
try {
console.log('=== 在庫情報一括更新開始 ===');
const startTime = new Date();

// スクリプトプロパティから設定値を取得
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');
const batchSize = parseInt(properties.getProperty('BATCH_SIZE')) || 100;
const apiWaitTime = parseInt(properties.getProperty('API_WAIT_TIME')) || 500;

if (!spreadsheetId) {
throw new Error('SPREADSHEET_IDがスクリプトプロパティに設定されていません。');
}

if (!sheetName) {
throw new Error('SHEET_NAMEがスクリプトプロパティに設定されていません。');
}

// スプレッドシートを取得
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);

if (!sheet) {
throw new Error(`シート "${sheetName}" が見つかりません`);
}

// データ範囲を取得
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
console.log('データが存在しません');
return;
}

const dataRange = sheet.getRange(2, 1, lastRow - 1, 12);
const values = dataRange.getValues();
console.log(`処理対象: ${values.length}行`);

// トークンを取得
const tokens = getStoredTokens();

// 商品コードのリストを作成(空でないもののみ)
const goodsCodeList = [];
const rowIndexMap = new Map(); // 商品コード → 行インデックスのマッピング

for (let i = 0; i < values.length; i++) {
const goodsCode = values[i][COLUMNS.GOODS_CODE];
if (goodsCode && goodsCode.toString().trim()) {
goodsCodeList.push(goodsCode.toString().trim());
rowIndexMap.set(goodsCode.toString().trim(), i + 2); // 実際の行番号(1ベース)
}
}

console.log(`有効な商品コード: ${goodsCodeList.length}件`);

if (goodsCodeList.length === 0) {
console.log('処理対象の商品コードがありません');
return;
}

// バッチ処理で在庫情報を取得・更新
let totalUpdated = 0;
let totalErrors = 0;

for (let i = 0; i < goodsCodeList.length; i += batchSize) {
const batch = goodsCodeList.slice(i, i + batchSize);
console.log(`\n--- バッチ ${Math.floor(i / batchSize) + 1}: ${batch.length}件 ---`);

try {
// バッチで在庫情報を取得
const inventoryDataMap = getBatchInventoryData(batch, tokens);

// スプレッドシートを更新
for (const goodsCode of batch) {
const inventoryData = inventoryDataMap.get(goodsCode);
const rowIndex = rowIndexMap.get(goodsCode);

if (inventoryData && rowIndex) {
try {
updateRowWithInventoryData(sheet, rowIndex, inventoryData);
totalUpdated++;
console.log(` ✓ ${goodsCode}: 更新完了`);
} catch (error) {
console.error(` ✗ ${goodsCode}: 更新エラー - ${error.message}`);
totalErrors++;
}
} else {
console.log(` - ${goodsCode}: データなし`);
}
}

// バッチ間の待機(APIレート制限対策)
if (i + batchSize < goodsCodeList.length) {
console.log(`次のバッチまで ${apiWaitTime}ms 待機...`);
Utilities.sleep(apiWaitTime);
}

} catch (error) {
console.error(`バッチ処理エラー:`, error.message);
totalErrors += batch.length;
}
}

const endTime = new Date();
const duration = (endTime - startTime) / 1000;

console.log('\n=== 一括更新完了 ===');
console.log(`処理時間: ${duration.toFixed(1)}秒`);
console.log(`更新成功: ${totalUpdated}件`);
console.log(`エラー: ${totalErrors}件`);
console.log(`処理速度: ${(goodsCodeList.length / duration).toFixed(1)}件/秒`);

// 従来版との比較情報を表示
const conventionalTime = goodsCodeList.length * 2; // 従来版の推定時間(2秒/件)
const speedImprovement = conventionalTime / duration;
console.log(`\n--- 性能改善結果 ---`);
console.log(`従来版推定時間: ${conventionalTime.toFixed(1)}秒`);
console.log(`高速化倍率: ${speedImprovement.toFixed(1)}倍`);

} catch (error) {
console.error('一括更新エラー:', error.message);
throw error;
}
}

/**
* バッチで在庫情報を取得
* @param {string[]} goodsCodeList - 商品コードの配列
* @param {Object} tokens - アクセストークンとリフレッシュトークン
* @returns {Map} 商品コード → 在庫情報のマップ
*/
function getBatchInventoryData(goodsCodeList, tokens) {
const inventoryDataMap = new Map();

try {
console.log(` 商品マスタ一括検索: ${goodsCodeList.length}件`);

// ステップ1: 商品マスタAPIで複数商品を一括検索
const goodsDataMap = getBatchGoodsData(goodsCodeList, tokens);
console.log(` 商品マスタ取得完了: ${goodsDataMap.size}件`);

if (goodsDataMap.size === 0) {
console.log(' 商品が見つかりませんでした');
return inventoryDataMap;
}

// ステップ2: 在庫マスタAPIで複数商品の在庫を一括取得
console.log(` 在庫マスタ一括検索: ${goodsDataMap.size}件`);
const stockDataMap = getBatchStockData(Array.from(goodsDataMap.keys()), tokens);
console.log(` 在庫マスタ取得完了: ${stockDataMap.size}件`);

// ステップ3: 商品情報と在庫情報を結合
for (const [goodsCode, goodsData] of goodsDataMap) {
const stockData = stockDataMap.get(goodsCode);

const completeInventoryData = {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: stockData ? parseInt(stockData.stock_quantity) || 0 : parseInt(goodsData.stock_quantity) || 0,
stock_allocated_quantity: stockData ? parseInt(stockData.stock_allocation_quantity) || 0 : 0,
stock_free_quantity: stockData ? parseInt(stockData.stock_free_quantity) || 0 : 0,
stock_defective_quantity: stockData ? parseInt(stockData.stock_defective_quantity) || 0 : 0,
stock_advance_order_quantity: stockData ? parseInt(stockData.stock_advance_order_quantity) || 0 : 0,
stock_advance_order_allocation_quantity: stockData ? parseInt(stockData.stock_advance_order_allocation_quantity) || 0 : 0,
stock_advance_order_free_quantity: stockData ? parseInt(stockData.stock_advance_order_free_quantity) || 0 : 0,
stock_remaining_order_quantity: stockData ? parseInt(stockData.stock_remaining_order_quantity) || 0 : 0,
stock_out_quantity: stockData ? parseInt(stockData.stock_out_quantity) || 0 : 0
};

inventoryDataMap.set(goodsCode, completeInventoryData);
}

console.log(` 結合完了: ${inventoryDataMap.size}件`);
return inventoryDataMap;

} catch (error) {
console.error(`バッチ在庫取得エラー:`, error.message);
return inventoryDataMap;
}
}

/**
* 複数商品の基本情報を一括取得
* @param {string[]} goodsCodeList - 商品コードの配列
* @param {Object} tokens - トークン情報
* @returns {Map} 商品コード → 商品情報のマップ
*/
function getBatchGoodsData(goodsCodeList, tokens) {
const url = `${NE_API_URL}/api_v1_master_goods/search`;

// スクリプトプロパティからバッチサイズを取得
const properties = PropertiesService.getScriptProperties();
const batchSize = parseInt(properties.getProperty('BATCH_SIZE')) || 100;

// 複数の商品IDを検索条件に設定
const goodsIdCondition = goodsCodeList.join(',');

const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'goods_id-in': goodsIdCondition, // IN条件で複数商品を一括検索
'fields': 'goods_id,goods_name,stock_quantity',
'limit': batchSize.toString() // 取得件数制限
};

const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};

const goodsDataMap = new Map();

try {
const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);

console.log(` 商品マスタAPI応答: result=${responseData.result}, count=${responseData.count || 0}`);

// トークンが更新された場合は保存
if (responseData.access_token && responseData.refresh_token) {
updateStoredTokens(responseData.access_token, responseData.refresh_token);
// トークンを更新
tokens.accessToken = responseData.access_token;
tokens.refreshToken = responseData.refresh_token;
}

if (responseData.result === 'success' && responseData.data) {
responseData.data.forEach(goodsData => {
goodsDataMap.set(goodsData.goods_id, {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: goodsData.stock_quantity
});
});

console.log(` API応答: ${responseData.data.length}件取得`);
} else {
console.error(` 商品マスタAPI エラー:`, responseData.message || 'Unknown error');
}

return goodsDataMap;

} catch (error) {
console.error(`商品マスタ一括取得エラー:`, error.message);
return goodsDataMap;
}
}

/**
* 複数商品の在庫情報を一括取得
* @param {string[]} goodsCodeList - 商品コードの配列
* @param {Object} tokens - トークン情報
* @returns {Map} 商品コード → 在庫情報のマップ
*/
function getBatchStockData(goodsCodeList, tokens) {
const url = `${NE_API_URL}/api_v1_master_stock/search`;

// スクリプトプロパティからバッチサイズを取得
const properties = PropertiesService.getScriptProperties();
const batchSize = parseInt(properties.getProperty('BATCH_SIZE')) || 100;

// 複数の商品IDを検索条件に設定
const goodsIdCondition = goodsCodeList.join(',');

const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'stock_goods_id-in': goodsIdCondition, // IN条件で複数商品の在庫を一括検索
'fields': 'stock_goods_id,stock_quantity,stock_allocation_quantity,stock_defective_quantity,stock_remaining_order_quantity,stock_out_quantity,stock_free_quantity,stock_advance_order_quantity,stock_advance_order_allocation_quantity,stock_advance_order_free_quantity',
'limit': batchSize.toString()
};

const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};

const stockDataMap = new Map();

try {
const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);

console.log(` 在庫マスタAPI応答: result=${responseData.result}, count=${responseData.count || 0}`);

// トークンが更新された場合は保存
if (responseData.access_token && responseData.refresh_token) {
updateStoredTokens(responseData.access_token, responseData.refresh_token);
// トークンを更新
tokens.accessToken = responseData.access_token;
tokens.refreshToken = responseData.refresh_token;
}

if (responseData.result === 'success' && responseData.data) {
responseData.data.forEach(stockData => {
stockDataMap.set(stockData.stock_goods_id, stockData);
});

console.log(` API応答: ${responseData.data.length}件取得`);
} else {
console.error(` 在庫マスタAPI エラー:`, responseData.message || 'Unknown error');
}

return stockDataMap;

} catch (error) {
console.error(`在庫マスタ一括取得エラー:`, error.message);
return stockDataMap;
}
}

/**
* 保存されたトークンを取得(既存関数)
*/
function getStoredTokens() {
const properties = PropertiesService.getScriptProperties();
const accessToken = properties.getProperty('ACCESS_TOKEN');
const refreshToken = properties.getProperty('REFRESH_TOKEN');

if (!accessToken || !refreshToken) {
throw new Error('アクセストークンが見つかりません。先に認証を完了してください。');
}

return {
accessToken,
refreshToken
};
}

/**
* スプレッドシートの行を在庫データで更新(既存関数)
*/
function updateRowWithInventoryData(sheet, rowIndex, inventoryData) {
const updateValues = [
inventoryData.stock_quantity || 0,
inventoryData.stock_allocated_quantity || 0,
inventoryData.stock_free_quantity || 0,
inventoryData.stock_advance_order_quantity || 0,
inventoryData.stock_advance_order_allocation_quantity || 0,
inventoryData.stock_advance_order_free_quantity || 0,
inventoryData.stock_defective_quantity || 0,
inventoryData.stock_remaining_order_quantity || 0,
inventoryData.stock_out_quantity || 0
];

const range = sheet.getRange(rowIndex, COLUMNS.STOCK_QTY + 1, 1, updateValues.length);
range.setValues([updateValues]);
}

/**
* トークンを更新保存(既存関数)
*/
function updateStoredTokens(accessToken, refreshToken) {
const properties = PropertiesService.getScriptProperties();
properties.setProperties({
'ACCESS_TOKEN': accessToken,
'REFRESH_TOKEN': refreshToken,
'TOKEN_UPDATED_AT': new Date().getTime().toString()
});
console.log(' トークンを更新しました');
}

/**
* テスト用:小規模バッチでの動作確認
* @param {number} maxItems - テスト対象の最大商品数(デフォルト: 10)
*/
function testBatchProcessing(maxItems = 10) {
try {
console.log(`=== バッチ処理テスト(最大${maxItems}件) ===`);

// スクリプトプロパティから設定値を取得
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');

if (!spreadsheetId || !sheetName) {
throw new Error('SPREADSHEET_IDまたはSHEET_NAMEがスクリプトプロパティに設定されていません。');
}

// スプレッドシートから商品コードを取得
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
const lastRow = sheet.getLastRow();

if (lastRow <= 1) {
console.log('テスト用データが存在しません');
return;
}

const dataRange = sheet.getRange(2, 1, Math.min(maxItems, lastRow - 1), 1);
const values = dataRange.getValues();
const goodsCodeList = values
.map(row => row[0])
.filter(code => code && code.toString().trim())
.slice(0, maxItems);

console.log(`テスト対象商品コード: ${goodsCodeList.join(', ')}`);

const tokens = getStoredTokens();

// バッチで在庫情報を取得
const startTime = new Date();
const inventoryDataMap = getBatchInventoryData(goodsCodeList, tokens);
const endTime = new Date();
const duration = (endTime - startTime) / 1000;

console.log(`\n=== テスト結果 ===`);
console.log(`処理時間: ${duration.toFixed(1)}秒`);
console.log(`取得件数: ${inventoryDataMap.size}件`);
console.log(`処理速度: ${(goodsCodeList.length / duration).toFixed(1)}件/秒`);

// 取得したデータの内容を表示
for (const [goodsCode, data] of inventoryDataMap) {
console.log(`${goodsCode}: 在庫${data.stock_quantity} 引当${data.stock_allocated_quantity} フリー${data.stock_free_quantity}`);
}

} catch (error) {
console.error('バッチテストエラー:', error.message);
throw error;
}
}

/**
* パフォーマンス比較用:従来版と一括版の処理時間を比較
* @param {number} sampleSize - 比較対象のサンプル数(デフォルト: 10)
*/
function comparePerformance(sampleSize = 10) {
console.log(`=== パフォーマンス比較テスト(${sampleSize}件) ===`);

// スクリプトプロパティから設定値を取得
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');

if (!spreadsheetId || !sheetName) {
throw new Error('SPREADSHEET_IDまたはSHEET_NAMEがスクリプトプロパティに設定されていません。');
}

// スプレッドシートから商品コードを取得
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(sheetName);
const lastRow = sheet.getLastRow();

if (lastRow <= 1) {
console.log('テスト用データが存在しません');
return;
}

const dataRange = sheet.getRange(2, 1, Math.min(sampleSize, lastRow - 1), 1);
const values = dataRange.getValues();
const goodsCodeList = values
.map(row => row[0])
.filter(code => code && code.toString().trim())
.slice(0, sampleSize);

console.log(`比較対象商品コード: ${goodsCodeList.join(', ')}`);

const tokens = getStoredTokens();

// 従来版の推定時間(実際には実行しない)
const conventionalEstimatedTime = goodsCodeList.length * 2; // 2秒/件

// 一括版の実際の処理時間
console.log('\n一括版実行中...');
const startTime = new Date();
const inventoryDataMap = getBatchInventoryData(goodsCodeList, tokens);
const endTime = new Date();
const batchTime = (endTime - startTime) / 1000;

// 結果比較
const speedImprovement = conventionalEstimatedTime / batchTime;

console.log('\n=== 性能比較結果 ===');
console.log(`従来版推定時間: ${conventionalEstimatedTime.toFixed(1)}秒(${sampleSize} × 2秒/件)`);
console.log(`一括版実際時間: ${batchTime.toFixed(1)}秒`);
console.log(`高速化倍率: ${speedImprovement.toFixed(1)}倍`);
console.log(`取得成功率: ${(inventoryDataMap.size / goodsCodeList.length * 100).toFixed(1)}%`);

// 数千件での推定効果
const estimatedFor1000 = {
conventional: 1000 * 2 / 60, // 分
batch: 1000 / goodsCodeList.length * batchTime / 60 // 分
};

console.log('\n=== 1000件処理時の推定時間 ===');
console.log(`従来版: ${estimatedFor1000.conventional.toFixed(1)}分`);
console.log(`一括版: ${estimatedFor1000.batch.toFixed(1)}分`);
console.log(`時間短縮: ${(estimatedFor1000.conventional - estimatedFor1000.batch).toFixed(1)}分`);
}

/**
* 使用方法ガイド
*/
function showBatchUsageGuide() {
console.log('=== 一括処理版 使用方法ガイド ===');
console.log('');
console.log('【主要関数】');
console.log('1. updateInventoryDataBatch()');
console.log(' - 全商品の在庫情報を一括処理で更新');
console.log(' - 100件ずつのバッチで自動分割処理');
console.log(' - 従来版より大幅に高速化');
console.log('');
console.log('2. testBatchProcessing(件数)');
console.log(' - 小規模テスト用(デフォルト10件)');
console.log(' - 例: testBatchProcessing(5)');
console.log('');
console.log('3. comparePerformance(件数)');
console.log(' - 従来版との性能比較テスト');
console.log(' - 例: comparePerformance(20)');
console.log('');
console.log('【期待される改善効果】');
console.log('- APIコール数: 1/50~1/100に削減');
console.log('- 処理速度: 10~50倍の高速化');
console.log('- 実行時間制限: 数千件でも制限内で完了');
console.log('- API制限: レート制限に引っかかりにくい');
console.log('');
console.log('【推奨実行手順】');
console.log('1. testBatchProcessing(10) で動作確認');
console.log('2. comparePerformance(20) で性能確認');
console.log('3. updateInventoryDataBatch() で全件更新');
console.log('');
console.log('【設定変更可能項目】');
console.log('- BATCH_SIZE: バッチサイズ(現在100件)');
console.log('- API_WAIT_TIME: API間隔(現在500ms)');
}
-------------------------------------------
updateInventoryDataBatchを実行した結果のログ

16:47:53 お知らせ 実行開始
16:47:43 情報 === 在庫情報一括更新開始 ===
16:47:44 情報 処理対象: 3106行
16:47:44 情報 有効な商品コード: 3106件
16:47:44 情報
--- バッチ 1: 100件 ---
16:47:44 情報 商品マスタ一括検索: 100件
16:47:45 情報 商品マスタAPI応答: result=success, count=100
16:47:45 情報 トークンを更新しました
16:47:45 情報 API応答: 100件取得
16:47:45 情報 商品マスタ取得完了: 100件
16:47:45 情報 在庫マスタ一括検索: 100件
16:47:46 情報 在庫マスタAPI応答: result=success, count=100
16:47:46 情報 トークンを更新しました
16:47:46 情報 API応答: 100件取得
16:47:46 情報 在庫マスタ取得完了: 100件
16:47:46 情報 結合完了: 100件
中略
16:48:47 情報
=== 一括更新完了 ===
16:48:47 情報 処理時間: 64.5秒
16:48:47 情報 更新成功: 3106件
16:48:47 情報 エラー: 0件
16:48:47 情報 処理速度: 48.1件/秒
16:48:47 情報
--- 性能改善結果 ---
16:48:47 情報 従来版推定時間: 6212.0秒
16:48:47 情報 高速化倍率: 96.3倍
16:48:59 お知らせ 実行完了

これまでと比べて大幅に短縮しました。
ありがとうございます。

をClaudeに伝えると・・・
-------------------------------------------
素晴らしい結果ですね!一括処理版が期待通りの性能を発揮していることが確認できました。

**改善結果の詳細:**

| 項目 | 従来版(推定) | 一括処理版(実測) | 改善効果 |
|------|---------------|------------------|----------|
| **処理時間** | 6,212秒(約1時間43分) | 64.5秒(約1分) | **96.3倍高速化** |
| **処理速度** | 0.5件/秒 | 48.1件/秒 | **96倍向上** |
| **成功率** | - | 100%(3,106件全て成功) | **完璧** |
| **エラー数** | - | 0件 | **安定動作** |

**技術的な改善点が効果を発揮:**

1. **APIコール数の削減**:
- 従来版:3,106 × 2 = 6,212回のAPIコール
- 一括版:約62回のAPIコール(100件ずつ31バッチ × 2API)
- **99%削減**

2. **バッチ処理の効率性**:
- ネットワーク遅延の影響を最小化
- APIレート制限を回避
- メモリ効率の良いデータ処理

3. **Google Apps Scriptの実行時間制限対策**:
- 従来版では制限(6分)を大幅に超過
- 一括版では制限内(64.5秒)で完了

**今後の運用について:**

この一括処理版により、日常的な在庫情報更新が非常に効率的になりました。3,000件超のデータでも1分程度で完了するため、定期実行やリアルタイム更新にも適用できます。

N88-BASICから始まられたプログラミングの基礎が、このような現代的な最適化にも活かされていますね。バッチ処理の概念は、昔からプログラミングの重要な技術の一つです。

何かご質問や、さらなる改善のご要望があれば、お気軽にお声がけください!
-------------------------------------------
途中GPTでショートカットした時間で言えば、
午後の1時間でここまで来ました。
因みに上のスクリプトは時間をかけてClaudeでコーディングしてくれた内容です。

ログの見積もりと異なりますが、
午前中までは3000行の取得が3600秒(60分=1時間)程必要だったのに対して、
午後の1時間だけで3000行の取得がわずか64.5秒で完了しました(・・;)

これは、TepsやYoomのログを見てきた私からすると信じられない速度です。

予想の斜め上を行く結果となりました(・・;)

在庫情報取得が48.1件/秒とは・・・

1回のAPIコールで100件の在庫情報が取得できるとは・・・

3000行の取得が60秒ほどで実行可能になるということは
更新頻度をもっと高めることが出来るということになります。
-------------------------------------------
ここまで出来たのは先週の木曜日の話です。
コーディングは複数のAIとGitHubのブランチを利用することで、
APIコール数を半分にすることまで出来るようになりました。

また、1回のAPIコールで100件の在庫情報取得というのに疑問を持って・・・

今日はこのくらいにしておきましょうかね^^;
Posted at 2025/09/12 23:00:57 | コメント(0) | トラックバック(0) | AI | 日記
2025年09月11日 イイね!

初めて購入したコンパクトデジタルカメラはCanon IXY Digital 300

初めて購入したコンパクトデジタルカメラはCanon IXY Digital 300チェイサーを購入して、
Tourer Freaksに加入して、
カービレッジに登録したらデジカメで写真を取りたくなって購入したのが、
Canon IXY Digital 300
でした。

それから25年が経過して購入したデジタル一眼レフカメラが
Canon EOS 7D
ってまたCanonか~いヾ(ーー )ォィ

ってまだCanon IXY Digital 300持っとったんか~いヾ(ーー )ォィ

気になったのでスペックを比較してみました。

Canon IXY Digital 300
発表日:2001/03/15
発売日:2001/04/07
価格:\93,500(税込)そんなに高かったか?(-_-;)
撮像素子(有効画素数) 約211万画素 1/2.7型CCD(約202万画素)
感度 ISO 100相当(低輝度時は自動的にISO150相当まで感度アップ)
記録媒体 コンパクトフラッシュカード(Type I)
静止画 JPEG
動画 AVI(画像:MotionJPEG、音声:WAVE(モノラル))

そうか~
当時のコンデジは約211万画素だったんですね。

因みに入っていたCFカードは64MBでした。

Canon EOS 7D
発表日 2009年9月1日
発売日 2009年10月2日
希望小売価格 オープン価格
カメラ部有効画素 約1800万画素
ISO感度(推奨露光指数) 全自動、クリエイティブ全自動:ISO100~3200自動設定
P, Tv, Av, M, B:ISO100~6400任意設定(1/3段ステップ)、自動設定、
およびISO12800の感度拡張が可能
記録媒体 CFカード(タイプI、II準拠、UDMA対応)
印刷対応画像 JPEG画像、RAW 画像
映像圧縮方式 MPEG-4 AVC
可変(平均)ビットレート方式

こちらに入れたCFカード・・・は高かったのでアダプターを介しまして、
Canon EOS 7D CFカードアダプターを使ったSDカードのフォーマット
のように入れたSDカードは64GBです。

Canon IXY Digital 300の発売から8年後のCanon EOS 7Dのスペックの違い(-_-;)
全くの別物ですね^^;

有効画素数で単純比較すると9倍・・・
考えてみるとCanon IXY Digital 300の発売から
25年が経過した私のスマホの有効画素数が約2000万画素というのもどうなん?^^;

Canon EOS 7Dは一眼レフカメラの完成された形と詳しい方からお伺いしまして、
少し触っただけですが、
確かに一級品の能力を持っていることを肌で感じざるを得ません。

CFカード容量の推移を見てみましたが、
2000年頃は32、64、128、256、512MB
だったのに
2009年頃には64GBまでUpしていました。

すげ~な(・・;)

という事で、カメラに差している記録媒体の容量ですが
Canon IXY Digital 300:64MB
Canon EOS 7D:64GB
と単位が変わっていました(・・;)
なんちゅう~偶然^^;

カメラは沼なのでこちらの記事に思うところが有る方が沢山いそうですね(^^)
お気兼ねなくコメントを頂ければと思いますm(_ _)m
Posted at 2025/09/11 21:27:28 | コメント(3) | トラックバック(0) | 日記
2025年09月10日 イイね!

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:在庫情報取得編

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:在庫情報取得編みんカラブログはなんでも好きなことを書いて良いと認識しているので好きなことを書きます。
備忘録のようなものです。
タイトルからして、見返してみたらエンジニア語しか書いていませんね^^;

・ネクストエンジン
ネクストエンジンと言うのは複数モールに展開している受注情報を一元化できるOrder Manegement System(OMS)のことで・・・

・Googleスプレッドシート
Googleスプレッドシートと言うのはMicrosoft社のExcelが有るものの、Googleが提唱する超並列処理を実現するための・・・

・API接続
API接続と言うのはApplication Programming Interfaceの略で、アプリケーション同士を接続するための国際規格に則った接続方式で・・・

・GASの開発
Google Apps Scriptの略でJavaScriptを元にしたGoogle独自のスクリプト言語で・・・

そもそもネクストエンジンとGoogleスプレッドシートを繋げたいん?
我が社は情報を以前はExcelで店長がUSBメモリーで各店舗に回ってデータを抜いていたのが20世紀でない状況を見て、クラウドにしようと思い・・・

と説明しだすとそれぞれで1つ以上の記事が書ける内容です^^;

なので、みん友の方に最後まで見ていただけると思っていません。
ブログなので好き勝手書いて良いですからね(笑)

この3週間の間、狂気と思われる時間を過ごしてきたエンジニアのブログなので、エンジニア語満載です。

最後まで読み進められた貴方はエンジニアですよ。

プロのエンジニアの御方!
是非、無知の私にコメントなりメッセージで優しいアドバイスをお願いいたしますm(_ _)m
私は私自身の社会的な立ち位置がわかっておりません^^;

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:認証編
というブログで愚痴のような事を書いてしまいました^^;
なので、お時間の有る方は御覧くださいm(_ _)m

と言うものの、ネクストエンジンとGoogleスプレッドシートをAPI接続するGAS:在庫情報取得編のスクリプトを全文でご覧いただけます。
また、Claudeを使い、ネクストエンジンとGoogleスプレッドシートをAPI接続する:認証編でも、認証編コードの全文とステップごとの導入手順を紹介しています。

認証ができれば、次は在庫情報の取得を行ってまいります。
何度かClaudeとやり取りをして成功したと思われるスクリプト全文です。

やり取りしたプロンプトエンジニアリングは・・・長すぎるので割愛(笑)
若し、私が今回行ったプロンプトエンジニアリングの全文を知りたいという要望がありましたら、是非コメントくださいm(_ _)m
みんカラではマニアックすぎるのでQiitaで公開しようと思います。
是非コメントをお願いしますm(_ _)m
大事なことなので2度言いました(笑)

在庫情報取得.gs
--------------------------------------------------
/**
* ネクストエンジン在庫情報取得スクリプト(完全版)
*
* 【目的】
* スプレッドシートの商品コードに対応する詳細在庫情報をネクストエンジンAPIから取得し更新
*
* 【機能】
* 1. スプレッドシートから商品コードを読み取り
* 2. ネクストエンジン商品マスタAPIで基本情報を取得
* 3. ネクストエンジン在庫マスタAPIで詳細在庫情報を取得
* 4. スプレッドシートの在庫情報を詳細データで更新
*
* 【事前設定】
* スクリプトプロパティに以下の値を設定してください:
* - SPREADSHEET_ID: 対象スプレッドシートのID
* - SHEET_NAME: 対象シート名
*
* 【注意事項】
* - 認証スクリプトで事前にトークンを取得済みである必要があります
* - API制限を考慮して適切な間隔でリクエストを送信します
* - 大量データの場合は時間がかかる可能性があります
*/

// ネクストエンジンAPIのエンドポイントは認証.gsで定義済み

// 列のマッピング(0ベース)
const COLUMNS = {
GOODS_CODE: 0, // A列: 商品コード
GOODS_NAME: 1, // B列: 商品名
STOCK_QTY: 2, // C列: 在庫数
ALLOCATED_QTY: 3, // D列: 引当数
FREE_QTY: 4, // E列: フリー在庫数
RESERVE_QTY: 5, // F列: 予約在庫数
RESERVE_ALLOCATED_QTY: 6, // G列: 予約引当数
RESERVE_FREE_QTY: 7, // H列: 予約フリー在庫数
DEFECTIVE_QTY: 8, // I列: 不良在庫数
ORDER_REMAINING_QTY: 9, // J列: 発注残数
SHORTAGE_QTY: 10, // K列: 欠品数
JAN_CODE: 11 // L列: JANコード
};

/**
* スプレッドシート設定を取得
*/
function getSpreadsheetConfig() {
const properties = PropertiesService.getScriptProperties();
const spreadsheetId = properties.getProperty('SPREADSHEET_ID');
const sheetName = properties.getProperty('SHEET_NAME');

if (!spreadsheetId || !sheetName) {
throw new Error('スプレッドシート設定が不完全です。スクリプトプロパティにSPREADSHEET_IDとSHEET_NAMEを設定してください。');
}

return {
spreadsheetId,
sheetName
};
}

/**
* メイン関数:在庫情報を更新
*/
function updateInventoryData() {
try {
console.log('=== 在庫情報更新開始 ===');

// スプレッドシート設定を取得
const config = getSpreadsheetConfig();
console.log(`対象スプレッドシート: ${config.spreadsheetId}`);
console.log(`対象シート: ${config.sheetName}`);

// スプレッドシートを取得
const spreadsheet = SpreadsheetApp.openById(config.spreadsheetId);
const sheet = spreadsheet.getSheetByName(config.sheetName);

if (!sheet) {
throw new Error(`シート "${config.sheetName}" が見つかりません`);
}

// データ範囲を取得(ヘッダー行を除く)
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
console.log('データが存在しません');
return;
}

const dataRange = sheet.getRange(2, 1, lastRow - 1, 12); // 2行目から最終行まで、A列からL列まで
const values = dataRange.getValues();

console.log(`処理対象: ${values.length}行`);

// トークンを取得
const tokens = getStoredTokens();

// 各行の在庫情報を更新
let updateCount = 0;
let errorCount = 0;

for (let i = 0; i < values.length; i++) {
const row = values[i];
const goodsCode = row[COLUMNS.GOODS_CODE];

if (!goodsCode) {
console.log(`${i + 2}行目: 商品コードが空のためスキップ`);
continue;
}

try {
console.log(`${i + 2}行目: ${goodsCode} の在庫情報を取得中...`);

// 在庫情報を取得
const inventoryData = getInventoryByGoodsCode(goodsCode, tokens);

if (inventoryData) {
// スプレッドシートの行を更新
updateRowWithInventoryData(sheet, i + 2, inventoryData);
updateCount++;
console.log(`${i + 2}行目: ${goodsCode} 更新完了`);
} else {
console.log(`${i + 2}行目: ${goodsCode} の在庫情報が見つかりません`);
}

// API制限を考慮して少し待機(1秒)- 2つのAPIを呼び出すため少し長めに設定
Utilities.sleep(1000);

} catch (error) {
console.error(`${i + 2}行目: ${goodsCode} のエラー:`, error.message);
errorCount++;
}
}

console.log('=== 在庫情報更新完了 ===');
console.log(`更新成功: ${updateCount}件`);
console.log(`エラー: ${errorCount}件`);

} catch (error) {
console.error('在庫情報更新エラー:', error.message);
throw error;
}
}

/**
* 保存されたトークンを取得
*/
function getStoredTokens() {
const properties = PropertiesService.getScriptProperties();
const accessToken = properties.getProperty('ACCESS_TOKEN');
const refreshToken = properties.getProperty('REFRESH_TOKEN');

if (!accessToken || !refreshToken) {
throw new Error('アクセストークンが見つかりません。先に認証を完了してください。');
}

return {
accessToken,
refreshToken
};
}

/**
* 商品コードから完全な在庫情報を取得(完全版)
* @param {string} goodsCode - 商品コード
* @param {Object} tokens - アクセストークンとリフレッシュトークン
* @returns {Object|null} 完全な在庫情報
*/
function getInventoryByGoodsCode(goodsCode, tokens) {
try {
console.log('在庫情報取得開始...');
console.log(`対象商品コード: ${goodsCode}`);

// ステップ1: 商品マスタAPIで基本情報を取得
console.log('商品マスタAPI呼び出し中...');
const goodsData = searchGoodsWithStock(goodsCode, tokens);

if (!goodsData) {
console.log('商品が見つかりませんでした');
return null;
}

console.log('商品基本情報取得完了');
console.log(`商品名: ${goodsData.goods_name}`);
console.log(`基本在庫数: ${goodsData.stock_quantity}`);

// ステップ2: 在庫マスタAPIで詳細在庫情報を取得
console.log('在庫マスタAPI呼び出し中...');
const stockDetails = getStockByGoodsId(goodsCode, tokens);

let completeInventoryData;

if (stockDetails) {
console.log('詳細在庫情報取得完了');

// 商品情報と詳細在庫情報を結合
completeInventoryData = {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: parseInt(stockDetails.stock_quantity) || parseInt(goodsData.stock_quantity) || 0,
stock_allocated_quantity: parseInt(stockDetails.stock_allocation_quantity) || 0,
stock_free_quantity: parseInt(stockDetails.stock_free_quantity) || 0,
stock_defective_quantity: parseInt(stockDetails.stock_defective_quantity) || 0,
stock_advance_order_quantity: parseInt(stockDetails.stock_advance_order_quantity) || 0,
stock_advance_order_allocation_quantity: parseInt(stockDetails.stock_advance_order_allocation_quantity) || 0,
stock_advance_order_free_quantity: parseInt(stockDetails.stock_advance_order_free_quantity) || 0,
stock_remaining_order_quantity: parseInt(stockDetails.stock_remaining_order_quantity) || 0,
stock_out_quantity: parseInt(stockDetails.stock_out_quantity) || 0
};

console.log('完全な在庫情報を構築しました:');
console.log(`- 在庫数: ${completeInventoryData.stock_quantity}`);
console.log(`- 引当数: ${completeInventoryData.stock_allocated_quantity}`);
console.log(`- フリー在庫数: ${completeInventoryData.stock_free_quantity}`);
console.log(`- 不良在庫数: ${completeInventoryData.stock_defective_quantity}`);
console.log(`- 予約在庫数: ${completeInventoryData.stock_advance_order_quantity}`);
console.log(`- 予約引当数: ${completeInventoryData.stock_advance_order_allocation_quantity}`);
console.log(`- 予約フリー在庫数: ${completeInventoryData.stock_advance_order_free_quantity}`);
console.log(`- 発注残数: ${completeInventoryData.stock_remaining_order_quantity}`);
console.log(`- 欠品数: ${completeInventoryData.stock_out_quantity}`);

} else {
console.log('詳細在庫情報が取得できませんでした。基本情報のみで構築します。');

// 詳細情報が取得できない場合は基本情報のみ使用
completeInventoryData = {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: parseInt(goodsData.stock_quantity) || 0,
stock_allocated_quantity: 0,
stock_free_quantity: 0,
stock_defective_quantity: 0,
stock_advance_order_quantity: 0,
stock_advance_order_allocation_quantity: 0,
stock_advance_order_free_quantity: 0,
stock_remaining_order_quantity: 0,
stock_out_quantity: 0
};
}

console.log('在庫情報取得完了');
return completeInventoryData;

} catch (error) {
console.error(`商品コード ${goodsCode} の在庫取得エラー:`, error.message);
console.error('エラー詳細:', error.stack);
return null;
}
}

/**
* 商品コードで商品マスタを検索し在庫情報も取得(修正版)
* @param {string} goodsCode - 商品コード
* @param {Object} tokens - トークン情報
* @returns {Object|null} 商品情報と在庫情報
*/
function searchGoodsWithStock(goodsCode, tokens) {
const url = `${NE_API_URL}/api_v1_master_goods/search`;
const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'goods_id-eq': goodsCode, // goods_idで検索
'fields': 'goods_id,goods_name,stock_quantity'
};

const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};

try {
const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);

console.log('商品マスタAPI応答:', responseData.result);

// トークンが更新された場合は保存
if (responseData.access_token && responseData.refresh_token) {
updateStoredTokens(responseData.access_token, responseData.refresh_token);
}

if (responseData.result === 'success') {
if (responseData.data && responseData.data.length > 0) {
const goodsData = responseData.data[0];
console.log('取得した商品データ:', goodsData);
return {
goods_id: goodsData.goods_id,
goods_name: goodsData.goods_name,
stock_quantity: goodsData.stock_quantity
};
} else {
console.log(`商品コード ${goodsCode} が見つかりません`);
return null;
}
} else {
console.error(`商品検索エラー:`, JSON.stringify(responseData));
if (responseData.message) {
console.error('エラーメッセージ:', responseData.message);
}
return null;
}
} catch (error) {
console.error('商品マスタAPI呼び出しエラー:', error.toString());
return null;
}
}

/**
* 商品IDから詳細在庫情報を取得(完全版)
* @param {string} goodsId - 商品ID
* @param {Object} tokens - トークン情報
* @returns {Object|null} 詳細在庫情報
*/
function getStockByGoodsId(goodsId, tokens) {
const url = `${NE_API_URL}/api_v1_master_stock/search`;
const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'stock_goods_id-eq': goodsId, // 正しいフィールド名
'fields': 'stock_goods_id,stock_quantity,stock_allocation_quantity,stock_defective_quantity,stock_remaining_order_quantity,stock_out_quantity,stock_free_quantity,stock_advance_order_quantity,stock_advance_order_allocation_quantity,stock_advance_order_free_quantity'
};

const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};

try {
const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);

console.log('在庫マスタAPI応答:', responseData.result);

// トークンが更新された場合は保存
if (responseData.access_token && responseData.refresh_token) {
updateStoredTokens(responseData.access_token, responseData.refresh_token);
}

if (responseData.result === 'success' && responseData.data && responseData.data.length > 0) {
const stockData = responseData.data[0];
console.log('取得した詳細在庫データ:', stockData);

// 在庫マスタAPIから取得したデータをそのまま返す
return stockData;
} else {
console.log(`商品ID ${goodsId} の在庫情報が見つかりません`);
console.log('API応答詳細:', JSON.stringify(responseData, null, 2));
return null;
}
} catch (error) {
console.error('在庫マスタAPI呼び出しエラー:', error.toString());
return null;
}
}

/**
* スプレッドシートの行を在庫データで更新(完全版)
* @param {Sheet} sheet - シートオブジェクト
* @param {number} rowIndex - 更新する行番号(1ベース)
* @param {Object} inventoryData - 在庫データ
*/
function updateRowWithInventoryData(sheet, rowIndex, inventoryData) {
// 在庫情報の列を更新(C列からK列まで)
const updateValues = [
inventoryData.stock_quantity || 0, // C列: 在庫数
inventoryData.stock_allocated_quantity || 0, // D列: 引当数
inventoryData.stock_free_quantity || 0, // E列: フリー在庫数
inventoryData.stock_advance_order_quantity || 0, // F列: 予約在庫数
inventoryData.stock_advance_order_allocation_quantity || 0, // G列: 予約引当数
inventoryData.stock_advance_order_free_quantity || 0, // H列: 予約フリー在庫数
inventoryData.stock_defective_quantity || 0, // I列: 不良在庫数
inventoryData.stock_remaining_order_quantity || 0, // J列: 発注残数
inventoryData.stock_out_quantity || 0 // K列: 欠品数
];

// C列からK列まで更新
const range = sheet.getRange(rowIndex, COLUMNS.STOCK_QTY + 1, 1, updateValues.length);
range.setValues([updateValues]);

console.log('更新データ:', updateValues);
console.log('在庫管理スクリプト: データ取得の不具合で最新版を実行したログとなります。');
console.log('');
console.log(`取得した在庫データ: { goods_id: '${inventoryData.goods_id}', goods_name: '${inventoryData.goods_name}', stock_quantity: ${inventoryData.stock_quantity}, stock_allocated_quantity: ${inventoryData.stock_allocated_quantity}, stock_free_quantity: ${inventoryData.stock_free_quantity}, stock_defective_quantity: ${inventoryData.stock_defective_quantity}, stock_advance_order_quantity: ${inventoryData.stock_advance_order_quantity}, stock_advance_order_allocation_quantity: ${inventoryData.stock_advance_order_allocation_quantity}, stock_advance_order_free_quantity: ${inventoryData.stock_advance_order_free_quantity}, stock_remaining_order_quantity: ${inventoryData.stock_remaining_order_quantity}, stock_out_quantity: ${inventoryData.stock_out_quantity} }`);
}

/**
* トークンを更新保存
* @param {string} accessToken - 新しいアクセストークン
* @param {string} refreshToken - 新しいリフレッシュトークン
*/
function updateStoredTokens(accessToken, refreshToken) {
const properties = PropertiesService.getScriptProperties();
properties.setProperties({
'ACCESS_TOKEN': accessToken,
'REFRESH_TOKEN': refreshToken,
'TOKEN_UPDATED_AT': new Date().getTime().toString()
});
console.log('トークンを更新しました');
}

/**
* 特定の商品コードのみ更新(テスト用・完全版)
* @param {string} goodsCode - 更新したい商品コード
*/
function updateSingleProduct(goodsCode) {
try {
console.log('=== バージョン確認: 完全版が実行されています ===');
console.log(`=== 単品更新開始: ${goodsCode} ===`);

// スプレッドシート設定を取得
const config = getSpreadsheetConfig();

const spreadsheet = SpreadsheetApp.openById(config.spreadsheetId);
console.log('スプレッドシート取得成功');

const sheet = spreadsheet.getSheetByName(config.sheetName);
console.log('シート取得結果:', sheet ? 'success' : 'null');

if (!sheet) {
throw new Error(`シート "${config.sheetName}" が見つかりません`);
}

// 商品コードを検索
const dataRange = sheet.getRange(2, 1, sheet.getLastRow() - 1, 12);
console.log('データ範囲取得成功');

const values = dataRange.getValues();
console.log('データ取得成功、行数:', values.length);

let targetRowIndex = -1;
for (let i = 0; i < values.length; i++) {
if (values[i][COLUMNS.GOODS_CODE] === goodsCode) {
targetRowIndex = i + 2; // 実際の行番号(1ベース)
break;
}
}

if (targetRowIndex === -1) {
throw new Error(`商品コード ${goodsCode} がスプレッドシートに見つかりません`);
}

console.log('商品コード発見、行番号:', targetRowIndex);

// トークンを取得
const tokens = getStoredTokens();
console.log('トークン取得成功');

// 在庫情報を取得
console.log('getInventoryByGoodsCode 関数を呼び出します');
const inventoryData = getInventoryByGoodsCode(goodsCode, tokens);
console.log('getInventoryByGoodsCode 完了、結果:', inventoryData ? 'データあり' : 'データなし');
console.log('在庫情報取得結果:', inventoryData ? 'success' : 'null');

if (inventoryData) {
console.log('在庫データ更新開始...');
updateRowWithInventoryData(sheet, targetRowIndex, inventoryData);
console.log(`商品コード ${goodsCode} の更新が完了しました`);
} else {
console.log(`商品コード ${goodsCode} の在庫情報が見つかりませんでした`);
}

} catch (error) {
console.error('単品更新エラー:', error.message);
console.error('エラー発生箇所の詳細:', error.stack);
throw error;
}
}

/**
* 在庫マスタAPIのフィールド確認用テスト関数
*/
function testStockMasterFields(goodsCode = "dcmcoverg-s-S") {
try {
console.log('=== 在庫マスタAPI フィールド確認テスト ===');
console.log(`対象商品コード: ${goodsCode}`);

const tokens = getStoredTokens();
const url = `${NE_API_URL}/api_v1_master_stock/search`;
const payload = {
'access_token': tokens.accessToken,
'refresh_token': tokens.refreshToken,
'stock_goods_id-eq': goodsCode,
'fields': 'stock_goods_id,stock_quantity,stock_allocation_quantity,stock_defective_quantity,stock_remaining_order_quantity,stock_out_quantity,stock_free_quantity,stock_advance_order_quantity,stock_advance_order_allocation_quantity,stock_advance_order_free_quantity'
};

const options = {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-www-form-urlencoded'
},
'payload': Object.keys(payload).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])
).join('&')
};

const response = UrlFetchApp.fetch(url, options);
const responseText = response.getContentText();
const responseData = JSON.parse(responseText);

console.log('API応答結果:', responseData.result);

if (responseData.result === 'success' && responseData.data && responseData.data.length > 0) {
console.log('取得できたフィールド一覧:');
const stockData = responseData.data[0];
Object.keys(stockData).forEach(key => {
console.log(`- ${key}: ${stockData[key]}`);
});
} else {
console.log('データが取得できませんでした');
console.log('応答詳細:', JSON.stringify(responseData, null, 2));
}
} catch (error) {
console.error('テストエラー:', error.message);
}
}

/**
* 在庫情報の手動リセット(テスト用)
* すべての在庫数値を0にリセット
*/
function resetAllInventoryData() {
try {
console.log('=== 在庫情報リセット開始 ===');

// スプレッドシート設定を取得
const config = getSpreadsheetConfig();

const spreadsheet = SpreadsheetApp.openById(config.spreadsheetId);
const sheet = spreadsheet.getSheetByName(config.sheetName);
const lastRow = sheet.getLastRow();

if (lastRow <= 1) {
console.log('データが存在しません');
return;
}

// C列からK列までを0でクリア
const range = sheet.getRange(2, COLUMNS.STOCK_QTY + 1, lastRow - 1, 9);
const resetValues = Array(lastRow - 1).fill(Array(9).fill(0));
range.setValues(resetValues);

console.log(`${lastRow - 1}行の在庫情報をリセットしました`);
} catch (error) {
console.error('リセットエラー:', error.message);
throw error;
}
}

/**
* スプレッドシートのシート名を確認
*/
function checkSheetNames() {
try {
// スプレッドシート設定を取得
const config = getSpreadsheetConfig();

console.log('使用しているスプレッドシートID:', config.spreadsheetId);
const spreadsheet = SpreadsheetApp.openById(config.spreadsheetId);
const sheets = spreadsheet.getSheets();

console.log('=== スプレッドシート内のシート名一覧 ===');
for (let i = 0; i < sheets.length; i++) {
const sheetName = sheets[i].getName();
console.log(`シート${i + 1}: "${sheetName}" (文字数: ${sheetName.length})`);
}
console.log('');
console.log('現在のSHEET_NAME設定:', `"${config.sheetName}"`);
console.log('上記のシート名のいずれかと完全に一致するようにSHEET_NAMEを設定してください');
} catch (error) {
console.error('シート名確認エラー:', error.message);
console.error('スプレッドシートIDまたは設定が正しいか確認してください');
}
}

/**
* スクリプト設定状況を確認
*/
function checkScriptConfiguration() {
try {
console.log('=== スクリプト設定状況確認 ===');

const properties = PropertiesService.getScriptProperties();

// 必要な設定項目をチェック
const requiredProperties = [
'CLIENT_ID',
'CLIENT_SECRET',
'REDIRECT_URI',
'SPREADSHEET_ID',
'SHEET_NAME',
'ACCESS_TOKEN',
'REFRESH_TOKEN'
];

console.log('【必須設定項目の確認】');
let allConfigured = true;

requiredProperties.forEach(prop => {
const value = properties.getProperty(prop);
const status = value ? '✓ 設定済み' : '✗ 未設定';
console.log(`${prop}: ${status}`);

if (!value) {
allConfigured = false;
}
});

console.log('');
console.log(`【設定完了状況】: ${allConfigured ? '✓ 全て設定済み' : '✗ 未設定項目があります'}`);

if (!allConfigured) {
console.log('');
console.log('【設定方法】');
console.log('GASエディタで以下の手順で設定してください:');
console.log('1. 左メニューの「設定」(歯車アイコン)をクリック');
console.log('2. 「スクリプト プロパティ」セクションで「スクリプト プロパティを追加」をクリック');
console.log('3. 以下のプロパティを追加:');
console.log(' - SPREADSHEET_ID: 対象スプレッドシートのID');
console.log(' - SHEET_NAME: 対象シート名');
console.log(' ※ 認証関連は認証.gsで設定済みの場合はそのまま使用');
}

} catch (error) {
console.error('設定確認エラー:', error.message);
}
}

/**
* スクリプト使用方法ガイド(修正版)
*/
function showUsageGuide() {
console.log('=== 在庫情報取得スクリプト使用方法(修正版) ===');
console.log('');
console.log('【事前設定】');
console.log('スクリプトプロパティに以下を設定してください:');
console.log('- SPREADSHEET_ID: 対象スプレッドシートのID');
console.log('- SHEET_NAME: 対象シート名');
console.log('- その他認証関連設定(認証.gsで設定済みの場合はOK)');
console.log('');
console.log('【設定確認】');
console.log('- checkScriptConfiguration(): 設定状況の確認');
console.log('');
console.log('【前提条件】');
console.log('- 認証スクリプトでトークンが取得済みであること');
console.log('- スプレッドシートに商品コードが入力済みであること');
console.log('');
console.log('【主要関数】');
console.log('1. updateInventoryData()');
console.log(' - 全商品の詳細在庫情報を更新');
console.log(' - 処理時間: 商品数 × 約2.5秒(2つのAPIを呼び出すため)');
console.log('');
console.log('2. updateSingleProduct("商品コード")');
console.log(' - 特定商品の詳細在庫情報のみ更新(テスト用)');
console.log(' - 例: updateSingleProduct("dcmcoverg-s-S")');
console.log('');
console.log('3. testStockMasterFields("商品コード")');
console.log(' - 在庫マスタAPIの動作確認(デバッグ用)');
console.log('');
console.log('4. resetAllInventoryData()');
console.log(' - 全在庫数値を0にリセット(テスト用)');
console.log('');
console.log('【更新される在庫情報】');
console.log('- C列: 在庫数');
console.log('- D列: 引当数');
console.log('- E列: フリー在庫数');
console.log('- F列: 予約在庫数');
console.log('- G列: 予約引当数');
console.log('- H列: 予約フリー在庫数');
console.log('- I列: 不良在庫数');
console.log('- J列: 発注残数');
console.log('- K列: 欠品数');
console.log('');
console.log('【注意事項】');
console.log('- APIレート制限のため各商品間に2秒の待機時間があります');
console.log('- 商品マスタAPIと在庫マスタAPIの2つを順次呼び出すため処理時間が長くなります');
console.log('- 大量の商品がある場合は処理時間が長くなります');
console.log('- エラーが発生した商品はスキップされます');
console.log('');
console.log('【実行推奨順序】');
console.log('1. まず checkScriptConfiguration() で設定確認');
console.log('2. 次に testStockMasterFields("商品コード") でAPIの動作確認');
console.log('3. 次に updateSingleProduct("商品コード") で詳細在庫取得をテスト');
console.log('4. 問題なければ updateInventoryData() で全更新');
}

/**
* テスト実行用関数(設定値を使用)
*/
function testSingleUpdate() {
// デフォルトの商品コードでテスト(実際の商品コードに変更してください)
updateSingleProduct("dcmcoverg-s-S");
}
--------------------------------------------------
こちらのコードですが、ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:認証編と同じ、NE_在庫情報取得というプロジェクトの中に認証編とは別のスクリプトで作成しておりますので、ハードコーディングを行うことなく、認証編と同じスクリプトプロパティを使用することが出来ます。

因みに、こちらのコーディングを行う前に行ったプロンプトエンジニアリングの中の文章ですが、
--------------------------------------------------
ヘッダーに
商品コード 商品名 在庫数 引当数 フリー在庫数 予約在庫数 予約引当数 予約フリー在庫数 不良在庫数 発注残数 欠品数 JANコード
が並んでおりまして、商品コード 商品名 JANコードは商品コードを追加するたびにシートを最新の商品コードを含んだものに上書きするのでネクストエンジンからの取得は不要です。
在庫数 引当数 フリー在庫数 予約在庫数 予約引当数 予約フリー在庫数 不良在庫数 発注残数 欠品数
の情報をネクストエンジンからAPI取得したいと思いますので、コード案を出してもらえますか?
--------------------------------------------------
という指示を出しています。

こちらのスクリプトを実行すると・・・
おぉ~取得できるようになりました(^^)

ですが、取得できる件数が少なかったので、
--------------------------------------------------
// API制限を考慮して少し待機(2秒)- 2つのAPIを呼び出すため少し長めに設定
Utilities.sleep(2000);
--------------------------------------------------
を修正して、
--------------------------------------------------
// API制限を考慮して少し待機(0.5秒)- 2つのAPIを呼び出すため少し長めに設定
Utilities.sleep(500);
--------------------------------------------------
に修正しました。

このことで200行程まででしたら在庫情報の取得を行うことが出来ました(^^♪

すげ~な(・・;)

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGoogle Apps Script(GAS)の開発がClaudeで行える時代なのですね(・・;)
しかも、私はGASの開発を殆ど行っていないと言えるほどの、少ない知識レベルしか持ち合わせていません。

というものの、私はエンジニアなのでClaudeに適切なプロンプトエンジニアリングを行った結果なのでしょうね。

こちらの記事はQiitaにて公開予定です。

また、ネクストエンジンでアプリを作る際に、
"特定の企業に公開"では無料の範囲がかなり狭められているのに対して、
"すべての企業に公開"とすることで制限が解除されます。

私の望む月間APIコール数では特定の企業に公開の無料の範囲内では実現不可能でした(-_-;)
"すべての企業に公開"にすることで、ネクストエンジン単体では不足している機能がアプリで実現できることから、ネクストエンジン自体の顧客数獲得に繋げようとしているのでしょうね。

Qiitaの記事は、私が作成したアプリの説明書に転用できます。
そういうのも踏まえたうえで、Qiitaでの公開を選考して行ったわけです。

また、ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:認証編ではClaudeの作成したコードをGASのスクリプトに書きに行くだけだったのですが、バージョン管理を行う必要性を感じて、VS Code&GitHubでパブリックでの作成を行うことにしました。

プライベートではなくパブリックで作成すると言うのは、いわゆるオープンソースでの開発になるのですが、こうすることでGitHubでの制限もかなり解除されます。

一気に新しい単語が出てきましたよね。
私もそう思います^^;

すべての単語がいわゆるエンジニア語です。
その単語に対して理解できないとわからない言葉です。

ですが、エンジニアのやりたいという欲求は止めようがないのです。

ネクストエンジンとGoogleスプレッドシートをAPI接続できるGASの開発:認証編で、やっとスタートに立つことが出来ました。

ですが、それまで使用していたGeminiやGPT-5ではバージョン管理が出来ない故の動作ができなくなるという状況に陥ってしまいました。
失敗事例を経験したわけです。

余談ですが、エンジニアというのは毎日失敗事例を鬼のように経験しています。
何でも出来るというエンジニアと思われがちですが、
新しいことを行う際に失敗なんて無い筈がないので、
毎日一歩すら前に進めない日々を過ごすなんて日常です。

前に進めないというのは毎日膨大な失敗をしているということです。
エンジニアという人種は毎日狂気と思えるほどの失敗経験を得て
今その位置に立っているのです。

その失敗に対して後悔しない、という言い方は適切ではないと思いますが、
失敗した事例を元に別のやり方を行って成功に導くというのを
毎日延々と繰り返しています。

バックプロパゲーション(誤差逆伝播法)でもご存知のように
人は失敗することでしか学習できないのです。

Claudeでのコーディング:ネクストエンジンAPI認証編
でも書きましたが、
----------------------------------------
あらゆる興味に対して勉強する姿勢で失敗は決して無い。
失敗はやり遂げられなかったという事実によって発生する。
----------------------------------------
間違いを恐れない。
諦めたことこそが失敗であり、
やり遂げたことこそが人生の成功体験である。
----------------------------------------
と私は思っていますし、
そうであることを信じています。
----------------------------------------

閑話休題

考えてみるとたしかに、認証を乗り越えて在庫数の取得までは進んだように思います。
ですが、ClaudeのArtifactsは当時の(と言っても2週間ほど前の)GeminiやGPTにはありませんでした。

GeminiやGPTには更にバグが発生した際の、デバッグ能力も不足していました。
GeminiやGPTやGitHub Copilotにはバグが発生した際にコーディングを修正するという機能はありますが、Claudeはコードの中にエラーログを残すコーディングを行うことで、デバッグ能力が飛躍的に上がるのだと思いました。

途中まで上手くいったのに、機能を追加したことによりバグが発生したことに対してバージョン管理をしたいという欲求がとても高まってきて、
GitHub Copilotで使用したVS Codeを使うことで、
バージョン管理を行いながらオープンソース化することが出来ると学びました。

しかも、一人で大規模なプロジェクトを行うというのは現実的に不可能なことに対して、世の中で行われていることというのを学んで、GitHubの必要性に迫られました。

ですが、GitHubだけでは私のような初心者に使いこなすことは難しいと感じて、VS CodeとGitHubを組み合わせて使いやすい環境を整えることにしたわけです。

VS CodeとGitHubの組み合わせはたまたま上手く言っている状況なので、それを記事にするには中々ハードルが高いのですが・・・
考えたらGitHubだけでも記事にするには中々ハードルが高いですね^^;

どちらも大規模プロジェクトに対応出来るツールですからね^^;

と言うものの、たまたま上手く言っている今の状況をフル活用して
引き続き開発を進めたいと思います。
--------------------------------------------------
在庫情報取得.gsにより200件の在庫情報の取得を行うことが出来るだけでも凄いと思ますが、
それが出来たことで今日一日で開発が一気に進みました。

しかし、在庫情報取得.gsでは200件程の在庫情報取得が上限になりました。
もっと沢山の在庫情報を取得したい!
と言う欲求から更に新しいスクリプトの開発を行うことにしました。

これが、
在庫情報取得_高速版.gs
です。
これは上記とスクリプトではない、別の改良版スクリプトです。

1回の取得件数が200行ほどという状況から、
プロンプトエンジニアリングを行うことで、
今日の午前中だけで1回のスクリプトで今までより1.5倍の在庫情報が取得できまして、
取得できなかった在庫情報も次に同じスクリプトを実行することで、
前回取得できた最終行の次の行から取得することが出来るようになりました。
これが、
在庫情報取得_高速版.gs
です。

一気に進んで大事なことなので2度言いました(笑)

また、最終行まで取得できたら
次の実行の際には最初から取得するというスクリプトになりました。
これが今日の午前中で出来た内容です。
--------------------------------------------------
ですが、GASの制限を鑑みると
在庫情報取得_高速版.gs
でも制限に接触することが見込まれたので、
Claudeに対して
一部を抜粋して
--------------------------------------------------
例えば、数十の商品コードをネクストエンジンに渡して、一気に在庫情報を取得するというようなスクリプトを書くことは可能ですか?
--------------------------------------------------
というプロンプトエンジニアリングを行うと・・・

商品マスタAPI(/api_v1_master_goods/search)を使った方法:
商品コードを配列で渡すことで、複数商品の情報を一度に取得できます。一回のAPIコールで最大100件程度まで処理可能です。

と回答してくれました。
おぉ~(・・;)
それが実現できるとスクリプトの実行時間の大幅な短縮に繋がります。

で、午後のかかりの1時間ほどで実現可能になりました^^;

これが、
在庫情報取得_一括処理版.gs
です。

3000行の在庫情報取得がわずか2分ほどで取得可能になりました(・・;)

これならば普段使いの見込みが経ってきました。

すげ~な(・・;)

このスクリプト・・・
売り物になるんじゃないんですかね?^^;

当初からオープンソース化する予定でしたので、
もう一度はじめからプロジェクトを作成するところから
Qiitaで説明書を作成するつもりですが、
会社がなんていうかわかりませんが・・・
言ってもわからない可能性がとてもありそうな予感^^;
わかってくれなくて難癖つけられても、
無料枠での制限がかけられるのでオープンソース化が最適だと思います。
と報告するつもりです。

途中から内容が詰め込まれすぎましたので、
別のブログにて発表したいと思いますm(_ _)m
Posted at 2025/09/10 22:58:47 | コメント(0) | トラックバック(0) | AI | 日記

プロフィール

「バイブコーディングという新しい開発手法 http://cvw.jp/b/11052/48699400/
何シテル?   10/07 22:37
2025/06/23追記 ヘッダー画像について興味をお持ちの方もいらっしゃると思いますが、こちらは2025/06/21に香川県で開催されました、GAZOO 愛...
みんカラ新規会員登録

ユーザー内検索

<< 2025/10 >>

    1 2 34
5 6 7891011
12131415161718
19202122232425
262728293031 

リンク・クリップ

GAZOO 愛車広場 出張取材会 in 香川 満を持してチェイサーの記事が公開されました(^^) 
カテゴリ:その他(カテゴリ未設定)
2025/08/22 08:26:45
電費履歴リセット 
カテゴリ:その他(カテゴリ未設定)
2024/10/12 07:34:32
裏ワザでウィンドウズ11へ^^笑)。 
カテゴリ:その他(カテゴリ未設定)
2024/01/27 14:12:45

愛車一覧

トヨタ チェイサー GR Chaser TourerV TRD sports version (トヨタ チェイサー)
2025/06/21 GAZOO様の出張取材会で取材を頂きました。 https://ga ...
日産 リーフ 日産 リーフ
JZX100 Chaser 1JZ-GTE 2500cc ターボを所有しつつのセカンドカ ...
ホンダ アコード ホンダ アコード
私のではなく弟の車です。 色はアークティックブルーパールのEuro-Rです。 購入してし ...

過去のブログ

2025年
01月02月03月04月05月06月
07月08月09月10月11月12月
2024年
01月02月03月04月05月06月
07月08月09月10月11月12月
2023年
01月02月03月04月05月06月
07月08月09月10月11月12月
2022年
01月02月03月04月05月06月
07月08月09月10月11月12月
2021年
01月02月03月04月05月06月
07月08月09月10月11月12月
2020年
01月02月03月04月05月06月
07月08月09月10月11月12月
2019年
01月02月03月04月05月06月
07月08月09月10月11月12月
2018年
01月02月03月04月05月06月
07月08月09月10月11月12月
2017年
01月02月03月04月05月06月
07月08月09月10月11月12月
2016年
01月02月03月04月05月06月
07月08月09月10月11月12月
2015年
01月02月03月04月05月06月
07月08月09月10月11月12月
2014年
01月02月03月04月05月06月
07月08月09月10月11月12月
2013年
01月02月03月04月05月06月
07月08月09月10月11月12月
2012年
01月02月03月04月05月06月
07月08月09月10月11月12月
2011年
01月02月03月04月05月06月
07月08月09月10月11月12月
2010年
01月02月03月04月05月06月
07月08月09月10月11月12月
2007年
01月02月03月04月05月06月
07月08月09月10月11月12月
2006年
01月02月03月04月05月06月
07月08月09月10月11月12月
2005年
01月02月03月04月05月06月
07月08月09月10月11月12月
ヘルプ利用規約サイトマップ
© LY Corporation