시퀀스 다이어그램
sequenceDiagram
participant C as Python Client
participant A as authyo
participant API as api/cart api/order
participant P as payo
participant W as wpay.inicis.com
participant O as orderyo
C->>A: POST /api/v1/auth/refresh
A-->>C: { access_token, refresh_token }
C->>API: POST /cart/v3/food/.../vd/
API-->>C: { cart_uuid, cart_signature }
C->>API: POST /order/cart/submit/food/{cust}/{lat}/{lng}
API-->>C: { payment_no, order_number, payment_url }
Note over C: payment_no 발급, 주문 가예약
C->>P: GET {payment_url}
P-->>C: HTML Form A (mid, wpayUserKey, wpayToken, oid, signature, ...)
C->>W: POST /ygypay/u/v2/payreqauth (Form A)
W-->>C: HTML Form B (wtid, wdata, ukey, uskey, ...)
C->>W: POST /ygypay/u/payreqauthAction (Form B)
W-->>P: redirect /certify?wpayToken=...&signature=...
P-->>O: redirect /callback/payment/result/?result_code=success
O-->>C: HTML { order_number, purchase_id, ygy_order_id }
Note over C: 결제 완료, 매장 주문 활성화
C->>API: GET /order/api/v2/orders/{order_number}/
API-->>C: 주문 상세 (transmission_status: ACCEPTED)
참조 — Step 4 body 스키마 전체
Next.js bundle (chunk-489-*.js) 의 getFoodCartSubmitParams 함수 정적 분석으로 확정. 모든 필드 + 출처 + 예시값:
{
// ─────── 식별자 ───────
"customer_id": <int>, // customer.id
// ─────── 회원 정보 (receiveInitialDataFromApp 의 customer 객체) ───────
"customer": {
"id": <int>,
"authyo-token": "<RS256 JWT>", // Step 1 의 access_token 그대로
"location_token": "<HS256 JWT — 위치 토큰>",
"lat": <float>, "lng": <float>,
"address": {
"street_name": "서울특별시 강남구 역삼동 737-1",
"road_name": "서울특별시 강남구 테헤란로 521", // ★ sido+gugun prefix
"alias_type": 0, "alias_text": "",
"street_number": "101동 1001호",
"display_address_long": "테헤란로 521"
},
"address_detail": {
"sido": "서울특별시", "gugun": "강남구",
"admin_dong": "역삼1동", "dong": "역삼동",
"bunji": "737-1", "road": "테헤란로",
"building": "521", "ri": ""
},
"adiyo_address": { /* law, admin, road, point, location_token */ },
"isLogin": true,
"email": "user@example.com",
"phone": "01012345678",
"phone_uuid": "<uuid-v4>", // = DEVICE_ID
"advertisement_id": "<uuid-v4>", // GAID
"subscription_type": null,
"limit_ad_tracking": false,
"app_tracking_transparency": null
},
// ─────── 배달/연락처 ───────
"delivery_info": {
"email": "...", "street_number": "...", "phone": "...",
"use_safety_number": true, // 픽업이면 false
"extra_comments": [],
"comment_to_vendor": "", "comment_to_rider": "",
"is_cutlery_requested": false, // food restaurant 만
"is_contactless_delivery": true,
"expected_takeout_time": "" // 픽업/예약픽업이면 "YYYY-MM-DD HH:mm"
},
// ─────── 결제 (★ 핵심) ───────
"payment": {
"payo_service": "YGYPAY_06", // 카드사별 매핑
"checkout_method_type": "YGYPAY", // 1초결제 고정
"service": "ygypay",
"user_benefits": {
"subscription": { "discount_amount": 0, "discount_info_id": null, "benefit_code": null },
"point": 0,
"coupons": [
{ "code": "<coupon_code>", "amount": <int>, "bank_card_code": "<카드사 코드>" }
]
},
"total": <int>,
"cash_receipt_info": null,
"ygypay_info": {
"token": "<REDACTED>",
"installment_plan": 0
}
},
// ─────── 동의 항목 ───────
"agreements": {
"email_accept": false, "sms_accept": false, "push_accept": false,
"efinance_agreement": true, // ★ 필수 true
"tos": true // ★ 필수 true
},
// ─────── 카트 식별 ───────
"serving_type": "vd",
"cart_uuid": "<uuid-v4>-b24b",
"cart_signature": "<sha1 hex>",
"vendor": { "vendor_id": <int>, "franchise_id": <int|null> },
// ─────── feature flags ───────
"configuration": {
"feature_configuration": {
"additional_discount": true, "restaurant_discount": true,
"delivery_fee_funding": true, "yotime_deal": true,
"bargain_promotion": true, "bargain_vendor_promotion": true,
"bargain_point_promotion": true
}
},
"referral": "",
"build_type": ""
}
자동화 코드 — Python 핵심부
전체 구현은 tools/scripts/replay_client.py 참조. PG chain 자동화의 핵심:
import re, httpx, html as html_lib
from urllib.parse import urljoin, urlparse
def extract_first_form(html_text):
m = re.search(r'<form([^>]*)>(.*?)</form>', html_text, re.DOTALL | re.IGNORECASE)
if not m: return None
attrs, inner = m.group(1), m.group(2)
action = re.search(r'action="([^"]+)"', attrs).group(1)
method = (re.search(r'method="([^"]+)"', attrs) or [None, 'POST'])[1].upper()
inputs = re.findall(r'<input[^>]*name="([^"]+)"[^>]*value="([^"]*)"', inner)
fields = {html_lib.unescape(n): html_lib.unescape(v) for n, v in inputs}
return action, method, fields
def follow_pg_chain(payment_url, restaurant_id, serving_type="vd"):
pg = httpx.Client(timeout=30, follow_redirects=True) # ★ 자동 redirect
url, method, data = payment_url, "GET", None
referer = f"https://checkout.yogiyo.co.kr/food-checkout/?restaurant_id={restaurant_id}&serving_type={serving_type}"
headers = {"User-Agent": "Mozilla/5.0 ...", "Accept": "text/html"}
for _ in range(10):
h = {**headers, "Referer": referer}
if data: h["Content-Type"] = "application/x-www-form-urlencoded"
r = pg.get(url, headers=h) if method == "GET" else pg.post(url, headers=h, data=data)
# orderyo callback 도착하면 종료
if "orderyo.yogiyo.co.kr" in str(r.url):
order_num = re.search(r'"order_number":\s*"([^"]+)"', r.text).group(1)
purchase_id = int(re.search(r'"purchase_id":\s*(\d+)', r.text).group(1))
return {"order_number": order_num, "purchase_id": purchase_id}
action, method, data = extract_first_form(r.text)
if action.startswith("/"):
p = urlparse(str(r.url))
action = f"{p.scheme}://{p.netloc}{action}"
referer, url = str(r.url), action