この記事は「N高グループ・N中等部・NCodeLabo Advent Calendar 2025」の19日目の記事です。
いらっしゃいませ
入店時にほぼ必ず店員から投げかけられるこの挨拶。
実は、万引き防止に一役買っているのはご存知でしょうか?
目を合わせて挨拶をすることで「自分は見られているんだ」と心理的に不安にさせ、万引きを未然に防ぐことができると言われています。
私自身も、ドラッグストアでバイトを始める際、一番最初に相手の目を見て語先後礼することを叩き込まれました。
そう言った意味では「いらっしゃいませ」はある意味店員さんの「お呪い」と言えるのではないでしょうか。
↑ かっこいい言葉を使ってみたかったんです
では逆に、店員さんはいつ万引きされたことに気が付くのでしょうか?
もちろん、お店の出入り口にある盗難防止ゲートが鳴ったり、私服警備員が犯行を現認して発覚することもあります。
が、ほとんどの場合は年に1回の棚卸しで、データ上の在庫数と実際に売り場にある商品の個数が異なることで気づく場合が多いです。
その、データ上の在庫数を管理しているのが POSシステム(販売時点情報管理システム) です。
POS(ポス)とは「Point of Sale」の略で、日本語では「販売時点情報管理」と訳されています。カンタンにいうと、お店で商品やサービスを提供したときに、「いつ」「何を」「どうやって」「いくらで」「いくつ」売ったかなどという会計処理の情報を、リアルタイムで自動集約・蓄積し、一括で管理する仕組みのことです。
引用元:AirREGI
↑ POSの説明はこちらの引用元が一番分かりやすいです。
何故作ろうとしたのか
商品の価格や在庫情報、売上や現在の天気など、その店のありとあらゆる情報を管理する全知全能の神がPOSシステム。
その情報を活用して、仕事をテキパキこなす超優秀なレジがPOSレジです。
取り扱う情報が多いだけではなく、会計機(セミセルフレジのお金払う機械)や発注などの在庫管理を行える ハンディターミナル などもPOSシステムを構築する一部で、POSシステムは「巨大なシステム」と十分胸を張って言えると思います。
人は大きいものに惹かれるわけで、開発しごたえがあり、個人開発でどこまでやれるのか挑戦したい。自分でPOSシステム(POSレジ)を作ってみたい!!
と思うようになり、行動に移るまでそう時間はかかりませんでした。
この記事では、レジの仕組みや、開発の過程で出会った問題、気づいたことを切り抜いてご紹介します。
POSシステムのレシピ
今回必要となる材料はこちら

作成:レシピ本ジェネレーター
一つづつ解説していきます。
POSサーバー
POSシステムにおいて、最もコアな部分です。
全ての情報を集約、管理しています。
POSレジ
みなさんが一番身近でお世話になるPOSシステムの要素です。
手打ちせずともバーコードを ピッ するだけで価格が出たり、他のレジ(会計機など)に取引情報を送信したりすることができます。
ハンドスキャナ
バーコードを ピッ ってするあれです。
これがないと、バーコードを全て手打ちする羽目になります。
今回はスマホのカメラ機能で代用しました。
レジのキーボード
普通のキーボードとは違い、レジ操作に特化した配置や形になっています。
普通に買おうとすると1万円を超えるため、安いキーボードを塗装してそれっぽくすることにしました。
NFCリーダー
NFCタグが入ったカードの情報を読み取る機器です。
こちらも高いため、スマホで代用しようと考えました。
が、iPhoneはネイティブアプリからしかNFCの機能を使用することができず、今回はAndroidスマホにPWAのWebアプリを入れて、代用することにしました。
NFCカードは、デザインを指定して購入できる所もあり、余裕があれば独自の電子決済や、ポイントカードを作ってみたいと思っていました。
レシートプリンター
そのまま、レシートを印刷するためのプリンターです。
こちらも買うと高くつくため、小学3年生の誕生日プレゼントとして買ってもらったEPSONの普通のインクジェットプリンターで代用します。
※ 本当です。どうしても自分用のプリンターが欲しかったんです。
印刷には Epson が提供している Epson Connect メールプリント を使用します。
Epson Connect から割り当てられるメールアドレスに、レシートのPDFを添付して送信するだけで、プリンターが自動で印刷してくれる神サービスです。
ハンディーターミナル
POSサーバーとリアルタイムで通信し、在庫情報の確認や発注、見切り(割引)シールの発行などを行える、二次元コードのスキャナーが搭載された端末です。
店員さんがPOP(値札)を腰掛けたスマホで ピッピ している様子を見たことがあるかもしれません。あれです。
こちらも普通に買うと20万オーバーであるため、スマホで代用します。
これをまとめると
このようになります。
ここに乗っている物を全て作って紹介したかったのですが、都合によりPOSレジしか形にできていません。
なので、以降POSレジを主題として進めていきます。
なお、通常であればPOSサーバーとPOSレジは独立しているのですが、今回はレジが1台だけであり、一緒にした方が都合が良いため、合体しているものとして作ります。
初めて尽くしの技術選定
POSレジの開発を始める前まで、PHPを裸でぶん回していたのですが、POSシステムには リアルタイム通信 が必要となってくるため、これを機に新しい技術に挑戦しようと考えました。
ですが、NextとNestとNuxtの区別すら付かない無知な状態だったので、チャッピーにありとあらゆる相談を投げつけました。

