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

ヒデノリのブログ一覧

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 | 日記
2025年09月09日 イイね!

Canon EOS 7D リーフの撮り比べ

Canon EOS 7D リーフの撮り比べ昨夜から撮影が可能になったCanon EOS 7Dをリーフに積んで、
午前中の屋島ハイキング終わりにレグザムフィールドにて撮り比べてみました。

スマホはAIモード、コンデジと一眼レフカメラはフルオートモードです。

・記録カメラ:スマホ Ulefone Power Armor14 Pro

Ulefone Power Armor14 Pro
ƒ/1.8 1/100 5.46 mm ISO 119

・撮影カメラ:コンデジ CASIO EX-Z920

CASIO COMPUTER CO.,LTD. EX-Z920
ƒ/3.5 1/80 4.6 mm ISO 100

・撮影カメラ:一眼レフ Canon EOS 7D

Canon EOS 7D
ƒ/4.5 1/60 18 mm ISO 100

拡大して見比べるとヘッドライト周りを見ると案外悪くないのがスマホのように感じてちょっとびっくり(・・;)
CAISOのコンデジはヘッドライト周りがどうもボンヤリ(-_-;)
全体をスマホとCanonの一眼レフで比較すると、やっぱりスマホのほうがぼんやりした感じですね。

また、撮影用ナンバーと三角コーンの色を見比べると、
スマホとコンデジは色が薄く見えるのに対して、一眼レフは色が濃く見えます。

また、デジタルカメラで撮影された画像には、**Exif(Exchangeable Image File Format)**と呼ばれるメタデータが記録されています。これは、撮影日時、カメラの機種、シャッタースピード、F値、ISO感度といった撮影条件に関する情報だけでなく、GPS機能があれば位置情報も含まれています。このデータは画像ファイルに付随しており、写真管理や編集に役立ちます。

Exif情報はいくつか有るので、Googleフォトで確認できる4つについて調べてみました。

---------------------------------------------------------------------

Canon EOS 7D

ƒ/4.5:絞り値
絞り値は「F値」とも呼ばれ、レンズからカメラに入る光の量を調整する役割を担っています。数値が小さいほど絞りが大きく開き、より多くの光を取り込めるため写真が明るくなります。また、ピントが合う範囲(被写界深度)が狭くなり、背景が大きくボケた写真を撮ることができます。一方、数値が大きいほど絞りが小さく絞られ、光が少なくなるため暗くなり、手前から奥までピントの合ったシャープな写真になります。

1/60:露出時間
露出時間、別名「シャッタースピード」は、カメラのシャッターが開いている時間のことです。この時間が長いほど、より多くの光がセンサーに届くため、写真は明るくなります。逆に、時間が短いと写真は暗くなります。
露出時間は、写真の明るさを調整するだけでなく、被写体の動きの表現にも影響します。時間が短いと、動きの速い被写体もブレずに一瞬を捉えることができます。一方、時間が長いと、動く被写体がブレて写るため、車のライトの光跡や水の流れを糸のように表現するなどの特殊な効果を生み出せます。

18 mm:焦点距離
焦点距離は、レンズの中心からイメージセンサーまでの距離のことで、写真に写る範囲(画角)を決める重要な要素です。焦点距離が短いほど画角が広くなり、広い範囲を写せる「広角レンズ」となります。風景写真や集合写真に適しています。一方、焦点距離が長いほど画角が狭くなり、遠くの被写体を大きく写せる「望遠レンズ」となります。動植物やポートレート撮影などで背景を大きくぼかす際にも使われます。この焦点距離の違いが、写真の印象を大きく左右します。

ISO 100:ISO速度(ISO感度)
ISO速度(ISO感度)は、デジタルカメラのイメージセンサーが光を感じ取る能力を示す数値です。数値が高いほど感度が高くなり、より少ない光で写真を明るく撮影できます。暗い場所での撮影や、速いシャッタースピードで被写体のブレを抑えたい場合に有効です。しかし、ISO速度を上げすぎると、画像に「ノイズ」と呼ばれるザラザラした粒状感が発生し、画質が劣化するというデメリットもあります。そのため、撮影シーンに応じて適切なバランスを見つけることが重要です。

