無人販売所のために2日で作ったセルフレジの解説

Bohfula / ボーフラ
執筆者: Bohfula / ボーフラ
無人販売所のために2日で作ったセルフレジの解説

六方画材渋谷店(無人販売所)の開設にあたり、2日間で構築したキャッシュレス・完全セルフレジシステムについて解説します。

はじめに:六方画材の無人販売所プロジェクト

六方画材は高橋の趣味から始まったセル画用品のブランドで、これまでネット通販を中心に展開してきました。 今年初め、六方画材のイベントスペースとして小さなテナントを借りることになったのですが、その一角を商品の在庫置き場にする予定だったところ、「せっかく在庫を置くなら、そのまま販売所にできないか」というアイデアが浮かびました。

実店舗の要望は以前からありましたが、店舗運営に必要な人的リソースの確保が困難でした。 そこで思いついたのが「画材の無人販売所」という形態です。野菜の無人販売所のようなノリで、セル画用品を24時間365日買える夢のような(狂気の!)場所です。

画材の無人販売所(イメージ)
画材の無人販売所(イメージ)

1. 既存セルフレジの課題と解決策

常駐スタッフが不要な無人販売所を実現するには、フルセルフなレジシステムが必要です。今回、盗難対策としてキャッシュレス決済のみに対応することを前提とします。

当初は市販のセルフレジソリューションを検討したのですが、2つの大きな課題がありました。

  1. 初期コストに加えて、月額固定費が高い
  2. 入金までのサイクルが長い

持続性を上げるには、固定費を最小限に抑えつつ、キャッシュフローを良好に保つことが重要です。 そこで着目したのが、過去のイベント出店時に導入していたSquareの決済システムです。

Squareは多様な決済手段に対応するだけでなく、月額固定費がゼロ(決済手数料のみ)で、最短翌営業日に入金される迅速な入金サイクルが魅力です。 またSquare APIを用いることでカスタムアプリケーションの作成が可能です。 既にSquare Terminalも所有していたため、これを活用することで初期コストも抑えられると判断しました。

ただし、開発にあたっては大きな制約がありました。店舗開店のための商品製造業務だけで手一杯で、セルフレジシステムの開発にはわずか2日間しか時間を割けなかったのです。

2. システム設計:セキュアで使いやすいセルフレジ

全体アーキテクチャ

システム構成図

システムは大きく分けて以下のコンポーネントで構成されています。

  • Linuxアプリケーションサーバー:ReactフロントエンドとExpressバックエンド
  • 店舗内システム:Windows 11 Pro キオスクモードのクライアント端末、周辺機器、Square Terminal
  • Squareサービス:Square API、商品マスターデータ
  • 監視・運用システム:監視カメラ、無停電電源装置、ネットワーク冗長化

利用者から見えるのはタッチ対応モニターとバーコードスキャナー、そしてSquare Terminalのみです。クライアントはWindows 11 Proのキオスクモードで動作し、メインのアプリケーションロジックは店舗外のLinuxマシン上にあります。

セキュリティーとネットワーク

ネットワークはTailscaleを使ったVPN環境下にあり、これによりクライアント端末とLinuxサーバー間の通信が保護されています。 また、すべての機器の電源は無停電電源装置に接続して落雷・停電対策を行い、ネットワークも冗長化して安定稼働を確保しています。

Tailscaleの導入により、遠隔地からのメンテナンスも容易になりました。 各端末にはローカルデータを一切保存せず、すべてSquareデータから取得する仕組みです。

ハードウェア構成

  • タッチ対応モニター
  • USBバーコードスキャナー
  • Square Terminal(決済処理・レシート印刷)
  • 監視カメラ(リアルタイム監視用)

フロントエンド実装

決済手段選択画面
決済手段選択画面

フロントエンドはReactで実装し、以下の主要な画面で構成されています。

  1. 商品スキャン画面
  2. 決済手段選択画面
  3. 決済処理中画面
  4. 決済完了画面

機械翻訳ですが多言語化も行っており、日本語、英語、フランス語、スペイン語、繁体字中国語、簡体字中国語の6言語に対応しています。 これにより、訪日外国人のお客様も安心して利用できるようになりました。

// 言語設定の例
const translations = {
  ja: {
    title: 'セルフレジシステム',
    scanTitle: '商品スキャン',
    // ...略
  },
  en: {
    title: 'Self-Checkout System',
    scanTitle: 'Product Scan',
    // ...略
  },
  // その他の言語...
};

また、バーコードスキャナーの入力を最優先で受け付けるように設計し、ユーザーが操作に迷わないインターフェースを心がけました。

3. Square API活用のポイント

Terminal APIによる決済処理

Square APIの中でも特に重要なのがTerminal APIです。これを使うことで、Square Terminalに決済処理をリクエストできます。

