はじめに
私は小売業で複数店舗を担当しており、
・店舗チーフやパートスタッフの教育
・本部施策や徹底事項の実施確認
・売場づくりの指導や水平展開
などの業務を普段行っています。今回は、そんな日々の業務の中で感じていた「売場改善報告書づくりの手間」を減らすために、人生で初めてGoogle Apps Script(GAS)に挑戦した話です。
店舗を巡回していると、
「本部で決めた売場になっていないな」
「この商品の見せ方、少し変えた方が良さそうだな」
「この成功事例、他店舗にも展開したいな」
と思うことがよくあります。
そのため、必要に応じて売場を修正したり、改善提案を行ったりすることがあります。
そして、その内容を本部や上司へ報告するのも大切な仕事のひとつです。
今回は、そんな日々の業務の中で感じていた「売場改善報告書づくりの手間」を減らすために、人生で初めてGoogle Apps Script(GAS)に挑戦した話です。
売場を直すより、報告書を作る方が大変だった
店舗巡回中、本部施策が正しく展開されていない場合があります。
例えば、
- POPの位置が違う
- 商品の並び順が違う
- 売場の見せ方がもったいない
などです。
その場合は、その場で売場を修正します。しかし問題はその後でした。
改善内容を報告するため、
- Before写真
- After写真
- 作業コメント
を資料にまとめることがあります。
これまでの報告フローは
-
Beforeの売り場を撮影
-
売り場を手直し
-
Afterの売り場を撮影
-
PCでExcelやPowerPointを立ち上げる
-
写真を取り込み、貼り付け、サイズ調整
-
コメントを入力
-
ファイルを保存して上司へ送信
地味だけど面倒な作業
特に大変だったのが写真の貼り付けです。
サイズを合わせる、配置を整える、少しズレる、また直す。
報告書1件なら大したことはありません。
でも、これが何件も重なると意外と時間を取られます。
気付けば、「売場を直す時間より報告書を作る時間の方が長いんじゃないか?」 と思うこともありました。
写真を撮るだけで報告書が完成したら?
ある日ふと思いました。
「写真を撮ってコメントを書くだけで報告書ができたら楽なのでは?」今回実現したかったのは、とてもシンプルです。
やりたかったこと
- スマホやPCで簡単に完結
- Before写真を登録
- After写真を登録
- コメントを入力
- PDFを自動生成
事務所へ戻ってPowerPointやExcelで作成しなくても良い。
そんな仕組みを目指しました。
完成イメージ
そもそもGASって何?
今回使ったのはGoogle Apps Script(GAS)です。
Googleが提供しているサービス同士を連携させたり、自動化したりできる仕組みです。
正直に言うと、私は今回初めて触りました。「名前は聞いたことあるけど難しそう」というレベルです。
それでも、
Googleフォーム
↓
Googleドライブ
↓
GAS
↓
PDF作成
という流れならできそうだと思い、挑戦してみることにしました。
AIに相談しながら作ってみた
今回も頼ったのはAIです。
まずは、「こんな仕組みを作りたい」という内容を整理して相談しました。
すると、
必要な手順やサンプルコードを提案してくれました。
その時は、「思ったより簡単にできるかもしれない」 と思っていました。
現実はそんなに甘くなかった
実際に作り始めると、すぐに壁にぶつかりました。
コードを貼り付ける。
エラー。
修正する。
またエラー。
別の方法を試す。
またエラー。
前回のPower Automateの記事と同じ展開です(笑)
特に苦労したところ
- 写真の反映
- PDFレイアウト
- 画像配置
AIが教えてくれるコードをそのまま貼れば完成。
そんな簡単な話ではありませんでした。
「本当にできるのか...」
途中で何度も思いました。
でも、
エラー内容を調べる
↓
AIに聞く
↓
修正する
↓
試す
これを繰り返していくうちに、少しずつ前に進めました。
3日後、ついに完成
試行錯誤を続けること約3日。
テスト実行後にGoogleドライブを開くと、そこにはPDFが生成されていました。
思わず声が出ました。 「できたーーー!」
本当に嬉しかったです。
たった1枚のPDFです。
でも、自分で考えた仕組みが動いた瞬間でした。
完成した仕組み
作成したコード
コードを開く
/**
* フォーム送信時に実行されるメイン関数(公開用サンプル)
*/
function onFormSubmit(e) {
let sheet;
let row;
if (e && e.range) {
sheet = e.range.getSheet();
row = e.range.getRow();
} else {
sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
row = sheet.getLastRow();
Logger.log("手動で実行されました。最新の行(" + row + "行目)を処理します。");
}
if (row <= 1) {
Logger.log("処理できるデータ行がありません。");
return;
}
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const rowValues = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0];
let beforeCell = "";
let afterCell = "";
let storeName = "";
let commentParts = [];
for (let i = 0; i < headers.length; i++) {
const headerName = headers[i].toString().trim();
const val = rowValues[i] ? rowValues[i].toString().trim() : "";
if (!val) continue;
if (val.includes("drive.google.com")) {
if (!beforeCell) beforeCell = val;
else if (!afterCell) afterCell = val;
}
else if (headerName.includes("店") || val.includes("店")) {
storeName = val;
}
else if (headerName !== "タイムスタンプ") {
commentParts.push(val);
}
}
function getBase64Image(cellValue, label) {
if (!cellValue) return null;
const url = cellValue.split(",")[0].trim();
const match = url.match(/id=([a-zA-Z0-9_-]+)/);
if (match && match[1]) {
try {
const file = DriveApp.getFileById(match[1]);
const blob = file.getBlob();
return `data:${blob.getContentType()};base64,${Utilities.base64Encode(blob.getBytes())}`;
} catch(err) {
Logger.log(label + "の画像データ取得に失敗しました: " + err.message);
return null;
}
}
return null;
}
const beforeBase64 = getBase64Image(beforeCell, "Before画像");
const afterBase64 = getBase64Image(afterCell, "After画像");
if (!beforeBase64 || !afterBase64) {
Logger.log("【終了】画像が解析できなかったため、PDF生成を停止しました。");
return;
}
// 🔒 セキュリティのため公開用にフォルダーIDを書き換えています
const folderId = "★ここに保存先のGoogleドライブのフォルダIDを入力★";
const folder = DriveApp.getFolderById(folderId);
const timestamp = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm");
const timestampFile = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyyMMdd_HHmmss");
const rawComment = commentParts.join("\n\n");
const formattedComment = rawComment ? rawComment.replace(/\n/g, '<br>') : "(記述なし)";
// HTML組み立て
const html = `
<html>
<head>
<style>
@page { size: A4 landscape; margin: 3mm 4mm; }
body { font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', sans-serif; margin: 0; padding: 0; color: #333; line-height: 1.2; }
.header-container { width: 100%; position: relative; text-align: center; margin-bottom: 1px; padding: 2px 0; }
.header-title { font-size: 22px; font-weight: bold; color: #1e293b; display: inline-block; }
.meta { position: absolute; right: 4px; bottom: 4px; font-size: 10px; color: #64748b; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; margin-bottom: 2px; }
.col-img { width: 48%; text-align: center; }
.col-arrow { width: 4%; text-align: center; vertical-align: middle; }
td { padding: 0; vertical-align: top; }
h3 { margin: 0 0 1px 0; font-size: 13px; color: #475569; font-weight: bold; text-align: center; text-transform: uppercase; }
.img-container { width: 100%; height: 480px; display: table; background: #fff; }
.img-cell { display: table-cell; vertical-align: middle; text-align: center; }
img { max-width: 100%; max-height: 480px; object-fit: contain; border: 1px solid #cbd5e1; border-radius: 4px; }
.arrow { font-size: 32px; font-weight: bold; color: #ef4444; line-height: 480px; }
.comment-box {
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 8px 12px;
background: #f8fafc;
font-size: 13px;
line-height: 1.4;
text-align: left;
margin-top: 2px;
height: 180px;
max-height: 200px;
overflow: hidden;
box-sizing: border-box;
}
.comment-title { font-size: 11px; font-weight: bold; color: #475569; margin-bottom: 4px; display: block; border-bottom: 1px solid #e2e8f0; padding-bottom: 2px; }
</style>
</head>
<body>
<div class="header-container">
<div class="header-title">売り場変更報告 【${storeName || '未入力'}】</div>
<div class="meta">作成日時:${timestamp}</div>
</div>
<table>
<tr>
<td class="col-img">
<h3>BEFORE</h3>
<div class="img-container"><div class="img-cell"><img src="${beforeBase64}"></div></div>
</td>
<td class="col-arrow"><div class="arrow">➡</div></td>
<td class="col-img">
<h3>AFTER</h3>
<div class="img-container"><div class="img-cell"><img src="${afterBase64}"></div></div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="comment-box">
<span class="comment-title">【コメント】</span>${formattedComment}
</div>
</td>
</tr>
</table>
</body>
</html>
`;
const blob = Utilities.newBlob(html, "text/html", "report.html");
const pdf = blob.getAs("application/pdf");
const pdfFile = folder.createFile(pdf);
pdfFile.setName(`売り場変更報告_${storeName || '名称未設定'}_${timestampFile}.pdf`);
Logger.log("PDFの生成に成功しました: " + pdfFile.getName());
}
実際どれくらい楽になったのか
Before
- 写真整理
- PowerPoint編集
- PDF化
約20〜30分
After
- 写真撮影(アップロード)
- コメント入力
約5分
効果
報告書作成に使っていた時間を大幅に削減できました。
浮いた時間を、
- 店舗巡回
- 売場改善指導
- スタッフ教育
など、本来やるべき業務に時間が使えるようになりました。
初心者だからこそ感じたこと
今回改めて思ったことがあります。
それは、 現場の困りごとは、現場にいる人が一番知っている ということです。
私はエンジニアではありません。
GASも今回が初めてでした。
それでも、「面倒だな」と思っていた作業を見直し、少しずつ調べながら形にすることができました。
AIは魔法ではない
今回わかったことがあります。
AIは便利です。
でも、AIに聞けば一発で完成するわけではありません。
試す。
失敗する。
調べる。
また試す。
結局はその繰り返しでした。
でも、その過程で学んだことはとても大きかったです。
今後の展望
今回作った仕組みはまだ試作品です。
今後は、
- Teams通知との連携
- 会議資料向けにレイアウト改善
- 他の方に使ってもらい、ブラッシュアップする
なども進めていきたいと思っています。
おわりに
今回作った仕組みは、まだ完成形ではありません。
もっと改善したい部分もあります。
それでも、
Before写真を撮る
↓
売場を修正する
↓
After写真を撮る
↓
コメントを書く
↓
PDFが完成する
ここまでできた時は本当に嬉しかったです。
デジタルが苦手な私でも、調べながら形にすることができました。
だからこそ、「自分には難しそうだな」と思っている人ほど、一度挑戦してみてほしいです。
私もまだ勉強中ですが、これからも現場で感じた「面倒」を少しずつ改善していこうと思います。
最後まで読んでいただき、ありがとうございました。
もし「こうするともっと良くなるよ!」というアイデアがあれば、ぜひコメントで教えてください。





