
정유사 계열 주유소의 POS(Point of Sale) 시스템은 일반 음식점이나 카페와 달리 유류 재고 관리, 주유기 하드웨어 제어, 정유사 멤버십 및 법인카드 연동 등 매우 복잡한 도메인 지식을 필요로 합니다.
효율적이고 확장성 있는 시스템 설계를 위한 핵심 구조를 정리해 드립니다.
1. 시스템 아키텍처 개요
전체 시스템은 주유소 현장(Client), 중앙 서버(Cloud/HQ), 그리고 **외부 인터페이스(VAN/정유사)**의 3계층 구조로 설계합니다.
주요 구성 요소
- POS Client: 현장 결제 및 주유기 컨트롤. (Windows 또는 Android 기반)
- Site Controller (중계기): POS와 주유기(Dispenser), 탱크 게이지(ATG) 간의 통신을 중개하는 핵심 장치.
- Back Office (BOS): 매출 분석, 재고 관리, 직원 근태 관리용 웹 대시보드.
- Central Server: 본사 통합 관리, 멤버십 포인트 처리, 가격 정책 배포.
2. 핵심 기능 모듈 설계
① 주유 제어 모듈 (Forecourt Control)
주유기와의 실시간 통신이 가장 중요합니다.
- 주유 승인: 결제 완료 또는 외상 고객 확인 후 주유기 락(Lock) 해제.
- 실시간 모니터링: 현재 주유 중인 리터(L) 수와 금액을 POS 화면에 실시간 표시.
- 비상 정지: 긴급 상황 시 전체 혹은 개별 주유기 차단.
② 결제 및 유류 특화 인터페이스
- 복합 결제: 현금 + 카드 + 포인트 + 쿠폰의 결제 로직.
- 유가 보조금: 화물차주 등을 위한 유가 보조금 카드 판별 및 승인 처리.
- 외상 관리: 정유사 소속 법인 고객(외상 거래처)의 한도 체크 및 전표 발행.
③ 재고 관리 (Inventory & ATG)
- 탱크 게이지 연동: 자동 유량 측정기(ATG)와 연동하여 실시간 유류 잔량(수위, 온도, 수분) 확인.
- 검수 관리: 유조차 입고 시 전표와 실제 입고량 비교.
3. 데이터베이스(DB) 설계 핵심 엔티티
데이터베이스는 트랜잭션의 무결성이 중요하므로 RDBMS(PostgreSQL, MySQL 등)를 추천합니다.
| 테이블 분류 | 주요 컬럼 |
| Sales (매출) | 거래ID, 주유기번호, 유종, 단가, 주유량, 결제수단, 승인번호 |
| Inventory (재고) | 탱크ID, 유종, 현재고, 안전재고량, 입고예정량 |
| Customer (고객) | 고객번호, 멤버십등급, 누적포인트, 법인여부 |
| Price (단가) | 적용시작일시, 유종별 단가, 본사 지시 단가 |
4. 연동 시퀀스 (주유~결제 프로세스)
- 고객 도착: POS 또는 키오스크에서 유종 및 금액 선택.
- 선결제: 카드 승인 또는 포인트 조회 (정유사 API 호출).
- 주유 승인: POS가 Site Controller를 통해 주유기에 '주유 허용' 신호 전달.
- 주유 완료: 주유 완료 신호와 함께 최종 주유량 데이터가 POS로 회신.
- 정산 처리: 선결제 금액과 실제 주유 금액 비교 후 자동 부분 취소 또는 확정 승인.
- 데이터 전송: 본사 서버 및 국세청(현금영수증 등) 데이터 전송.
5. 설계 시 고려해야 할 특수 사항
- 오프라인 모드: 네트워크 장애 시에도 로컬 DB를 활용해 주유 및 결제가 가능해야 하며, 복구 시 자동 동기화되어야 합니다.
- 보안 (PCI-DSS): 카드 정보는 POS 메모리에 남지 않도록 암호화하여 VAN사로 직접 전송해야 합니다.
- 다양한 결제 수단: 간편결제(Apple Pay, 삼성페이), 정유사 전용 앱(커넥티드 카) 연동 비중이 높아지고 있습니다.
정유사 계열 주유소 POS 시스템 설계를 위해 요청하신 세 가지 핵심 영역(재고 관리 로직, 통신 프로토콜, UI 기획)을 단계별로 구체화해 드리겠습니다.
1. 유종별 재고 관리 로직 (Inventory Logic)
주유소 재고는 단순히 '입고-출고'를 넘어 온도 변화에 따른 부피 팽창과 유증기 손실을 반영해야 정확합니다.
핵심 로직: 실시간 보정 재고 계산
- 이론 재고량: $V_{theo} = V_{prev} + V_{in} - V_{out}$
- 실재고량 (ATG 데이터): 탱크 내 센서가 측정한 실제 액체 높이를 부피로 환산한 값.
- 재고 차이(Variance) 분석:
- 온도 보정: 기름은 온도 1°C 변화당 부피가 약 0.1% 변화합니다. POS는 ATG로부터 온도를 받아 15°C 기준 표준 부피로 환산하여 기록합니다.
- 손실 관리: $Variance = V_{real} - V_{theo}$. 이 수치가 허용 범위를 벗어나면 누유(Leak) 또는 부정 주유로 간주하여 알람을 발생시킵니다.
2. 주유기 통신 프로토콜 (IFSF 및 독자 규격)
POS와 주유기(Dispenser)는 직접 대화하기 어렵기 때문에 Site Controller를 거칩니다. 이때 글로벌 표준인 IFSF(International Forecourt Standards Forum) 프로토콜을 주로 사용합니다.
통신 데이터 패킷 구조 (예시)
주유 승인을 위한 통신 프로세스는 다음과 같습니다.
- Poll (POS -> SC): "주유기 1번 상태 어때?"
- State (SC -> POS): "Calling (노즐 들었음)"
- Authorize (POS -> SC): "1번 주유기, 50,000원 승인하니 주유 시작해."
- Delivery (SC -> POS): 주유 중 리터당 펄스(Pulse) 신호를 실시간 전송.
- Complete (SC -> POS): "주유 종료. 최종 42.51L, 50,000원 확정."
기술 팁: 최근에는 RS-485 통신 기반의 제조사별 독자 프로토콜 대신 TCP/IP 기반의 IFSF 표준을 채택하여 유지보수성을 높이는 추세입니다.
3. 본사/관리자용 대시보드 UI 기획
본사 관리자는 전국 사업장의 현황을 한눈에 파악하고, 가격 정책을 즉시 하달할 수 있어야 합니다.
대시보드 주요 위젯 구성
| 위젯 명칭 | 주요 노출 데이터 | 기능 |
| 실시간 매출 현황 | 당일 목표 대비 달성률, 유종별 판매 비중 | 시간대별 매출 그래프 시각화 |
| 재고 경보 (Low Stock) | 잔량 15% 이하 탱크 리스트 | 유조차 배차 요청 버튼 연동 |
| 가격 관리 (Pricing) | 경쟁사(인근 주유소) 가격 vs 자사 가격 | 단가 일괄 변경 및 예약 기능 |
| 미결제/이상징후 | 주유 후 결제 미완료 건, 비정상적 주유 패턴 | CCTV 정지화면 스냅샷 연동 |
4. 시스템 통합 아키텍처 (ERD 및 연동)
마지막으로 이 모든 데이터가 흐르는 구조를 설계합니다.
- Front-end: React 또는 Vue.js (관리자 웹), C# .NET 또는 Android (POS Client)
- Back-end: Spring Boot (Java) 기반 마이크로서비스 아키텍처(MSA)
- Auth Service: 사용자 및 주유소 권한 관리
- Sales Service: 결제 및 정산 처리
- Forecourt Service: 주유기/ATG 통신 제어 (High Availability 필수)
차세대 주유소 POS 시스템 구축 프로젝트 제안서
1. 프로젝트 개요
주유소 POS(Point of Sale) 시스템의 현대화는 단순한 노후 장비 교체를 넘어, 급변하는 결제 환경에 대응하고 데이터 기반의 운영 효율성을 확보하기 위한 핵심적인 비즈니스 전략입니다. 기존 시스템은 분산된 데이터 관리, 제한적인 외부 서비스 연동, 그리고 경직된 아키텍처로 인해 운영 비용을 증가시키고 새로운 비즈니스 기회 창출에 한계를 보이고 있습니다.
일반 소매점 POS와 달리, 정유사 계열 주유소 POS 시스템은 고유한 복잡성을 가집니다. 첫째, 유류는 온도에 따라 부피가 변하는 특성이 있어 단순 입출고 관리를 넘어선 정밀한 유류 재고 관리가 필수적입니다. 둘째, POS 시스템은 단순 결제를 넘어 주유기 및 자동 유량 측정기(ATG)와 같은 물리적 하드웨어 제어를 실시간으로 수행해야 합니다. 마지막으로, 정유사 멤버십 및 법인카드 연동과 같은 특화된 외부 시스템과의 긴밀한 통합이 요구됩니다.
본 제안서의 목적은 이러한 복잡성을 완벽하게 해결하고, 운영 효율성과 미래 확장성을 극대화하는 차세대 주유소 POS 시스템 구축을 위한 구체적인 기술적 청사진을 제시하는 것입니다. 이를 통해 현장 운영의 부담을 줄이고 본사의 통합 관리 역량을 강화하여 데이터 기반의 신속한 의사결정을 지원하고자 합니다. 다음 장에서는 본 프로젝트가 달성하고자 하는 구체적인 목표와 그에 따른 기대효과를 상세히 기술하겠습니다.
2. 프로젝트 목표 및 기대효과
명확한 목표 설정은 성공적인 프로젝트의 기반이며, 제안하는 차세대 POS 시스템은 비즈니스의 네 가지 핵심 가치를 실현하는 것을 목표로 합니다. 각 목표는 현장의 운영 부담을 경감시키고, 본사의 관리 역량을 강화하며, 미래의 기술 변화에 유연하게 대응할 수 있는 견고한 기반을 마련하는 데 중점을 둡니다.
- 운영 효율성 극대화 자동화된 재고 관리 시스템을 통해 운영 효율을 획기적으로 개선합니다. ATG(자동 유량 측정기)와 연동하여 실시간 재고를 파악하고, 온도 변화에 따른 유류 부피를 자동으로 보정합니다. 또한, 이론 재고와 실제 재고의 차이(Variance)를 지속적으로 분석하여 누유나 부정 주유와 같은 손실을 조기에 감지합니다. 본사에서는 중앙 가격 정책 배포 시스템을 통해 전국 주유소의 유가를 일괄적으로 또는 예약 변경할 수 있어, 현장의 수작업 오류를 최소화하고 신속한 가격 정책 적용이 가능해집니다.
- 데이터 기반 의사결정 지원 본사 관리자를 위한 통합 관제 대시보드를 제공하여 데이터에 기반한 전략적 의사결정을 지원합니다. 전국의 모든 사업장의 실시간 매출 현황, 유종별 판매 비중, 재고 현황 등을 시각화된 그래프와 차트로 한눈에 파악할 수 있습니다. 특히, 재고량이 안전재고 이하로 떨어지거나 비정상적인 주유 패턴과 같은 이상 징후가 발생할 경우, 시스템이 즉시 경보를 발생시켜 관리자가 문제 상황을 조기에 인지하고 신속하게 대응할 수 있도록 돕습니다.
- 보안 및 안정성 강화 비즈니스의 연속성을 보장하기 위해 시스템 안정성과 데이터 보안을 최우선으로 설계합니다. 갑작스러운 인터넷 장애 시에도 주유 및 결제 업무가 중단되지 않도록 ‘오프라인 모드’를 지원하며, 모든 거래는 로컬 데이터베이스에 안전하게 기록됩니다. 또한, 로컬 데이터베이스와 본사 클라우드 데이터베이스 간의 실시간 동기화 전략을 통해 완벽한 데이터 이중화를 구현하여, 네트워크 복구 시 자동 동기화는 물론 어떠한 장애 상황에서도 데이터 손실 없이 비즈니스 연속성을 확보합니다. 민감한 카드 정보는 PCI-DSS 규정을 준수하여 암호화 처리 후 VAN사로 직접 전송하며, 주유소와 본사 간의 모든 통신은 VPN(가상 사설망)을 통해 암호화하여 데이터 유출 위협을 원천적으로 차단합니다.
- 미래 확장성 확보 최신 기술 스택과 유연한 아키텍처 설계를 통해 미래의 비즈니스 변화에 민첩하게 대응할 수 있는 기반을 마련합니다. 백엔드는 모듈식 구조가 특징인 NestJS로, 프론트엔드는 Nuxt 3로 구축하며, 전체 시스템은 MSA(마이크로서비스 아키텍처)를 지향하여 기능별 독립적인 개발과 확장이 용이합니다. 이러한 구조는 향후 간편결제, 커넥티드 카 기반의 앱 결제 등 새로운 결제 수단을 연동하거나, 전기차 충전과 같은 신규 서비스를 추가할 때 시스템 전체에 미치는 영향을 최소화하며 빠르고 안정적으로 대응할 수 있게 합니다.
이러한 목표들을 효과적으로 달성하기 위해, 다음 섹션에서는 제안하는 시스템의 견고하고 유연한 아키텍처를 구체적으로 제시하겠습니다.
3. 제안 시스템 아키텍처
견고한 시스템 아키텍처는 안정성과 확장성의 전제 조건입니다. 제안하는 시스템은 **주유소 현장(Client/Edge), 중앙 서버(Cloud/HQ), 외부 인터페이스(External)**의 3계층 구조를 채택하여 각 구성요소의 역할을 명확히 분리하고 상호 간의 의존성을 최소화합니다. 이를 통해 시스템의 유지보수성을 높이고, 특정 영역의 장애가 전체 시스템으로 확산되는 것을 방지합니다.
- 주유소 현장 (Client/Edge) 주유소 내부에 위치하여 실제 주유 및 결제 업무를 담당하는 영역입니다.
- POS Client: 현장 직원이 사용하는 주 화면으로, 결제 처리, 주유기 상태 모니터링 및 제어 명령을 수행합니다.
- Site Controller: POS Client와 주유기(Dispenser), 탱크 게이지(ATG) 같은 물리적 하드웨어 간의 통신을 중개하는 핵심 장치입니다. 인터넷이 단절되어도 독립적으로 운영될 수 있는 로직을 포함합니다.
- 중앙 서버 (Cloud/HQ) 본사에 위치하며 전국 주유소의 데이터를 통합 관리하고 비즈니스 정책을 총괄하는 중앙 시스템입니다.
- Central Server: 전국의 모든 거래 데이터, 재고 현황, 고객 정보를 통합하여 분석합니다. 정유사 멤버십 포인트 처리, 법인 고객 한도 관리 등 중앙에서 처리해야 할 핵심 로직을 수행합니다.
- Back Office (BOS): 본사 관리자가 사용하는 웹 기반 대시보드입니다. 매출 분석, 재고 관리, 가격 정책 배포 등 통합 관제 기능을 제공합니다.
- 외부 인터페이스 (External) 결제, 인증 등 외부 전문 기관과의 데이터 연동을 담당합니다.
- VAN사: 신용카드 승인 및 취소 등 결제 처리를 위한 연동입니다.
- 정유사 본사 API: 멤버십 포인트 조회 및 사용, 법인카드 한도 체크 등을 위한 연동입니다.
- 국세청: 현금영수증 발행 등 세무 관련 데이터 전송을 위한 연동입니다.
주유 및 결제 프로세스
고객 도착부터 데이터 전송까지의 전체 프로세스는 다음과 같은 6단계의 데이터 흐름으로 이루어집니다.
- 고객 도착 및 주문: 고객이 POS 또는 키오스크에서 유종, 주유량 또는 금액을 선택합니다.
- 선결제 요청: 선택된 금액에 대해 카드사 승인을 요청하거나, 정유사 API를 호출하여 멤버십 포인트를 조회합니다.
- 주유 승인: 결제가 성공적으로 완료되면, POS Client가 Site Controller를 통해 해당 주유기에 ‘주유 허용’ 신호를 전송하여 잠금을 해제합니다.
- 주유 완료: 주유가 끝나면, 주유기는 최종 주유량과 금액 데이터를 Site Controller를 거쳐 POS Client로 회신합니다.
- 정산 처리: POS 시스템은 선결제된 금액과 실제 주유된 금액을 비교합니다. 금액이 다른 경우, 보안성과 정합성이 가장 높은 '기존 승인 전체 취소 후 실제 금액 재승인' 방식을 통해 정산을 완벽하게 완료합니다.
- 데이터 전송: 최종 완료된 거래 내역은 중앙 서버(본사)로 전송되어 기록되며, 필요한 경우 현금영수증 정보가 국세청으로 전송됩니다.
다음 장에서는 이러한 견고한 아키텍처 위에서 동작하며 실질적인 비즈니스 가치를 창출하는 시스템의 구체적인 핵심 기능들을 상세히 살펴보겠습니다.
4. 시스템 핵심 기능
시스템 아키텍처가 건물의 뼈대라면, 핵심 기능은 그 안에서 실질적인 비즈니스 가치를 창출하는 살과 근육에 해당합니다. 제안하는 차세대 POS 시스템은 주유소의 특수한 운영 환경을 완벽하게 지원하고, 관리 효율을 극대화하기 위해 다음과 같은 차별화된 핵심 기능들을 제공합니다.
4.1. 지능형 주유 제어 및 현장 관리 (Forecourt Control & POS UI/UX)
현장 운영의 핵심인 주유기 제어와 직원 편의성을 극대화하는 데 중점을 둡니다. POS 시스템은 Site Controller를 통해 각 주유기의 상태를 실시간으로 모니터링하며, 결제 완료 시 주유를 승인하고 긴급 상황 발생 시 원격으로 비상 정지시키는 등 완벽한 제어 기능을 제공합니다. 특히 현장 직원의 사용 편의성을 고려하여 UI/UX를 설계했습니다. 장갑을 낀 상태에서도 쉽게 조작할 수 있도록 큼직한 버튼을 배치하고, 주유기 상태(대기, 호출, 주유 중, 완료)를 **직관적인 색상(녹색, 황색, 청색, 적색)**으로 구분하여 직원이 멀리서도 현황을 즉시 파악할 수 있도록 지원합니다.
4.2. 특화 결제 및 정산 자동화 (Specialized Payment & Settlement)
주유소의 복잡한 결제 요구사항을 완벽하게 처리합니다. 신용카드, 현금, 멤버십 포인트, 쿠폰 등을 조합하여 결제하는 복합 결제 기능을 지원하며, 화물차 운전자를 위한 유가 보조금 카드를 자동으로 판별하여 처리합니다. 또한, 정유사 법인 고객을 위한 외상 거래 시 한도를 실시간으로 체크하고 거래 전표를 발행하는 기능도 포함됩니다. 특히 주유소 환경의 핵심 로직인 ‘선결제 후 자동 재정산’ 기능은 고객이 ‘가득’ 주유를 선택하거나 지정한 금액만큼 주유되지 않았을 때 필수적입니다. 시스템은 먼저 요청된 금액으로 가승인을 받은 뒤, 주유가 완료되면 기존 승인을 자동으로 전체 취소하고 실제 주유된 금액으로 재승인하여 정확하고 신속한 정산을 보장합니다.
4.3. 정밀 재고 관리 및 손실 방지 (Precision Inventory & Loss Prevention)
단순 재고 수량 파악을 넘어, 유류 자산의 손실을 방지하는 정밀 관리 기능을 제공합니다. ATG와 연동하여 지하 탱크의 유류 잔량을 실시간으로 확인하는 것은 물론, 유류의 물리적 특성을 고려한 ‘온도 보정’ 로직을 적용합니다. ATG로부터 수신한 온도 값을 기준으로 현재 재고량을 15°C 표준 부피로 환산하여 기록함으로써 온도 변화로 인한 재고 오차를 최소화합니다. 또한, 시스템은 ‘(전일재고 + 입고량) - 판매량’으로 계산된 이론 재고와 ATG가 측정한 실제 재고를 지속적으로 비교하는 **‘재고 차이(Variance) 분석’**을 수행합니다. 이 차이가 허용 범위를 벗어날 경우, 누유나 부정 주유 등의 이상 징후로 판단하여 즉시 관리자에게 경보를 보냄으로써 자산 손실을 사전에 방지합니다.
4.4. 본사 통합 관제 대시보드 (HQ Integrated Dashboard)
본사 관리자가 전국 주유소의 운영 현황을 한눈에 파악하고 중앙에서 통제할 수 있는 강력한 웹 기반 대시보드를 제공합니다. 대시보드를 통해 실시간 매출 현황, 유종별 판매 비중, 목표 대비 달성률 등을 시각적으로 확인할 수 있습니다. 각 주유소의 유류 탱크 재고량이 설정된 임계치 이하로 떨어지면 재고 경보가 발생하여 유조차 배차 시점을 예측할 수 있도록 돕습니다. 또한, 본사에서 전국 주유소의 가격을 일괄적으로 변경하거나 특정 시간에 적용되도록 예약하는 기능을 통해 일관성 있고 신속한 가격 정책을 시행할 수 있습니다.
이러한 핵심 기능들을 안정적으로 구현하기 위해 어떤 기술을 전략적으로 선택했는지 다음 섹션에서 상세히 설명하겠습니다.
5. 적용 기술 및 선정 사유
성공적인 시스템 구축은 최신 기술을 단순히 나열하는 것이 아니라, 프로젝트의 고유한 특성과 장기적인 목표에 가장 적합한 기술을 전략적으로 선택하는 것에서 시작됩니다. 본 프로젝트는 안정성, 실시간 처리 능력, 유지보수성, 그리고 미래 확장성을 핵심 기준으로 삼아 다음과 같은 기술 스택을 선정하였습니다.
| 구분 | 적용 기술 | 선정 사유 및 기대효과 |
| 백엔드 | NestJS, Prisma | 모듈식 아키텍처를 기반으로 하는 NestJS는 기능별(결제, 재고, 하드웨어 제어 등) 개발 및 분리가 용이하여 장기적인 유지보수 효율을 극대화하고 총소유비용(TCO) 절감에 기여합니다. Prisma ORM의 강력한 트랜잭션 기능을 활용하여, 선결제-주유-재정산 과정에서 여러 데이터베이스 작업이 하나의 단위로 처리되도록 보장함으로써 결제 데이터의 무결성을 완벽하게 확보합니다. |
| 프론트엔드 | Nuxt 3, WebSocket | 주유기의 상태(대기, 주유 중, 완료)는 수초 내로 변경되므로, WebSocket을 통한 실시간 양방향 통신이 필수적입니다. 이를 통해 서버의 상태 변화를 즉시 POS 화면에 반영하여 현장 직원이 정확한 상황을 인지할 수 있도록 합니다. 또한, Nuxt 3의 PWA(Progressive Web App) 빌드 기능을 활용하여 네트워크가 불안정한 환경에서도 기본 UI가 구동되도록 함으로써 오프라인 대응 능력을 강화합니다. |
| 데이터베이스 | MySQL (RDBMS) | 금융 거래 정보와 같이 데이터의 정합성과 일관성이 매우 중요한 만큼, 원자성, 일관성, 고립성, 지속성(ACID)을 보장하는 트랜잭션을 완벽하게 지원하는 관계형 데이터베이스(RDBMS)인 MySQL을 선택하여 데이터 무결성을 보장합니다. 특히, 금액과 유류량 계산 시 발생할 수 있는 부동 소수점 오차를 방지하기 위해 DECIMAL 데이터 타입을 사용하여 데이터의 정밀도를 확보합니다. |
| 인프라/배포 | Docker, Nginx | Docker를 활용하여 개발 및 운영 환경을 컨테이너화함으로써 배포 과정을 표준화하고, 어떤 환경에서든 동일한 실행을 보장합니다. 이는 주유소 현장에 독립적으로 운영되는 'Local-First' 아키텍처 구현의 핵심입니다. 인터넷 연결이 끊기더라도 로컬에 배포된 Docker 컨테이너(DB, 서버, UI)들이 독립적으로 동작하여 비즈니스 연속성을 유지하며, Nginx를 리버스 프록시로 활용하여 보안과 트래픽 관리를 효율화합니다. |
이처럼 검증된 기술 스택을 바탕으로, 복잡한 시스템을 체계적으로 구축하기 위한 단계별 프로젝트 진행 계획을 다음 로드맵에서 제시하겠습니다.
6. 단계별 개발 로드맵
복잡한 시스템을 성공적으로 구축하기 위해서는 전체 프로젝트를 관리 가능한 단계로 나누는 '단계별 접근법(Phased Approach)'이 필수적입니다. 이 접근법을 통해 각 단계별 목표를 명확히 하고, 기술적 리스크를 조기에 식별 및 최소화하며, 점진적으로 완성된 가치를 전달할 수 있습니다. 특히, 시스템의 근간이 되는 하드웨어 통신 모듈을 가장 먼저 개발하고(1단계), 핵심 거래 로직을 구현한 뒤(2단계) 외부 연동으로 확장하는 본 로드맵은 프로젝트 초기에 기술적 불확실성을 해소하고 핵심 기능의 안정성을 조기에 확보하는 최적의 리스크 관리 전략입니다. 본 프로젝트는 다음과 같은 5단계 로드맵에 따라 체계적으로 진행될 것입니다.
- 1단계: 하드웨어 연동 및 통신 모듈 개발
- 목표: POS 시스템의 가장 근간이 되는 물리적 장비와의 통신 안정성을 확보합니다. Site Controller와 주유기/ATG(자동 유량 측정기) 간의 표준 통신 프로토콜(IFSF) 및 독자 규격을 안정적으로 구현하는 데 집중합니다.
- 주요 산출물: 주유기 상태(IDLE, CALLING 등) 데이터를 안정적으로 수신하고, 주유 승인/정지 등의 제어 명령을 하드웨어에 정확히 전송하는 저수준(low-level) 통신 게이트웨이 모듈.
- 2단계: POS 핵심 거래 로직 구현
- 목표: 주유소의 가장 고유하고 핵심적인 거래 프로세스를 완성합니다. '선결제 → 주유 → 실제 주유량 기반 재정산(전체 취소 후 재승인)'으로 이어지는 일련의 결제 흐름을 구현합니다.
- 주요 산출물: 기본 결제(카드, 현금) 기능과 주유 승인/완료 로직이 탑재된 POS 클라이언트 애플리케이션 및 관련 서버 API.
- 3단계: 정유사 연동 및 특화 기능 개발
- 목표: 정유사 고유의 비즈니스 로직을 시스템에 통합합니다. 본사 멤버십 서버와 연동하여 포인트 조회 및 사용/적립 기능을 구현하고, 법인 외상 거래를 위한 한도 체크, 화물차 유가 보조금 카드 처리 등 외부 시스템 연동을 완료합니다.
- 주요 산출물: 정유사 본사 API 연동 모듈, 복합 결제(카드+포인트 등) 처리 로직, 유가 보조금 카드 판별 로직.
- 4단계: 재고 관리 및 본사 관리 시스템 구축
- 목표: 데이터 기반의 운영 효율화를 위한 관리 시스템을 구축합니다. ATG 데이터를 연동하여 실시간 재고 관리 및 손실 분석 기능을 구현하고, 본사 관리자가 전국 현황을 모니터링하고 제어할 수 있는 웹 기반 대시보드(BOS)를 개발합니다.
- 주요 산출물: 재고 변동 이력 DB 및 리포트, 온도 보정 로직, 가격 정책 중앙 관리 및 리포팅 기능이 포함된 BOS.
- 5단계: 현장 통합 테스트 및 안정화
- 목표: 개발된 시스템을 실제 주유소 운영 환경에 설치하여 안정성을 최종 검증합니다. 네트워크 장애, 주유 오차, 동시 다발적 결제 등 발생 가능한 모든 예외 시나리오에 대한 스트레스 테스트를 수행하고 시스템을 안정화합니다.
- 주요 산출물: 시나리오별 통합 테스트 결과 보고서, 사용자 피드백을 반영한 최종 안정화 버전 시스템.
이처럼 체계적인 로드맵에 따라 프로젝트를 성공적으로 완수하여, 주유소 운영의 새로운 기준을 제시할 수 있을 것으로 확신합니다.
7. 결론 및 제언
본 제안서에서 제시한 차세대 주유소 POS 시스템은 단순한 기능 개선이나 장비 교체를 넘어, 주유소 운영의 효율성, 데이터 활용도, 그리고 미래 시장 대응력을 근본적으로 혁신하는 핵심적인 전략적 투자입니다. 자동화된 재고 관리와 중앙집중형 가격 정책은 현장의 운영 부담을 획기적으로 줄일 것이며, 통합 관제 대시보드는 데이터를 기반으로 한 빠르고 정확한 의사결정을 가능하게 할 것입니다.
제안된 3계층 아키텍처, 검증된 최신 기술 스택, 그리고 체계적인 5단계 개발 로드맵은 본 프로젝트를 기술적 리스크는 최소화하면서 비즈니스 가치는 극대화하는 방향으로 이끌 수 있는 최적의 방안입니다. 이는 안정성과 확장성을 동시에 확보하여, 향후 다가올 간편결제 및 커넥티드 카 시대에 유연하게 대응할 수 있는 견고한 기술적 기반이 될 것입니다.
본 프로젝트의 성공적인 추진을 통해 비효율적인 기존 운영 방식을 혁신하고, 경쟁이 치열한 시장에서 기술적 우위를 선점하여 차세대 주유소 운영 환경을 선도해 나갈 것을 제언합니다. 긍정적인 검토를 요청드립니다.