// Terminal チェックアウト生成
app.post("/api/create-terminal-checkout", async (req, res) => {
  try {
    const { order, amountMoney, paymentType = "CARD_PRESENT" } = req.body;

    const ALLOWED = new Set([
      "CARD_PRESENT",
      "FELICA_TRANSPORTATION_GROUP", 
      "FELICA_ID", 
      "FELICA_QUICPAY",
      "QR_CODE"
    ]);
    
    if (!ALLOWED.has(paymentType)) {
      return res.status(400).json({ error: "サポートされていない決済手段が指定された" });
    }

    // 注文を先に作成
    const orderId = await createOrder(order);

    // Square Terminal Checkout 作成
    const checkoutResponse = await squareClient.terminal.checkouts.create({
      idempotencyKey: randomUUID(),
      checkout: {
        amountMoney: {
          // 金額は BigInt 必須
          amount: BigInt(amountMoney.amount),
          currency: amountMoney.currency,
        },
        deviceOptions: {
          deviceId: SQUARE_DEVICE_ID,
          skip_receipt_screen: true,
          show_itemized_cart: false,
        },
        referenceId: orderId,
        orderId,
        note: "LOPPOセルフレジでの決済",
        paymentType: paymentType
      },
    });

    res.json(checkoutResponse);
  } catch (error) {
    handleError("Terminal Checkout 作成エラー", error, res);
  }
});

多様な決済手段

Square Terminalは多様な決済手段に対応しているため、お客様は自分の好みの方法で支払いができます。

  • クレジット・デビットカード
  • 交通系IC(Suica/PASMO等)
  • iD
  • QUICPay
  • QRコード決済(PayPay等)

なお銀聯カードには対応していません。

決済状況確認のポーリング

決済処理はSquare Terminal上で行われるため、決済完了やキャンセルといった状態をポーリングで確認する必要があります。

// 決済状況をポーリングで確認
const checkPaymentStatus = async () => {
  try {
    const statusResponse = await fetch(`/api/get-checkout-status?checkoutId=${data.checkout.id}`);
    const statusData = await statusResponse.json();
    
    if (statusData.status === 'COMPLETED') {
      setPaymentStatus(t.paymentCompleted);
      // 完了処理
      setTimeout(() => {
        setStatus('complete');
        setCart([]);
      }, 2000);
    } else if (statusData.status === 'CANCELED' || statusData.status === 'CANCEL_REQUESTED') {
      setPaymentStatus(t.paymentCanceled);
      setTimeout(() => {
        setStatus('ready');
      }, 3000);
    } else {
      // まだ完了していない場合は再確認
      setPaymentStatus(t.processing);
      setTimeout(checkPaymentStatus, 2000);
    }
  } catch (error) {
    console.error(t.statusCheckFailed, error);
    setPaymentStatus(t.statusCheckFailed);
    setTimeout(() => {
      setStatus('ready');
    }, 3000);
  }
};

商品マスターデータ管理

商品情報はすべてSquareの管理画面で登録し、APIを通じて取得しています。 これにより、商品の追加や価格変更などの運用作業を簡素化しています。

app.get("/api/catalog-items", async (_req, res) => {
  try {
    const TYPES = "ITEM,ITEM_VARIATION,CATEGORY,IMAGE"; // 必要なタイプをすべて列挙
    //------------------------------------------------------------------
    // ① すべて読み込む
    //------------------------------------------------------------------
    const objects = [];
    for await (const obj of await squareClient.catalog.list({ types: TYPES }))
      objects.push(obj);

    //------------------------------------------------------------------
    // ② CATEGORY / IMAGE / VARIATION を先にマップ化
    //------------------------------------------------------------------
    const imageMap     = {};
    const categoryMap  = {};
    const variationMap = {};

    // ...略(マップ作成処理)

    //------------------------------------------------------------------
    // ③ ITEM を展開し、先に作ったマップで情報を埋め込む
    //------------------------------------------------------------------
    const filtered = objects
      .filter((o) => o.type === "ITEM")
      .map((item) => {
        // ...略(データ変換処理)
      })
      // -- ここで要件フィルタ --
      .filter(
        (item) =>
          !item.isArchived &&
          item.categoryNames.includes("六方画材")
      );

    res.json(filtered);
  } catch (error) {
    handleError("カタログアイテム取得エラー", error, res);
  }
});

4. LLM活用による爆速開発

このプロジェクトの最大の特徴は、わずか2日間という短期間で開発を完了させたことです。 これを可能にしたのが、LLM(大規模言語モデル)の活用でした。

開発時間の内訳

  • 基本システム開発:約2時間
  • 改良・UI調整:約4時間
  • テスト・デプロイ:残りの時間

Claude 3.7 Sonnetの活用方法

開発では主にClaude 3.7 Sonnetを活用し、実装の効率化を図りました。 アプリケーションロジックのみならずUI設計も難なくこなし、セットアップドキュメントまで用意されるなど、至れり尽くせりでした。 特に多言語対応のコードや、Square APIとの連携部分はLLMの提案が非常に役立ちました。