画像:Next.jsとNestJSとNuxt.jsの違いを聞く人
度重なる相談の結果、大規模向けで拡張性が高い下記のような技術を用いて開発することにしました。
- 開発言語:TypeScript*
-
フロントエンド
- Next.js*
- TailwindCSS*
- howler.js*
- レジのSE再生(ピッ)
- React-pdf*
- レシート生成
-
バックエンド
- NestJS*
- Nodemailer*
- レシート送信用
- bwip-js*
- バーコード生成用
- Prisma ORM*
- データベース:PostgreSQL*
- コード品質
- ESLint*
- Prettier*
- パッケージ管理:pnpm*
- 環境構築:Docker Compose*
※「*」 = 初めまして
全部初めましてやないかい
全て手探りの状態だったので、制作と並走して下記の書籍を読んで勉強しました。
I ♡ TailwindCSS
Next.js の理解が深まり、次に TailwindCSS でレジの画面の構築を始めました。
POSレジに必要な画面は非常に多岐に渡ります。
全て作るのはあまりにも大変なので、今回は必要最低限の画面だけを作ることにしました。
- 売上
- 返品
- レジマイナス(取引取消)
こんな感じです。
謎の言葉「レジマイナス」
ここで、聞きなれないワードが出てきましたので、説明します。
レジマイナス とは売上(商品を売った)取引を 無かったことにする 操作のことです。
「先生!返品じゃダメなんですか?」
いい質問です。
返品では売上取引を残しつつ、別で返品取引を記録します。
これが取引を無かったことにするレジマイナスと返品の違いです。
使い分けの例として、
- 商品を間違えて多くスキャンした
- → レジマイナス
- お客様がサイズを間違えた
- → 返品
のようなものが挙げられます。
また、豆知識として売上データは精算(取引金額をまとめ、POSサーバーに送信する)業務を行うとレジ本体からデータが綺麗さっぱり無くなるため、翌日などにレジマイナスを行うことはできません。
※ 機種によって異なります
話を戻します。
元々CSSは少し触っていたのですが、Tailwindが非常に楽で、もうCSSを普通に書く生活には戻れないなと思いました。
Tailwindは魔性のフレームワークです。
画面を作るにあたり、脳裏にこびり着いて離れないバイト先のレジ画面を参考にしました。
また、自宅にPOSレジを所有されている方の動画からも、UI/UXのアイディアをいただきました。
↑ ご自宅POSレジ動画
立ちはだかる消費税
バックエンドを書くため、Prisma でデータベースの構築を始めました。
そこで問題となったのが「税金の取り扱い」です。
消費税率は主に
- 10%
- 8%
- 0%(非課税)
があります。
これらが消費税免税であったり、イートイン/テイクアウト、消費税の改正などで変動することを考慮して、処理を実装するのは正直面倒です。
結局、消費税が存在しない国で使用されることを想定することで、この問題を解決しました。
ファミチキ、チキン抜き
すいません!ファミチキのチキン抜きで
(株)ファミリーマートですね。2兆円です。
という、インターネット上のネタがあります。
これをレジを作っている際に思い出し、このレジで (株)ファミリーマート を購入できるようにしたいと思いました。
そこで発生したのが、次の二つの問題です
1. 額が大き過ぎて、Int型がオーバーフローする
当時のPrismaでは商品単価と合計金額の値にInt型を使用していました。
model Product {
code String @id // JANコード
name String // 商品名
price Int // 価格
items SaleItem[]
@@map("product")
}
model Sale {
...
totalDiscount Int // 合計点数
pieces Int // 合計額
totalAmount Int // 合計割引額
custody Int // お預かり金額
change Int // お釣り
...
}
しかし、Int型の最大値は
2^{31} - 1 = 2,147,483,647
であり、(株)ファミリーマート を購入するともれなく オーバーフロー と言う刺客に刺されるという事が判明。
2. ファミリーマートが上場廃止していた
またなんと、2020年11月12日づけで、ファミリーマートが上場廃止となっていたのです。
解決法
問題1関しては仕方がないため、String型を使用するよう変更
※ 当時、BigInt型やDecimal型の概念を知りませんでした。許してください
model Product {
code String @id // JANコード
name String // 商品名
price String // 価格 <int → string>
items SaleItem[]
@@map("product")
}
model Sale {
...
totalDiscount Int // 合計点数
pieces String // 合計額 <int → string>
totalAmount String // 合計割引額 <int → string>
custody String // お預かり金額 <int → string>
change String // お釣り <int → string>
...
}
問題2は廃止直前の時価総額を使用することで、ファミリーマートを購入できるようにしました。
立ちはだかるレシートの印紙税
購入時に発行されるレシート。
実は、領収書として効力を有する正式な私文書なんです。
そのため、レシートには「領収書」の文字が刻まれていますし、別で領収書を必要とする場合は、レシートの原本を回収し、領収書とレシートのコピーをお渡ししています。
そして、5万円以上の領収書には印紙税がかかります。
もちろん、印紙税を払わなければ脱税。3倍になってあなたに襲い掛かります。
バレなきゃ犯罪じゃないは通用しません。
ファミチキのチキン抜きは印紙税20万
実際に適用されるかは分かりませんが、もし仮に私がファミチキのチキン抜きを購入したレシートを印刷した場合、取られる印紙税は20万円。
汗水垂らして貯めた貯金が全て吹っ飛ぶことになります。
それは流石にまずいので
- 「領収書」の文字を「お買い上げ明細」に変更
- 明細書が領収書でないことを明確に示す
の二つの工夫を行い、脱税を免れました。
※実際の税務上の取り扱いはケースによって異なります
レシートの生成
ちなみに、レシートの生成は TSX でデザインと金額等の情報を入れ、それを React-pdf でPDF化して出力→メールでプリンターに送信→自動印刷しています。
例えば、
のような購入品の情報を印刷したい場合、
{data.body.merchandise.map((item: any, i: number) => (
<View key={i} style={{ marginTop: 20 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text style={{ fontSize: 30, letterSpacing: -1 }}>
{item.name}
</Text>
<Text style={{ fontSize: 30, textAlign: 'right' }}>
¥{(item.price * Number(item.qty)).toLocaleString()}
</Text>
</View>
{Number(item.qty) > 1 ? (
<Text style={{ fontSize: 28 }}> 単{Number(item.price).toLocaleString()}円 x {item.qty}個</Text>
) : ("") }
{item.discount != 0 ? (
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text style={{ fontSize: 28 }}> 割引 {item.discount}%</Text>
<Text style={{ fontSize: 30, textAlign: 'right' }}>-{(((item.price - item.price * (1 - item.discount / 100))) * Number(item.qty)).toLocaleString()}</Text>
</View>
) : ("") }
</View>
))}
↑ any は気にしないでください
このような HTML に似た JSX を PDFに変換し、家のプリンターにメールを送信します。
↑ こんなこともできちゃいます
その他機能
その他機能として、
-
売価変更
- 売値を変える(100円 → 10,000円)操作
-
値引(割合)
- 1〜99%の間で商品を値引きする操作
-
直前取消
- 直前にスキャンした商品を削除する操作
-
取引中止
- 商品やレシート番号などをクリアする操作
たまに悪用される
-
ポイント
- 顧客番号を入れることでそのお客様の情報(年代、性別など)を取得&任意の倍率でポイントを付与する
などの機能を搭載しました。
MVP完成
そして、開発を始めてから1ヶ月強で無事にPOSレジ(MVP)を作る事ができました!!
現計(現金で決済) + レシート発行
電子決済 + レシート発行(音量注意)
返金(音量注意)
学んだこと
POSシステムはすごい
税金がない国での使用を想定したり、any型を多様するなど、幾つもの 「妥協」 を重ねて開発してきました。
が、もちろん本物のシステムでそんなことをすれば焼き土下座は避けられません。
POSシステムに関わらず、高品質かつクライアントを意識した開発を行うエンジニアの方々が尊敬でしかないし、私もいつか焼き土下座を回避できるエンジニアになりたいと強く感じました。
「自分のため」は技術習得が早いし楽しい
これまで「他の人のため」に開発する事が多く、品質やクライアントの目線に立たないといけない重圧の中の開発で、心が折れそうになったことが度々ありました。
が、今回は初めて「自分による自分のための個人開発」を行い重圧もなく、開発だけに没頭し続ける事ができました。
TypeScript初めましてが1ヶ月強の速さで形にできたのは「自分のため」の開発だった事が非常に大きいと感じています。
↑ anyのおかげでもありますが
作るなら「変態」と呼ばれるものの方が楽しい
2連続楽しい系です。
創作を行う際、作るものは「しっかりとした物」と「誰が使うねん。な物」に分けられると思います。
この際、「誰が使うねん。な物を作る」場合は人を困惑させれる物 = 変態と言われそうな物を作った方が楽しいし、絶対良いと今回気が付きました。
誰もやっていないことってすごく魅力に感じますよね。
他に誰もやっていないので比べようが無く、個性も抜群。故に変態。
もし普通の創作に飽きているのなら、変態と呼ばれうる物を作ってみるのも、私は大いにありだと思います。
終わりに
初めてのブログ&技術記事ということで、以前制作した自作POSレジを題材にしてみました。
今回この記事を書く中で、新しい学びや、発見が多くあったので、今後も執筆を続けていきたいと思っています。もし、気になっていただけましたらフォローして気長にお待ちいただけますと嬉しいです (● ˃̶͈̀ロ˂̶͈́)੭ꠥ⁾⁾
最後までお付き合いいただき、本当にありがとうございました。
またどこかで!
P.S.
開発したての頃、自作POSレジをやってる人は自分くらいだろう、と思っていました。
が、なんと同志が結構いらっしゃられて非常に驚きました。
皆さんも 自作POSレジ いかがでしょうか?
P.P.S.
バイト先に近づくと集中モードがONになるように設定していたのですが、この記事を執筆している最中、集中モードが勝手にONになり続けて怖かったです。
これも店員のお呪い(おまじない)でしょうか?
免責事項
実際の業務用POSとは仕様が異なったり、簡略化されている点が多くあります。
あれ、これ違うぞ?と感じられたり、もっと良い実装や考え方があれば、ぜひコメントでご教示いただけますと嬉しいです。
参考文献
- 万引き犯人の視点から見た効果的な防犯対策(日本チェーンドラッグストア協会)
- 万引き防止マニュアル 〜万引きされにくい店舗を目指すために〜(大阪府警)
- POSとは?POSシステムの仕組みや導入メリットを徹底解説(AirREGI)










