発注フォームの商品を店舗ごとに出し分ける方法|商品マスタに「対象店舗」列を1本足す設計と、画面で隠すだけでは対象外の店から注文できてしまう理由
発注フォームの商品を店舗ごとに出し分ける方法|商品マスタに「対象店舗」列を1本足す設計と、画面で隠すだけでは対象外の店から注文できてしまう理由
スプレッドシートとGAS(Google Apps Script)で作った発注フォームを、複数の店舗で使っている。そこへ「この商品は設備を入れている店だけが使うので、その店にだけ出したい」という要望が来る。多店舗の運用では、遅かれ早かれ必ず出てくる話です。
結論を先に書きます。
- 商品マスタに「対象店舗」という列を1本足す。店名や店舗コードをプログラムの中に書かない
- 書式は3通りだけにする。空欄=全店に表示/店舗コードを列挙=その店だけ/先頭にマイナス=その店だけ非表示
- 絞り込みは画面の表示と、注文を受け取る側の検証の2か所に入れる。表示だけで隠すと、対象外の店からでも注文が通ってしまう
この記事では、名古屋の美容サロンFC(25店舗)で使っている発注フォームに、12店舗限定の商品を追加したときの設計を例に、書式の決め方・実装の要点・テスト項目までをまとめます。
なぜ「コードに店名を書く」やり方を避けるのか
最初に思いつくのは、プログラムに条件を書く方法です。「もし店舗がA店かB店なら、この商品を表示する」。1商品だけなら10分で終わります。
ただ、この方法は次の変更のたびに開発者の手が必要になります。
- 対象店舗が1店増えた
- 別の限定商品が増えた
- 逆に「この店にだけ出したくない」商品が出てきた
発注フォームの商品は、現場の都合で頻繁に入れ替わります。そのたびにコードを直してデプロイし直す運用は、早い段階で回らなくなります。商品の追加・価格変更を本部の担当者がスプレッドシートだけで完結できる状態を保つこと。これが、店舗限定の機能を足すときにも守るべき前提です。
そこで、出し分けのルールも商品マスタのデータとして持たせます。プログラムは「列に書かれたルールを読んで判定する」だけにしておけば、対象店舗の変更はセルを書き換えるだけで済みます。
書式は3通りに絞る
商品マスタ(1行=1商品のシート)の右端に「対象店舗」列を足します。今回の例では、既存の15列(A〜O列)の隣、P列に追加しました。セルの書き方は次の3通りです。
| セルの値 | 動き |
|---|---|
| 空欄 | 全店に表示 |
store-a, store-b |
書かれた2店にだけ表示(限定) |
-store-a |
store-a にだけ表示しない(除外) |
設計で決めておくべきことが4つあります。
1. 空欄は「全店」にする
これがいちばん大事です。空欄を全店表示にしておけば、既存の商品は何も書き足さなくても今までどおり動きます。今回の例では既存の38商品すべてが空欄のまま、挙動は一切変わっていません。列を足しただけで既存商品が消える、という設計にしてはいけません。
さらに、「対象店舗」列そのものがシートに無い場合も全店表示として扱います。プログラムを先に更新しても、列を足すまでは従来どおり動く。この後方互換があると、切り替えの順序に余裕ができます。
2. 区切り文字は寛容に受ける
入力するのは開発者ではなく本部の担当者です。半角カンマ、読点(、)、改行、半角・全角スペース。どれで区切っても同じ結果になるようにします。大文字と小文字も区別しません。「カンマが全角だったので効かなかった」という問い合わせは、受ける側で吸収しておけば発生しません。
3. 限定と除外が混ざったら限定を優先する
store-a, -store-b のように両方が書かれたセルは、解釈が割れます。ルールを1つ決めて固定します。今回は「限定の指定が1つでもあれば、限定だけを見る」にしました。迷ったときに表示範囲が狭いほうへ倒れるので、出してはいけない店に出てしまう事故になりにくいからです。
4. ハイフン入りの店舗コードを壊さない
除外記号にマイナスを使う場合、店舗コード自体にハイフンが入っていると衝突します。実際の店舗コードには「地名-駅前」のような形式が混ざっていました。除外記号として扱うのは先頭の1文字だけにして、途中のハイフンは店舗コードの一部としてそのまま読みます。日本語入力のまま打たれた全角のマイナスや長音記号も、先頭にあれば除外記号として受けます。
判定の関数は、整理すると次のような形になります。
function matchesStore(spec, storeCode) {
var raw = String(spec || '').trim();
if (!raw) return true; // 空欄=全店
var target = String(storeCode || '').trim().toLowerCase();
var only = [], deny = [];
raw.split(/[,、\n\r\t ]+/).forEach(function (t) {
var v = t.trim().toLowerCase();
if (!v) return;
if ('-−ー'.indexOf(v.charAt(0)) >= 0) deny.push(v.slice(1)); // 全角も受ける
else only.push(v);
});
if (only.length) return only.indexOf(target) >= 0; // 限定が優先
if (deny.length) return deny.indexOf(target) < 0; // 除外のみ
return true;
}
20行ほどの関数ですが、この関数だけで24通りの入力パターンをテストしています。区切り文字の種類、大文字小文字、ハイフン入りコード、限定と除外の混在、空白だけのセル。担当者が実際に打ちそうな形を先に洗い出して、全部通ることを確かめてから組み込みます。
画面で隠すだけでは足りない理由
ここが本題です。出し分けの実装というと「その店の画面に商品を表示しない」ことだけを考えがちですが、それでは半分です。
Webのフォームは、画面に表示されている項目しか送れないわけではありません。フォームが送信するのは結局のところ「商品コードと数量の組」というデータで、送り先のURLと形式が分かれば、画面を通さずに同じデータを送れます。悪意のある操作に限らず、次のような経路で対象外の注文は届きえます。
- 対象店舗を変更する前に開いたままだった画面から送信された
- 途中にキャッシュがあり、古い商品一覧の画面が表示されていた
- 画面を介さず、送信先へ直接データが送られた
つまり、表示側のフィルタは「見せない」だけで、「注文させない」保証にはなりません。注文を受け取る側(GASなら送信を処理する関数)でも、同じ判定をもう一度かけます。
実装としては、商品一覧を取得する関数に店舗コードを渡すようにして、その関数を表示と送信検証の両方から呼びます。
- 「対象店舗」の列名を定数として定義する
- 上の判定関数を追加する
- 商品一覧・商品の対応表・カテゴリ構成を返す各関数が、店舗コードを受け取って絞り込む
- 画面を作る処理と、注文を受け付ける処理の両方が店舗コードを渡す
送信側では、届いた商品コードが「その店舗用に絞り込んだ商品一覧」に存在するかを確認します。存在しなければ「取り扱いのない商品が含まれています。画面を開き直してください」と返して、注文を受け付けません。判定のロジックを1つの関数にまとめてあるので、表示と検証で基準がずれることもありません。
出し分けそのものに必要な変更は、この4点だけです。既存の38商品にも、フォームを中継している自社ドメイン側のプログラムにも、出し分けのための修正は要りませんでした。価格や配送区分も商品マスタから画面へ渡る作りにしてあったため、新商品を足すときにプログラム側の価格表を直す必要もありませんでした。データで動く作りに寄せておくと、機能を足すときの変更範囲がこのように小さく収まります。
実例:25店舗のうち12店舗にだけ商品を出す
実際に追加したのは、特定の設備を導入している店舗だけが使う消耗品です。25店舗のうち対象は12店舗。商品マスタに1行足し、「対象店舗」列に12店分の店舗コードをカンマ区切りで入れました。
反映前に確認したことは次のとおりです。
- 書いた店舗コードが店舗マスタに実在するかを12店すべて突き合わせる。打ち間違えたコードはエラーにならず、「どの店にも一致しない=その店に出ない」という形で静かに失敗します
- 店舗別フィルタの自動テスト11項目(店舗ごとの表示内容、カテゴリ構成、価格と配送区分の引き渡し、対象外店舗からの送信が弾かれること、対象店舗からの送信は通ること、列が無いシートでも全店表示になること)
- 既存機能の回帰テスト2本(セット商品の数量制御、仕入先向けCSVの生成)が変わらず通ること
反映後は、25店舗すべての画面を実際に取得して、12店で表示・13店で非表示を確認しました。対象店舗の画面は商品数163、対象外の店舗は159。画面を介さず送信先へ直接問い合わせても、対象店舗では商品が返り、対象外の店舗では返らないことを確かめています。「対象の1店で出たからOK」で終わらせず、出ない側の店も全部見るのがポイントです。出し分けの不具合は、出したい店に出ないことより、出してはいけない店に出ていることのほうが気づきにくいからです。
出し分けを入れると一緒に見直しが必要になる場所
店舗限定の商品は、たいてい既存の商品と性質が違います。表示の出し分けだけを考えていると、周辺の計算で想定外の動きが出ます。
送料や合計の判定に混ざらないか
このフォームには、配送元ごとに金額を合計し、一定額(税抜3万円)以上なら送料無料と表示する機能があります。元の実装は配送元を2種類に分ける前提で、「1つ目の仕入先でなければ、もう一方」という二択でした。
ここに3つ目の仕入先の商品を足すと、その金額が既存の配送グループに合算され、実際には送料がかかる注文が「送料無料」と表示されるおそれがあります。そこで「どちらでもない」を受ける3つ目のグループを追加し、送料判定の対象から外しました。このグループの欄は、該当商品がある店舗の画面にだけ出ます。残り13店舗の画面は変わりません。
二択で書かれた分岐は、3つ目が来た瞬間に「その他」側へ黙って流れ込みます。限定商品を足すときは、仕入先・配送・税率などで分岐している箇所を先に洗い出しておくと安全です。
発注メールの宛先
仕入先が増えれば、発注メールの送り先も増えます。今回は限定商品を含む注文が、本部宛(全量)・新しい仕入先宛(該当商品だけ)・既存の仕入先宛(その仕入先の商品だけ)の3通に分かれる構成になりました。宛先も設定シートから読む作りにして、CCの有無も行を足すだけで変えられるようにしています。メール送信の関数をテスト用に差し替え、誰に何が送られるかを実送信なしで11項目確認しました。
キャッシュの反映待ち
フォームを自社ドメイン経由で配信している場合(GASのWebアプリURLをそのまま配ってはいけない理由で書いた構成です)、中継側にキャッシュがあります。今回の構成では5分です。
キャッシュのキーに店舗コードまで含めてあれば、店舗ごとに別々に保存されるので、他店の画面が混ざることはありません。店舗限定を入れる前に、ここは必ず確認します。キーが店舗を区別していないと、出し分けそのものが成立しません。
一方で、マスタを変えてから画面に出るまで最大5分かかります。変更直後に確認して「反映されていない」と判断しがちなので、担当者には「5分待ってから確認」と先に伝えておきます。実際、確認を始めた時点で1店舗だけ古い画面が返り、数分後に切り替わりました。
反映の順序:プログラムが先、データが後
運用上の注意を1つだけ。マスタに値を書くのは、対応したプログラムをデプロイした後です。
「対象店舗」列を読まない旧版が動いている間に、限定商品の行をマスタへ入れると、旧版は列を無視して全店に表示します。止めるには有効フラグを落とせば済みますが、そもそも順序を守れば起きません。詳しくは公開前の商品をマスタに登録するときの順番にまとめています。
またGASの更新では、「新しいデプロイ」ではなく既存デプロイの「新バージョン」を選びます。前者はURLが変わり、各店舗に配ったリンクが使えなくなります(「新しいデプロイ」を押してしまったときの戻し方)。
チェックリスト
店舗別の出し分けを入れるときに確認する項目です。
- 出し分けのルールは商品マスタの列で持ち、コードに店名を書いていないか
- 空欄=全店になっていて、既存商品に書き足しが不要か
- 列が無いシートでも従来どおり動くか
- 区切り文字・大文字小文字・全角記号の揺れを受けられるか
- 限定と除外が混在したときの優先順位を決めたか
- ハイフン入りの店舗コードで誤判定しないか
- 表示だけでなく、注文を受け取る側でも同じ判定で弾いているか
- 書いた店舗コードが店舗マスタに実在するか突き合わせたか
- 送料・合計・メール宛先など、商品の属性で分岐している箇所に影響しないか
- キャッシュのキーが店舗を区別しているか、反映待ち時間を担当者に伝えたか
- 反映後、出る店と出ない店の両方を全店確認したか
まとめ
発注フォームの商品を店舗ごとに出し分けるなら、商品マスタに「対象店舗」列を1本足し、空欄=全店・列挙=限定・先頭マイナス=除外の3通りで書けるようにします。既存商品は空欄のまま動き続け、対象店舗の変更はセルの書き換えだけで完結します。
そして、絞り込みは表示と送信検証の両方に入れます。画面から消すのは「見せない」だけで、古い画面やキャッシュ経由の注文までは止められません。判定を1つの関数にまとめ、画面を作る処理と注文を受ける処理の両方から呼ぶ。これで、表示と受付の基準がずれない出し分けになります。
graciautoでは、スプレッドシートとGASを使った発注・集計の仕組みづくりから、多店舗での運用設計までお手伝いしています。「店舗が増えてフォームの管理が追いつかない」という段階でも、お気軽にご相談ください。