なおChatGPT 4oやChatGPT o3なども組み合わせましたが、WEBアプリケーションに対する理解度では3.7 Sonnetにはまったく及びませんでした。

LLM活用の具体例

LLMを開発に活用する際の当たり前の注意点ではありますが、生成されたコードをそのまま使うことは難しく、必ず理解した上で必要な修正を加えることが重要です。 例えば、Square Terminal APIとの連携部分では以下のような修正が必要でした。

  1. 決済手段の追加:LLMが生成したコードではクレジットカード決済にしか対応しておらず、決済手段を選択する画面を追加する必要がありました
  2. エラーハンドリング:Terminalでの決済キャンセルイベントの扱い方が誤っており、APIドキュメントをもとに正しい処理に修正しました
  3. セキュリティー:一部で脆弱なプロトコルでアプリ間通信を行っていたため、プライベートVPNを構築し通信経路の安全性を確保しました

LLMは基本的なコード構造を提供してくれましたが、本番運用に耐えられるようにするための調整は手作業で行う必要がありました。

5. 国際化と使い勝手

多言語対応の実装

訪日外国人の方も利用できるよう、システムは日本語、英語、フランス語、スペイン語、繁体字中国語、簡体字中国語の6言語に対応しています。 Reactコンポーネント内で言語設定を管理し、画面上の全てのテキストを翻訳オブジェクトから取得する方式を採用しました。

// 言語選択の状態管理
const [language, setLanguage] = useState('ja'); // デフォルト言語を日本語に設定
// 言語設定を取得
const t = translations[language];

// 使用例
<h1 className="text-4xl font-bold">{t.title}</h1>
<p className="text-lg text-gray-700 mb-6">
  {t.scanDescription}
</p>

使い勝手の工夫

スーパーやコンビニのセルフレジと同様の使い勝手を目指し、以下の工夫を行いました。

  1. バーコードスキャン優先:ページの任意の場所でキーボード入力を受け付け、バーコードスキャナーからの入力を常に優先
  2. 大きなボタン:タッチ操作がしやすい大きなボタンサイズ
  3. 明確なフィードバック:操作結果が分かりやすいメッセージ表示

これらの工夫により、利用者が迷うことなく操作できるインターフェースを実現できたと思います。

6. 運用面での工夫

リアルタイム監視と障害対応

店内にネットワークカメラを設置し、販売所の状況をリアルタイムで確認できるようにしています。 トラブルが発生した場合、お客様が店頭に掲示された電話番号に連絡することで対応可能です。

運用上の異常が検出された場合、30分~2時間以内に現地に駆けつけて対応できる体制を整えています。 また、万が一決済端末が動かない場合は、事後に決済リンクを渡して支払いを完了していただく代替手段も用意しています。

安定稼働のため、無停電電源装置による電源バックアップ、ネットワークの冗長化、無操作時の定期的なリブートなども組んであります。

1度だけクライアントの電源ケーブルの差し込みが甘くて抜けるトラブルがありましたが、以降はシステムは非常に安定しています。

7. 実際の成果と効果

販売機会の拡大

無人販売所の開設により、都心駅近の24時間営業という貴重な販売機会を獲得しました。 実店舗の開設要望に応えつつも、人的リソースの制約を克服できたことが最大の成果です。

決済方法の傾向

導入した全ての決済方法がまんべんなく利用されていますが、特に人気なのは以下の順です。

  1. 交通系IC
  2. QRコード決済(PayPayなど)
  3. クレジットカード(タッチ決済)

8. 今後の展望と拡張計画

ネット通販への展開

現在の六方画材ECサイトはBASEで構築されていますが、これもSquare APIを用いたものに変更する予定です。 これにより、決済手数料を節約できるだけでなく、商品の購入フローを改善し、また店舗とネット販売の在庫・売上管理を一元化できるメリットがあります。

さらに、オンラインで購入して店頭で受け取る手段も提供したいと思っています。

まとめ:絵具が作れるならレジシステムだって作れる

いろいろなセル画関連画材をハンドメイドで復刻してきた六方ですが、今回はレジシステムを手作りしました。

ソフトウェアアプリケーションは、このように既存APIの活用やLLMの支援で比較的簡単に構築できる場合があります。 とはいえ、レジシステムがこうも簡単に作れるとは思ってもみなかったので、大変勉強になりました。

注力した点

  1. 既存サービスの活用:Square APIなど既存プラットフォームを最大限に活用
  2. LLMなどの支援ツールの活用:開発効率を高めるツールの積極的な導入
  3. 必要最小限の範囲に集中:必要な機能に絞ってシンプルに実装

セルフレジシステム構築に興味のある方や、Square APIの活用を検討している方の参考になれば幸いです。

今回構築したシステムの全ソースコードは、GitHub上で公開しています。 loppo-llc/loppo-register - GitHub

Bohfula / ボーフラ

Bohfula / ボーフラ

急須のような異形頭の個人ゲーム開発者。しばしば高橋に呼び出されて、六方画材の運営やら広報やらを手伝わされている