---------------------------------------------------------------------

それぞれを比較していきます。
絞り値
Ulefone Power Armor14 Pro:ƒ/1.8
CASIO EX-Z920:ƒ/3.5
Canon EOS 7D:ƒ/4.5
数値が大きくなるほど絞りが小さくなるということでしたね。
Canonが一番大きいということは一番絞りが小さいということですので、
多くの光が撮像素子(センサー)に入ってきていることがわかります。

露出時間
Ulefone Power Armor14 Pro:1/100
CASIO EX-Z920:1/80
Canon EOS 7D:1/60
露出時間が短いほど撮像素子(センサー)に入る光が少なくなるのでしたよね。
絞り値と同様にCanonが一番多くの光が撮像素子(センサー)に入ってきていることがわかります。
露出時間が短いメリットは動きの速い被写体もブレずに一瞬を捉えることができます。
一方、露出時間を長くすることで車のライトの光跡や水の流れを糸のように表現するなどの特殊な効果を生み出せます。

ですが、後者を設定できるのは私の環境ではスマホと一眼レフカメラでした^^;
そう考えると、10年前の一眼レフカメラで設定できると言うのは、この機種はその時代で完成された商品であるということと、え?スマホで出来るん?というびっくり感がありました^^;

焦点距離
Ulefone Power Armor14 Pro:5.46 mm
CASIO EX-Z920:4.6 mm
Canon EOS 7D:18 mm
焦点距離が長いほど画角が狭くなり、遠くの被写体を大きく写せる「望遠レンズ」となります。
単位が違うのは、そらあんだけデカかったらさもありなん。
まぁ私の今の腕ではその効果を十分感じることはまだ難しいです(-_-;)

ISO速度(ISO感度)
Ulefone Power Armor14 Pro:ISO 119
CASIO EX-Z920:ISO 100
Canon EOS 7D:ISO 100
数値が高いほど感度が高くなり、より少ない光で写真を明るく撮影できます。
ですが、それぞれの数値がだいたい合っているのはオートモードでお任せ撮影しているからでしょうね。

一眼レフカメラ・・・
チェイサーと同じく沼ですよ(笑)

---------------------------------------------------------------------

ですが、道具というのは作られた目的が有るわけで、
ポケットに入れたスマホで遠い距離を撮影で出来るわけではなく、
タイヤハウスに入ってブレーキキャリパーの裏側を汚れた手で一眼レフカメラで撮影できるわけではなく、
それぞれのシチュエーションに合った道具というものが存在しているわけです。

どんな道具も使うのは結局人間ということですね。

その道具の優れたところと使い方を判断できやすいのはエンジニアという人種なのだと思います。

エンジニアは日本語ではなくエンジニア語を喋ります。
一般的には英語のエンジニア語を喋ります。

ですが、英語が喋れるからと言ってエンジニア語が理解できるわけではありません。

エンジニア語はその一言で膨大な情報が詰め込まれているわけです。

一言で膨大な情報が詰め込まれているわけですから、
エンジニアがエンジニア語で喋った文章を理解できるのは、
同じエンジニアという人種だけということになります。

エンジニアという人種は余計な説明をすることで、
その機能が狭められるのを何よりも恐れています。
物事の本質だけを言って、
そのことで他のエンジニアがその道具を使いこなしてもらうこと、
何ならば設計外の使い方をしてくれることを期待しています。

作った道具を使いこなしてくれること、
何なら設計以上の能力を発揮してくれることに、
エンジニアは期待して毎日を過ごしています。

---------------------------------------------------------------------