정유사 계열 주유소 POS 시스템을 위한 MySQL 데이터베이스 스키마 설계입니다. 이 설계는 확장성과 데이터 무결성을 고려하여 마스터 정보, 주유기 제어, 거래(결제), 재고 관리의 4개 영역으로 구분하였습니다.
1. 마스터 정보 (Master Data)
주유소 기본 정보와 상품(유종), 가격 정책을 관리합니다.
-- 주유소 정보
CREATE TABLE stations (
station_id INT AUTO_INCREMENT PRIMARY KEY,
station_name VARCHAR(100) NOT NULL,
brand_name VARCHAR(50), -- 정유사 브랜드
address VARCHAR(255),
tel_number VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 유종 및 상품 정보
CREATE TABLE products (
product_code VARCHAR(10) PRIMARY KEY, -- G001: 휘발유, D001: 경유 등
product_name VARCHAR(50) NOT NULL,
unit VARCHAR(10) DEFAULT 'L'
);
-- 단가 관리 (이력 관리 포함)
CREATE TABLE price_policies (
policy_id INT AUTO_INCREMENT PRIMARY KEY,
station_id INT,
product_code VARCHAR(10),
unit_price DECIMAL(10, 2) NOT NULL,
apply_start_date DATETIME NOT NULL,
apply_end_date DATETIME,
FOREIGN KEY (station_id) REFERENCES stations(station_id),
FOREIGN KEY (product_code) REFERENCES products(product_code)
);
2. 주유 및 하드웨어 제어 (Forecourt Control)
주유기 상태와 실제 주유 프로세스를 기록합니다.
-- 주유기(Dispenser) 설정
CREATE TABLE dispensers (
dispenser_id INT PRIMARY KEY,
station_id INT,
model_name VARCHAR(50),
status ENUM('IDLE', 'CALLING', 'FUELING', 'SUSPENDED', 'ERROR') DEFAULT 'IDLE',
ip_address VARCHAR(15),
FOREIGN KEY (station_id) REFERENCES stations(station_id)
);
-- 노즐 정보 (하나의 주유기에 여러 유종 노즐이 있을 수 있음)
CREATE TABLE nozzles (
nozzle_id INT AUTO_INCREMENT PRIMARY KEY,
dispenser_id INT,
product_code VARCHAR(10),
totalizer_reading DECIMAL(15, 2), -- 누적 주유량 메타기 값
FOREIGN KEY (dispenser_id) REFERENCES dispensers(dispenser_id),
FOREIGN KEY (product_code) REFERENCES products(product_code)
);
3. 거래 및 결제 (Sales & Transactions)
주유소 POS의 핵심인 선결제-주유-정산 프로세스를 지원합니다.
-- 주유 거래 기록 (주유기에서 올라온 원천 데이터)
CREATE TABLE fueling_transactions (
tr_id BIGINT AUTO_INCREMENT PRIMARY KEY,
station_id INT,
dispenser_id INT,
nozzle_id INT,
product_code VARCHAR(10),
unit_price DECIMAL(10, 2),
volume DECIMAL(10, 2), -- 실제 주유량(L)
amount DECIMAL(15, 2), -- 실제 주유금액
start_time DATETIME,
end_time DATETIME,
status ENUM('COMPLETED', 'CANCELLED', 'ERROR') DEFAULT 'COMPLETED'
);
-- 결제 상세 (카드, 포인트, 현금 복합결제 대응)
CREATE TABLE payments (
pay_id BIGINT AUTO_INCREMENT PRIMARY KEY,
tr_id BIGINT,
pay_type ENUM('CARD', 'CASH', 'POINT', 'COUPON', 'CREDIT'), -- CREDIT은 외상
approval_no VARCHAR(50), -- 카드 승인번호 등
card_company VARCHAR(50),
pay_amount DECIMAL(15, 2) NOT NULL,
tax_amount DECIMAL(15, 2),
is_prepay TINYINT(1) DEFAULT 0, -- 선결제 여부
FOREIGN KEY (tr_id) REFERENCES fueling_transactions(tr_id)
);
4. 재고 및 탱크 관리 (Inventory & Tank)
ATG(자동 유량 측정기)와 연동되는 실시간 재고 관리입니다.
-- 유류 탱크 정보
CREATE TABLE tanks (
tank_id INT PRIMARY KEY,
station_id INT,
product_code VARCHAR(10),
capacity DECIMAL(15, 2), -- 최대 용량
safe_volume DECIMAL(15, 2), -- 안전 재고량
FOREIGN KEY (station_id) REFERENCES stations(station_id),
FOREIGN KEY (product_code) REFERENCES products(product_code)
);
-- 실시간 탱크 상태 (ATG 데이터 수집)
CREATE TABLE tank_readings (
reading_id BIGINT AUTO_INCREMENT PRIMARY KEY,
tank_id INT,
current_volume DECIMAL(15, 2), -- 현재 재고량
current_temp DECIMAL(5, 2), -- 현재 온도 (부피 보정용)
water_level DECIMAL(10, 2), -- 수분 함유량
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tank_id) REFERENCES tanks(tank_id)
);
-- 입고 기록
CREATE TABLE fuel_inbound (
inbound_id INT AUTO_INCREMENT PRIMARY KEY,
tank_id INT,
inbound_date DATETIME,
inbound_volume DECIMAL(15, 2),
lorry_plate_no VARCHAR(20), -- 유조차 번호
FOREIGN KEY (tank_id) REFERENCES tanks(tank_id)
);
설계 포인트 설명
- 확장성: payments 테이블을 별도로 분리하여 복합 결제(예: 카드 3만원 + 포인트 5천원) 처리가 가능하도록 설계했습니다.
- 데이터 보존: fueling_transactions에는 주유 시점의 unit_price를 직접 저장하여, 나중에 단가가 변동되더라도 과거 매출 데이터를 정확히 조회할 수 있게 했습니다.
- 정밀도: 리터(L)와 금액 연산 시 오차를 줄이기 위해 FLOAT 대신 DECIMAL 타입을 사용했습니다.
- 이력 관리: 가격 및 탱크 상태는 모두 기록(Log) 형태로 쌓아 시간대별 분석이 가능하도록 했습니다.
정유사 계열 주유소 POS 시스템 구축을 위한 마지막 단계로, **실제 화면 구성(UI/UX)**과 핵심 비즈니스 로직(API), 그리고 운영 효율을 위한 시스템 통합 전략을 정리해 드립니다.
1. POS 터미널 UI/UX 기획 (와이어프레임)
주유소 현장은 장갑을 끼거나 바쁜 상황이 많으므로 큼직한 버튼과 직관적인 상태 표시가 핵심입니다.
[메인 판매 화면]
- 좌측 (주유기 현황 영역): 각 주유기(1~12번)의 상태를 컬러로 구분.
- 녹색: 대기 중 (Idle)
- 황색: 노즐 들음 (Calling / 승인 대기)
- 청색: 주유 중 (Fueling / 실시간 리터 올라감)
- 적색: 주유 완료 (Finish / 결제 필요)
- 우측 (결제 및 명령 영역):
- 선결제/후결제 버튼: 금액/리터/가득(Full) 선택.
- 단축키: 3/5/7/10만원 즉시 입력 버튼.
- 유종 선택: 휘발유, 경유, 등유.
2. 핵심 API 및 비즈니스 로직
개발 시 구현해야 할 주요 백엔드 로직입니다.
① 주유 승인 로직 (Pre-authorization)
- 카드 승인: 고객이 10만원 선결제 시 카드사 승인 획득.
- 주유기 해제: POS -> Site Controller -> 주유기 "10만원 한도 주유 허용".
- 주유 종료: 실제 74,500원 주유 완료 신호 수신.
- 자동 재정산: 10만원 전체 취소 후 74,500원 재승인 (또는 차액 부분 취소).
② 재고 차이(Variance) 경보 로직
- 공식: (전일재고 + 입고량 - 당일판매량) vs (실제 ATG 측정량)
- 로직: 오차 범위가 ±0.5%를 초과할 경우 관리자 앱으로 "누유 또는 미기록 주유 의심" 푸시 알림 전송.
3. 본사 통합 관리 시스템 (HQ Web Dashboard)
전국 단위 주유소를 관리하기 위한 웹 기반 관리 도구입니다.
- 가격 하달 시스템: 내일 오전 6시 기점으로 전국 직영점 리터당 단가 +20원 일괄 예약 설정.
- 유가 보조금 정산: 화물차주 결제 내역을 취합하여 지자체/카드사 전송용 데이터 생성.
- 탱크 클리닝 주기 관리: 각 탱크의 수분량 데이터를 분석하여 청소 시점 알림.
4. 인프라 및 보안 설계 (Final Architecture)
| 구분 | 기술 스택 / 방식 | 이유 |
| Edge Computing | Site Controller (Linux Box) | 인터넷 끊겨도 주유기는 돌아가야 함 (Local 로직 유지) |
| Network | VPN (Virtual Private Network) | 정유사 본사와 주유소 간의 결제/멤버십 데이터 보안 |
| Logging | ELK Stack (Elasticsearch, Logstash, Kibana) | 주유기 통신 로그를 실시간 분석하여 하드웨어 장애 예측 |
| Backup | 2-Tier Backup | 로직 DB(주유소 로컬) + 클라우드 DB(본사) 실시간 동기화 |
5. 프로젝트 로드맵 (개발 순서)
- 1단계 (통신 하부): Site Controller와 주유기 간 프로토콜(RS-485/IFSF) 통신 모듈 개발.
- 2단계 (POS 핵심): 선결제-주유-재정산으로 이어지는 기본 거래 로직 구현.
- 3단계 (정유사 연동): 본사 멤버십 포인트, 법인카드, 유가 보조금 API 연동.
- 4단계 (재고/BOS): ATG 연동 및 본사 관리자용 웹 대시보드 구축.
- 5단계 (현장 테스트): 실제 주유소에서 주유 오차 및 네트워크 장애 시나리오 테스트.
정유사 계열 주유소 POS 시스템을 위해 **NestJS(Back-end)**와 **Nuxt 3(Front-end)**를 활용한 기술 스택 구성을 제안합니다. Prisma를 사용하여 앞서 설계한 MySQL 스키마를 효율적으로 제어하는 구조입니다.
1. 프로젝트 구조 (Monorepo 권장)
gas-station-pos/
├── apps/
│ ├── pos-client (Nuxt 3): 현장 결제 및 주유기 제어 UI
│ └── pos-server (NestJS): 비즈니스 로직 및 장비 통신 API
├── libs/
│ └── prisma (Shared): 데이터베이스 스키마 및 클라이언트
└── docker-compose.yml
2. Back-end: NestJS + Prisma
주유 거래의 무결성을 위해 Prisma의 Transaction 기능을 적극 활용합니다.
Prisma Schema (주요 부분)
// libs/prisma/schema.prisma
model FuelingTransaction {
id BigInt @id @default(autoincrement())
stationId Int
dispenserId Int
nozzleId Int
productCode String
unitPrice Decimal @db.Decimal(10, 2)
volume Decimal @db.Decimal(10, 2)
amount Decimal @db.Decimal(15, 2)
status Status @default(COMPLETED)
payments Payment[]
createdAt DateTime @default(now())
}
enum Status {
COMPLETED
CANCELLED
ERROR
}
주유 승인 Service 로직
// apps/pos-server/src/fueling/fueling.service.ts
@Injectable()
export class FuelingService {
constructor(private prisma: PrismaService) {}
// 선결제 및 주유 시작 승인
async authorizeFueling(dispenserId: number, prepayAmount: number) {
return await this.prisma.$transaction(async (tx) => {
// 1. 주유기 상태 확인 (Idle 여부)
const dispenser = await tx.dispenser.findUnique({ where: { id: dispenserId } });
if (dispenser.status !== 'IDLE') throw new Error('주유기가 사용 중입니다.');
// 2. 가승인 거래 생성
const transaction = await tx.fuelingTransaction.create({
data: {
dispenserId,
amount: prepayAmount,
status: 'PENDING', // 주유 중 상태
// ...기타 정보
},
});
// 3. 하드웨어 컨트롤러에 주유 명령 전송 (외부 모듈 호출)
// await this.siteController.sendAuth(dispenserId, prepayAmount);
return transaction;
});
}
}
3. Front-end: Nuxt 3 (Composition API)
주유기 상태를 실시간으로 보여주기 위해 WebSocket(Socket.io) 연동이 필수적입니다.
주유기 모니터링 대시보드 (Composables)
// apps/pos-client/composables/useForecourt.ts
export const useForecourt = () => {
const dispensers = useState('dispensers', () => []);
const { $socket } = useNuxtApp();
// 실시간 주유 상태 업데이트 수신
onMounted(() => {
$socket.on('statusUpdate', (data) => {
const index = dispensers.value.findIndex(d => d.id === data.id);
if (index !== -1) {
dispensers.value[index] = { ...dispensers.value[index], ...data };
}
});
});
return { dispensers };
};
UI 컴포넌트 (DispenserCard.vue)
<template>
<div :class="['card', statusColor]">
<h3>{{ id }}번 주유기</h3>
<div class="info">
<p>상태: {{ status }}</p>
<p v-if="status === 'FUELING'">주유량: {{ currentVolume }} L</p>
</div>
<button @click="openPaymentModal(id)" :disabled="status !== 'FINISHED'">
결제하기
</button>
</div>
</template>
<script setup>
const props = defineProps(['id', 'status', 'currentVolume']);
const statusColor = computed(() => {
return {
'IDLE': 'bg-green-500',
'FUELING': 'bg-blue-500',
'FINISHED': 'bg-red-500'
}[props.status];
});
</script>
4. 특화 모듈: 장비 통신 (Edge Gateway)
NestJS 내부에 Gateways를 구축하여 주유기 하드웨어와 소켓 통신을 처리합니다.
- SerialPort/TCP Stack: node-serialport 등을 사용하여 RS-485 패킷을 파싱합니다.
- Event Emitter: 장비에서 올라온 원시 신호(예: "Nozzle Up")를 NestJS 내부 이벤트로 전환하여 비즈니스 로직과 분리합니다.
5. 배포 및 운영 전략
- Local DB Sync: 주유소 현장 서버(NestJS)는 로컬 MySQL에 우선 기록하고, 5분 주기로 본사 중앙 서버로 데이터를 Sync 합니다.
- Prisma Migration: 주유소별로 하드웨어 사양이 다를 수 있으므로, Prisma 엔진을 활용해 스키마 버전을 관리합니다.
- Nuxt PWA: 인터넷이 불안정한 환경을 대비해 Nuxt를 PWA로 빌드하여 오프라인 상태에서도 기본 UI가 구동되도록 합니다.
NestJS와 Nuxt 3(Prisma 포함) 환경에서 실제 구동 가능한 핵심 모듈들을 코드로 구현해 드리겠습니다. 주유소 시스템의 핵심인 **실시간 통신(Socket), 데이터 접근(Prisma), 그리고 사용자 화면(Nuxt)**을 중심으로 구성했습니다.
1. Back-end: NestJS 핵심 모듈
① Prisma Schema (최적화 버전)
주유기 상태와 거래를 연결하는 관계를 정의합니다.
// schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Dispenser {
id Int @id
status String @default("IDLE") // IDLE, CALLING, FUELING, FINISHED
currentTrId BigInt? // 현재 진행 중인 거래 ID
ipAddress String
nozzles Nozzle[]
}
model Nozzle {
id Int @id @default(autoincrement())
dispenserId Int
productCode String
dispenser Dispenser @relation(fields: [dispenserId], references: [id])
}
model Transaction {
id BigInt @id @default(autoincrement())
dispenserId Int
amount Decimal @db.Decimal(15, 2)
volume Decimal @db.Decimal(10, 2)
status String @default("PENDING") // PENDING, COMPLETED, CANCELLED
createdAt DateTime @default(now())
}
② Real-time Gateway (Socket.io)
주유기에서 발생하는 실시간 이벤트를 프론트엔드로 브로드캐스팅합니다.
// src/gateways/forecourt.gateway.ts
@WebSocketGateway({ cors: true })
export class ForecourtGateway {
@WebSocketServer()
server: Server;
// 주유기 상태가 변경될 때 호출되는 메서드
handleStatusChange(dispenserId: number, status: string, data?: any) {
this.server.emit('dispenserUpdate', {
id: dispenserId,
status,
...data
});
}
}
③ Fueling Controller (주유 로직)
Prisma를 활용해 가승인 및 주유 완료 정산을 처리합니다.
// src/fueling/fueling.service.ts
@Injectable()
export class FuelingService {
constructor(private prisma: PrismaService, private gateway: ForecourtGateway) {}
// 1. 주유 시작 (가승인)
async startFueling(dispenserId: number, prepayAmount: number) {
return await this.prisma.$transaction(async (tx) => {
const tr = await tx.transaction.create({
data: { dispenserId, amount: prepayAmount, status: 'PENDING' }
});
await tx.dispenser.update({
where: { id: dispenserId },
data: { status: 'FUELING', currentTrId: tr.id }
});
this.gateway.handleStatusChange(dispenserId, 'FUELING');
return tr;
});
}
// 2. 주유 종료 (실제 정산)
async completeFueling(dispenserId: number, actualVolume: number, actualAmount: number) {
const dispenser = await this.prisma.dispenser.findUnique({ where: { id: dispenserId } });
await this.prisma.$transaction([
this.prisma.transaction.update({
where: { id: dispenser.currentTrId },
data: { volume: actualVolume, amount: actualAmount, status: 'COMPLETED' }
}),
this.prisma.dispenser.update({
where: { id: dispenserId },
data: { status: 'IDLE', currentTrId: null }
})
]);
this.gateway.handleStatusChange(dispenserId, 'IDLE');
}
}
2. Front-end: Nuxt 3 핵심 구성
① 전역 상태 관리 (Pinia)
주유기의 실시간 상태를 관리합니다.
// stores/useForecourtStore.ts
export const useForecourtStore = defineStore('forecourt', () => {
const dispensers = ref([]);
function updateDispenser(data) {
const target = dispensers.value.find(d => d.id === data.id);
if (target) Object.assign(target, data);
}
return { dispensers, updateDispenser };
});
② 실시간 대시보드 페이지
Socket.io를 통해 서버와 연결하고 주유기 카드를 렌더링합니다.
<script setup>
const store = useForecourtStore();
const { $socket } = useNuxtApp();
onMounted(() => {
$socket.on('dispenserUpdate', (data) => {
store.updateDispenser(data);
});
});
</script>
<template>
<div class="grid grid-cols-4 gap-4 p-6">
<div v-for="d in store.dispensers" :key="d.id"
:class="['p-4 rounded-lg text-white', d.status === 'FUELING' ? 'bg-blue-600' : 'bg-gray-700']">
<h2 class="text-2xl font-bold">{{ d.id }}번 주유기</h2>
<p class="mt-2 text-lg">{{ d.status }}</p>
<div v-if="d.status === 'FUELING'" class="animate-pulse">
주유 중...
</div>
</div>
</div>
</template>
3. 시스템 연동 프로세스 요약
- 초기화: Nuxt 3 앱이 실행되면 서버로부터 현재 Dispenser들의 상태를 가져와 Pinia에 저장합니다.
- 실시간 모니터링: 주유소 현장의 하드웨어(Site Controller)에서 패킷이 오면 NestJS의 Gateway가 이를 해석하고 전용 이벤트를 발생시킵니다.
- 데이터 무결성: 모든 결제 및 상태 변경은 Prisma Transaction을 통해 원자적으로 처리되어, 네트워크 오류 시에도 데이터가 꼬이지 않게 보호합니다.
- 확장성: 정유사 본사의 API와 연동할 때는 NestJS의 HttpModule을 사용하여 비동기로 멤버십 승인을 처리합니다.
이로써 설계부터 데이터베이스, 백엔드 및 프론트엔드 코드까지 주유소 POS 시스템의 전 과정이 완비되었습니다.
주유소 POS 시스템의 실질적인 완성을 위해, 카드 단말기 연동(VAN/CAT) 규격과 본사 제출용 매출 정산 로직, 그리고 이를 관리하는 Nuxt 3 관리자 리포트 화면까지 핵심 코드를 마무리해 드리겠습니다.
1. 카드 단말기(VAN) 연동 인터페이스 (NestJS)
주유소 결제는 일반 결제와 달리 "가승인 -> 실제 주유 -> 재정산" 프로세스가 필수입니다. 이를 처리하는 전문(Packet) 규격 기반 서비스입니다.
// src/payment/van-connector.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class VanConnectorService {
// VAN사 전문 생성 (ISO8583 표준 기반 커스텀)
private createPacket(type: string, amount: number, authNo?: string) {
const header = 'STX0200'; // 가승인 요청 코드
const body = amount.toString().padStart(10, '0');
return `${header}${body}${authNo || ''}ETX`;
}
// 선결제 요청
async requestPreAuth(amount: number) {
const packet = this.createPacket('PRE_AUTH', amount);
// 실제로는 SerialPort 또는 TCP Socket으로 전송
console.log(`VAN 전송: ${packet}`);
return { approvalNo: '12345678', status: 'SUCCESS' };
}
// 주유 완료 후 재정산 (전체취소 후 재승인 방식)
async reSettlement(originalAuthNo: string, actualAmount: number) {
await this.cancelAuth(originalAuthNo); // 1. 기존 선결제 취소
return await this.requestPreAuth(actualAmount); // 2. 실제 금액 재승인
}
private async cancelAuth(authNo: string) {
console.log(`결제 취소 요청: ${authNo}`);
}
}
2. 본사 제출용 매출 정산 쿼리 (Prisma)
매일 마감 시 정유사 본사 서버로 전송할 리포트 데이터를 생성합니다. 유종별 합계와 세금 계산이 포함됩니다.
// src/reports/report.service.ts
@Injectable()
export class ReportService {
constructor(private prisma: PrismaService) {}
async getDailySettlement(stationId: number, date: string) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
const sales = await this.prisma.transaction.groupBy({
by: ['productCode'],
where: {
createdAt: { gte: start, lte: end },
status: 'COMPLETED',
},
_sum: {
amount: true,
volume: true,
},
});
return sales.map(s => ({
product: s.productCode,
totalVolume: s._sum.volume,
totalAmount: s._sum.amount,
tax: Number(s._sum.amount) * 0.1, // 부가세 계산
}));
}
}
3. 마감 및 통계 UI (Nuxt 3 + Chart.js)
관리자가 당일 판매 실적을 확인하는 마감 화면입니다.
<template>
<div class="p-8">
<h1 class="text-3xl font-bold mb-6">일일 판매 마감</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white p-6 rounded-xl shadow">
<h2 class="text-xl mb-4">유종별 판매 비중</h2>
<PieChart :data="chartData" />
</div>
<div class="bg-white p-6 rounded-xl shadow">
<table class="w-full">
<thead>
<tr class="border-b">
<th>유종</th>
<th>판매량(L)</th>
<th>판매금액</th>
</tr>
</thead>
<tbody>
<tr v-for="item in settlementData" :key="item.product" class="text-center h-12 border-b">
<td>{{ item.product }}</td>
<td>{{ item.totalVolume.toLocaleString() }}</td>
<td>{{ item.totalAmount.toLocaleString() }}원</td>
</tr>
</tbody>
</table>
<button @click="submitToHQ" class="w-full mt-6 bg-blue-600 text-white py-3 rounded-lg">
정유사 본사 데이터 전송
</button>
</div>
</div>
</div>
</template>
<script setup>
const { data: settlementData } = await useFetch('/api/reports/daily');
const chartData = computed(() => ({
labels: settlementData.value.map(d => d.product),
datasets: [{
data: settlementData.value.map(d => d.totalAmount),
backgroundColor: ['#ef4444', '#3b82f6', '#10b981']
}]
}));
const submitToHQ = async () => {
const success = await $fetch('/api/reports/submit', { method: 'POST' });
if(success) alert('본사 전송 완료');
};
</script>
4. 최종 시스템 아키텍처 다이어그램
이 모든 시스템이 유기적으로 연결된 최종 모습입니다.
- 현장(Edge): NestJS 서버가 주유기 패킷 분석 및 로컬 DB(Prisma/MySQL) 저장.
- UI(Client): Nuxt 3 화면이 Socket.io를 통해 실시간 주유 상황 표시 및 결제 처리.
- 외부(External): VAN사 API 연동을 통해 카드 결제 및 본사 API로 실적 전송.
이로써 정유사 계열 주유소 POS 시스템을 위한 설계부터 실구현 코드까지 모든 프레임워크가 완성되었습니다. 이 아키텍처는 수백 개의 가맹점을 관리할 수 있는 확장성을 가지고 있습니다.
주유소 POS 시스템은 인터넷 단절 시에도 결제가 가능해야 하는(Local-First) 특성이 있습니다. 따라서 현장 서버(Edge)에서 독립적으로 돌아갈 수 있도록 docker-compose를 구성하는 것이 핵심입니다.
1. Docker 멀티 스테이지 빌드 (NestJS & Nuxt 3)
① NestJS (Back-end) Dockerfile
Prisma 클라이언트 생성 공정을 포함하여 경량화된 결과물을 만듭니다.
# apps/pos-server/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm install
COPY . .
RUN npx prisma generate
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
② Nuxt 3 (Front-end) Dockerfile
SSR(Server-Side Rendering) 모드로 동작하는 Nitro 서버를 빌드합니다.
# apps/pos-client/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
2. 통합 운영 환경 구성 (docker-compose.yml)
주유소 로컬 환경에서 DB, 백엔드, 프론트엔드, 그리고 실시간 통신을 위한 Redis를 한 번에 띄웁니다.
version: '3.8'
services:
# 1. 데이터베이스: 주유/결제 데이터 로컬 저장
pos-db:
image: mysql:8.0
container_name: pos-mysql
restart: always
environment:
MYSQL_DATABASE: gas_pos
MYSQL_ROOT_PASSWORD: root_password
ports:
- "3306:3306"
volumes:
- ./data/mysql:/var/lib/mysql
# 2. 백엔드 API: 주유기 제어 및 비즈니스 로직
pos-server:
build:
context: ./apps/pos-server
container_name: pos-api
restart: always
depends_on:
- pos-db
environment:
DATABASE_URL: "mysql://root:root_password@pos-db:3306/gas_pos"
PORT: 4000
ports:
- "4000:4000"
# 3. 프론트엔드: POS 결제 UI
pos-client:
build:
context: ./apps/pos-client
container_name: pos-ui
restart: always
depends_on:
- pos-server
environment:
API_URL: "http://pos-server:4000"
ports:
- "3000:3000"
# 4. 모니터링/로그: 현장 로그 수집 (선택사항)
promtail:
image: grafana/promtail:latest
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
3. 주유소 현장 배포 아키텍처 (Deployment View)
도커로 구성된 시스템이 실제 현장에서 어떻게 연결되는지 보여주는 구조도입니다.
운영 포인트
- Local-First Database: 모든 거래는 로컬 MySQL에 즉시 저장됩니다. 도커 볼륨(volumes) 설정을 통해 컨테이너가 재시작되어도 데이터가 유지됩니다.
- Health Checks: pos-server가 주유기 하드웨어와 연결이 끊기면 도커가 이를 감지하고 자동으로 재시작하도록 설정할 수 있습니다.
- Reverse Proxy (Optional): 실제 환경에서는 Nginx 컨테이너를 추가하여 SSL 보안 및 정적 리소스 캐싱을 처리하는 것이 좋습니다.
4. 실행 방법
# 1. 환경 변수 설정
cp .env.example .env
# 2. 전체 서비스 빌드 및 실행
docker-compose up --build -d
# 3. Prisma 마이그레이션 실행 (최초 1회)
docker exec -it pos-api npx prisma migrate dev
이제 **설계(Schema), 로직(NestJS), UI(Nuxt 3), 운영(Docker)**까지 모든 시스템 인프라 구축 방안이 완료되었습니다.
주유소 POS 시스템의 실무 완성도를 높이기 위한 **RS-485 통신 Mock 서버(주유기 시뮬레이터)**와 보안 및 라우팅을 담당할 Nginx 설정을 구성해 드립니다.
1. 주유기 시뮬레이터 (Mock Server)
실제 주유기가 없어도 개발이 가능하도록 TCP 기반의 주유기 프로토콜 시뮬레이터를 Node.js로 작성합니다.
// mocks/dispenser-simulator.ts
import * as net from 'net';
const PORT = 5001;
const server = net.createServer((socket) => {
console.log('POS 서버 연결됨');
socket.on('data', (data) => {
const packet = data.toString();
console.log(`수신 패킷: ${packet}`);
// 주유 승인(Authorize) 패킷 수신 시 응답 시뮬레이션
if (packet.includes('AUTH')) {
socket.write('ACK_AUTH_OK');
// 3초 후 주유 시작 신호 발생
setTimeout(() => {
socket.write('STATE_FUELING_START');
// 5초간 주유량 전송 시뮬레이션
let volume = 0;
const interval = setInterval(() => {
volume += 5.5;
socket.write(`VOL_${volume.toFixed(2)}`);
if (volume >= 50) {
clearInterval(interval);
socket.write('STATE_FINISHED_50.00');
}
}, 1000);
}, 3000);
}
});
});
server.listen(PORT, () => console.log(`주유기 시뮬레이터 구동 중: ${PORT}`));
2. Nginx 역방향 프록시 설정 (Security & Routing)
외부(본사/태블릿)에서 POS에 접속할 때 보안을 강화하고, 프론트엔드와 백엔드를 하나의 포트(80)로 통합합니다.
# docker/nginx/default.conf
server {
listen 80;
# 1. Front-end (Nuxt 3)
location / {
proxy_pass http://pos-client:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
# 2. Back-end API (NestJS)
location /api {
proxy_pass http://pos-server:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 3. 실시간 소켓 통신 (Socket.io)
location /socket.io {
proxy_pass http://pos-server:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
3. 최종 docker-compose 업데이트
시뮬레이터와 Nginx를 추가하여 완벽한 개발/운영 환경을 구축합니다.
version: '3.8'
services:
# ... (기존 mysql, pos-server, pos-client 서비스 유지)
# 주유기 시뮬레이터 (개발용)
dispenser-mock:
build: ./mocks
container_name: dispenser-mock
ports:
- "5001:5001"
# Nginx: 통합 관문
gateway:
image: nginx:alpine
container_name: pos-gateway
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
ports:
- "80:80"
depends_on:
- pos-server
- pos-client
4. 최종 확인 및 운영 가이드
개발 흐름
- docker-compose up 실행 시 DB, API, UI, 주유기 시뮬레이터가 모두 뜹니다.
- 사용자가 Nuxt UI에서 "주유 시작"을 누르면 NestJS가 dispenser-mock에 패킷을 보냅니다.
- dispenser-mock은 실시간으로 주유량을 리턴하고, NestJS는 Socket.io를 통해 다시 Nuxt 화면의 게이지를 올립니다.
배포 전 체크리스트
- 장비 포트 매핑: 실제 현장에서는 dispenser-mock 대신 시리얼 포트(/dev/ttyUSB0)를 NestJS 컨테이너에 마운트해야 합니다. (devices 옵션 사용)
- 네트워크: 정유사 본사 서버와는 VPN 터널을 통해 전용망으로 통신하도록 라우팅을 잡아야 합니다.
이제 시스템 설계, 코드 구현, 하드웨어 연동 시뮬레이션, 인프라 자동화까지 주유소 POS 개발을 위한 전체 팩이 완성되었습니다.














'IT 개발,관리,연동,자동화' 카테고리의 다른 글
| 구글의 AI 코딩 비서 '안티그래비티', 직접 써보니 충격적인 5가지 사실 (0) | 2026.01.07 |
|---|---|
| 무인점포 통합 관리 플랫폼 사업 제안서: '리모컨 감옥'을 '진정한 패시브 인컴'으로 전환하는 혁신 (0) | 2026.01.06 |
| 코딩 한 줄 없이 나만의 AI 비서를? 카카오가 공개한 미래, 5가지 핵심 요약 (0) | 2026.01.04 |
| '무인점포는 무인이 아니다'...월 300 수익의 꿈이 '리모컨 감옥'으로 변하는 이유 (1) | 2026.01.04 |
| 인공지능 코딩의 진화 oh my opencode 이해하기 (1) | 2026.01.04 |