식당 주문 소프트웨어는 대개 두 지점에서 무너집니다. 모르는 사람이 남의 테이블로 주문을 넣을 수 있게 되는 순간, 그리고 손님 화면의 합계와 계산대의 합계가 어긋나는 순간. Korat의 주문 모듈은 이 둘에서 거꾸로 설계했습니다. 테이블 세션은 직원이 열어 단발성 토큰을 발급하고, 계산서는 계산대가 읽는 바로 그 줄들이며, place_dining_order가 모든 줄의 가격을 서버에서 매깁니다. 옵션과 뷔페 타이머와 분할 화면은 그 두 보장 위에 얹힌 것입니다.
테이블 QR은 번호가 아니라 단발성 토큰입니다
「테이블 번호 입력」 경로는 일부러 없습니다. 가장 먼저 떠오르는 기능이고, 그래서 수많은 매장 주문 시스템이 주차장에서도 악용되는 원인입니다. 유일한 자격 증명이 테이블에 인쇄된 번호라면, 그 가게에서 한 번이라도 먹어 본 사람은 앞으로 영원히 아무 테이블에나 주문을 넣을 수 있습니다. 대신 직원이 콘솔에서 테이블을 열고, 그 순간 새 단발성 토큰이 발급되어 그 테이블에 표시되는 QR에 실립니다. 찍어 둔 QR, 친구에게 전달된 스크린샷, 지난 화요일에 뽑아 둔 출력물은 모두 이미 유효하지 않은 토큰을 들고 있어서 아무것도 열지 못합니다.
테이블로 걸어오는 손님은 그 가게의 구성원도, 아직 존재하지 않는 세션의 호스트도 아닙니다. 그래서 행 수준 보안 아래에서 dining_sessions 테이블을 아예 읽지 못합니다. 남의 행이 아니라 단 한 행도 읽지 못합니다. RLS는 누가 호출했는지는 판단할 수 있어도, 요청이 유효한 토큰을 들고 있는지는 판단할 수 없기 때문입니다. 그래서 스캔은 join_session_by_token을 지나갑니다. 서버에서 토큰을 대조하고, 처음 찍은 사람을 호스트로 만들고, 맞지 않으면 이유를 말하지 않은 채 아무것도 돌려주지 않는 SECURITY DEFINER 함수입니다. 「틀린 토큰」과 「만료된 토큰」을 구분해 주는 오류 메시지는 그 자체로 추측 도구입니다.
안에 들어오면 호스트가 링크나 채팅 카드로 나머지 일행을 부를 수 있습니다. 늦게 도착한 친구도 같은 세션에 합류하고, 그 사람이 시킨 음식도 같은 계산서에 올라갑니다.
테이블 QR은 https 주소 — 앱에서도 웹에서도 열립니다
테이블의 QR은 앱 전용 스킴이 아니라 평범한 https 주소입니다. Korat이 설치된 폰은 도메인이 검증된 App Link로 앱에서 열고, 설치되지 않은 폰은 — 모든 iPhone을 포함해 — 웹 주문 페이지를 엽니다. 이것은 제품 전체에서 가장 완전히 죽어 있던 순간을 고친 것입니다. 손님이 자리에 앉아 카메라를 들어 스캔하고, 아무 일도 일어나지 않는 순간 말입니다. 그 페이지는 로그인하지 않은 상태에서 메뉴와 뷔페 남은 시간과 주류 규칙을 모두 읽고, 주문을 실제로 보내는 순간 — 피할 수 없는 유일한 순간 — 에만 로그인을 요구합니다. 예전 korat://dine을 담아 이미 인쇄된 QR도 앞으로 계속 스캔됩니다.
주문: 옵션, 메모, 그리고 마감을 아는 메뉴
메뉴는 분류로 묶이고, 각 항목은 옵션 그룹을 가질 수 있습니다. 샷 추가, 맵기 단계, 면 선택 — 각각 가격 차이와 필수 여부, 최대 선택 수를 갖습니다. 가격 차이는 메모에 적히는 것이 아니라 옵션에 붙어 있고, 그래서 주방 티켓과 계산서 줄이 같은 것을 말합니다. 항목마다 자유 메모와 수량을 넣을 수 있고, 장바구니는 보내기 전까지 계속 수정됩니다.
영업시간, 라스트오더, 그리고 주류 연령 확인
가게는 영업시간과 라스트오더 여유 시간을 설정합니다. 그 구간에 들어서면 메뉴가 주방이 곧 닫는다고 알리고, 지나면 주문 자체가 막힙니다. 일단 받아 두었다가 나중에 조용히 취소하지 않습니다. 품절이나 판매 중지 항목은 누르기 전에 카드 위에 그렇게 적혀 있습니다. 주류에는 별도 플래그가 있어 생년월일로 나이를 확인하고, 직원이 한 세션 전체를 한 번에 성인 확인 처리할 수도 있습니다.
확인 대기열 — 사람이 승낙하기 전에는 주방으로 가지 않습니다
제출된 주문은 곧장 조리대로 가지 않습니다. 주방 화면의 확인 대기열에 들어가고, 직원이 하나씩 확인하거나 거절한 뒤에야 일이 됩니다. 휴대폰이 주방에 티켓을 바로 꽂을 수 있는 시스템은 모르는 사람에게 가게의 식재료를 태울 능력을 넘겨준 것이고, 6번 테이블이 같은 세트를 실수로 네 개 시켰다는 것을 사람이 알아채는 순간도 함께 없앤 것입니다.
직원은 주방 화면에서 주문 상태를 진행시킵니다. 콘솔은 몇 초마다 갱신되는 실시간 배지로 처리할 일을 보여줍니다. 진행 중 주문, 직원을 호출한 테이블, 대기 중 예약, 새 배달. 테이블이 직원 호출을 누르면 상단에 빨간 배너가 즉시 뜹니다. 손님은 그 고리의 나머지 절반을 갖습니다. 직원 호출, 계산서 요청, 자리 뜨기.
실시간 항목별 계산서와 PromptPay QR 분할 계산
실시간 계산서는 계산대가 읽는 바로 그 줄들을 보여주며, 음식이 확인될 때마다 갱신됩니다. 식사 끝의 깜짝 공개도, 기억에 의존한 복원도 없습니다. 테이블이 닫히면 앱이 마무리 화면을 만들어 줍니다. 같은 세션에 있던 사람들을 친구로 추가하겠느냐는 제안이 함께 들어 있고, 이후에 리뷰 알림이 옵니다. 영수증은 개인 계산 이력에 남습니다.
분할 계산 — 나는 얼마를 내야 하는가
분할 화면은 사람들이 실제로 다투는 질문에 답합니다. 나는 얼마를 내야 하는가. 각자 자기 단품 소계에, 테이블에 뷔페 패키지가 있었다면 그것의 균등 분담분이 더해집니다. 금액에 맞춘 PromptPay QR을 만들 수 있고, CRC-16 체크섬이 들어가는 태국 표준 QR 형식을 따릅니다.
분할 계산은 내역이지, 각자 결제가 아닙니다. 각자 얼마를 내야 하는지 계산할 뿐, 네 사람 몫으로 카드를 네 번 긁지 않습니다. 돈을 나누는 일은 여전히 테이블에 앉은 사람들과 가게 사이에서 일어납니다.
그리고 「테이블 번호 입력」 대체 경로는 일부러 없습니다. 손님의 QR이 읽히지 않으면 직원이 콘솔에서 테이블을 다시 엽니다. 수동 경로를 하나 열어 두는 것은, 토큰을 만들어 막으려 했던 바로 그 구멍을 그대로 돌려주는 일입니다.
뷔페 — 인당 패키지, 타이머, 남긴 음식 요금
뷔페는 인당 가격을 붙인 메뉴 항목으로 흉내 내지 않고 자기 모델을 갖습니다. 패키지에는 인당 가격, 직원이 연장할 수 있는 카운트다운, 성인과 어린이 인원이 따로 있습니다. 메뉴 항목은 뷔페 역할 — 포함, 단품 전용, 포함이되 추가 요금 — 을 갖고 있어서 하나의 메뉴가 두 종류의 손님을 함께 받습니다. 잔반 요금도 단위 라벨과 함께 지원합니다. 태국 뷔페 대부분이 이미 테이블에 인쇄해 두었고 소프트웨어 대부분은 없는 척하는 규칙입니다.
배달, 포장, 그리고 테이블 예약
같은 메뉴가 배달과 포장도 받습니다. 주문은 배달비, 배달 수단, 예약 픽업 시각, 주소를 갖고 신규 → 조리 중 → 배달 중 → 완료로 옮겨 가며, 상태가 바뀔 때마다 양쪽에 알림이 갑니다. 테이블 예약은 날짜, 시간, 인원, 메모를 받아 가게의 확인을 기다리고, 대기 건수는 대시보드에 실시간 배지로 붙습니다.
모든 가격은 서버에서 계산됩니다
위의 모든 것을 성립시키는 규칙은 하나입니다. 클라이언트는 절대 가격을 정하지 않습니다. place_dining_order와 place_shop_order는 항목 가격과 옵션 차액과 배달비를 데이터베이스에서 읽고, 휴대폰이 보낸 값은 무시합니다. 신뢰받는 클라이언트 필드는 셀프 할인권입니다. 리뷰도 같은 원칙 위에 있습니다. 매장에서 먹었거나 배달을 시켰다는 것을 데이터베이스가 증명할 수 있는 손님만 쓸 수 있고, 별점은 트리거가 다시 계산하지 누가 입력하지 않습니다.
앉혀 두지 않고 번호만 주고 싶은 가게라면 줄서기를 보세요. 주방 바깥에서 파는 것(포장, 굿즈, 밀키트)은 리테일 쪽이고, 매장과 같은 재고를 읽습니다.
주방과 홀의 오늘 출근과 휴무는 내 직장의 같은 근무표에서 옵니다. 실제로 돌아가는 가게를 처음 여는 이야기는 비즈니스 페이지에 있습니다.