因みに、こちらの2枚目~の画像はみんカラ画像を圧迫させないためと、
色々管理を行いやすくするために書いたブログである
みんカラのブログ用追加画像容量である5.0GBを突破する方法
で紹介しました
Embed Google Photos
を使用しました。
Posted at 2025/09/09 21:45:39 | コメント(0) | トラックバック(0) | リーフ | 日記
2025年09月09日 イイね!

Canon EOS 7D CFカードアダプターを使ったSDカードのフォーマット

Canon EOS 7D CFカードアダプターを使ったSDカードのフォーマット2009年10月に発売され、2014年10月頃まで販売されていたCanon EOS 7Dですが、
記録メディアは当時高速と言われていたCFカードです。
CFカードをご存知のない方もいらっしゃいますよね(-_-;)

私が初めて購入したコンパクトデジタルカメラ(コンデジ)はCFカードでした。

令和の今どきCFカードって有るん?^^;

AmazonとAliexpressで探したらありました。
ですが、Gemini 2.5 FlashとGPT-5とClaudeのオススメの容量と速度(32GB UDMA)で
5000円~高っ(-_-;)

も~(-_-;)

という事で、CFカードアダプターを介してSDカードを購入することにしました。

メーカー名はわかりませんが、こういう見た目の商品はあちこちで売っていると思います。
Amazon:¥1,338


最高約8コマ/秒の高速連写(バーストモード)に対応すべく、購入したのが
SanDisk SDカード 64GB SDXC Class10 UHS-I 読取り最大140MB/s Ultra SDSDUNB-064G-GH3NN
Amazon:¥1,071
購入したEOS 7Dの場合、容量のオススメは32GBなのに64GBを購入した理由は、この辺りの容量の違いは価格にあまり影響しないため^^;

因みに、この速度Class10以上というのも上記のAIがオススメしてくれました。

128GBは1700円程でした^^;
そこまでの容量は必要ないかな?


で、CFカードアダプターを介してSDカードをカメラにさすと・・・
カードにアクセスできません・・・(-_-;)
失敗したか?(-_-;)


いゃ待てよ。
フォーマット形式が違うのか?
と思ってパソコンで見るとexFATでした。
EOS 7DってexFAT認識できないんじゃないの?
CFカードアダプターの失敗例ってフォーマット形式の違いじゃないんですかね?^^;


表示に従いEOS 7Dでフォーマットを行います。
メニューを押して画面にメニューを表示させます。


マルチコントローラーを操作してカード初期化を選択して、カメラでフォーマットをします。


FAT32でフォーマットされてカメラから認識できました。
因みにWindows11ではフォーマッター等を使わないと64GBのSDカードはFAT32でフォーマットできませんでした(-_-;)


我が家のベランダにおいてある観葉植物をガラス越しに撮影してみました。
すんげ~(・・;)

私のEOS 7Dと64GBのSDカードの組み合わせでは、SDカードがいっぱいになる前に電池が切れるという状態です。
AIからアドバイスを貰ったのですが、とりあえず写真を取りまくってベストショットを保存するという方法で一眼レフカメラの魅力を堪能したいと思います。

という事で、写真用の2つ目のGoogleアカウントの作成を行いました^^;

因みに、こちらの2枚目~の画像はみんカラ画像を圧迫させないためと、
色々管理を行いやすくするために書いたブログである
みんカラのブログ用追加画像容量である5.0GBを突破する方法
で紹介しました
Embed Google Photos
を使用しました。
Posted at 2025/09/09 09:02:48 | コメント(1) | トラックバック(0) | EOS 7D | 日記

プロフィール

「車検どうするかな~? http://cvw.jp/b/11052/48744393/
何シテル?   11/02 21:27
2025/06/23追記 ヘッダー画像について興味をお持ちの方もいらっしゃると思いますが、こちらは2025/06/21に香川県で開催されました、GAZOO 愛...
みんカラ新規会員登録

ユーザー内検索

<< 2025/11 >>

      1
2345678
9101112131415
16171819202122
23242526272829
30      

リンク・クリップ

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