import requests import json import os import time from pathlib import Path from datetime import datetime, timedelta # 환경변수에서 카페24 인증 정보 로드 CLIENT_ID = os.getenv("CAFE24_CLIENT_ID", "") CLIENT_SECRET = os.getenv("CAFE24_CLIENT_SECRET", "") MALL_ID = os.getenv("CAFE24_MALL_ID", "miraskitchen") REDIRECT_URI = os.getenv("CAFE24_REDIRECT_URI", "") TOKEN_FILE = os.getenv("CAFE24_TOKEN_FILE", str(Path(__file__).resolve().parent / "cafe24_tokens.json")) REQUEST_TIMEOUT = 30 def get_auth_url(redirect_uri=None, state=None): redirect_uri = redirect_uri or REDIRECT_URI url = f"https://{MALL_ID}.cafe24api.com/api/v2/oauth/authorize" params = { "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": redirect_uri, "scope": "mall.read_order mall.write_order mall.read_product", } if state: params["state"] = state return requests.Request("GET", url, params=params).prepare().url import base64 def _get_basic_auth_header(): credentials = f"{CLIENT_ID}:{CLIENT_SECRET}" encoded = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') return f"Basic {encoded}" def request_new_token(auth_code: str, redirect_uri=None): redirect_uri = redirect_uri or REDIRECT_URI url = f"https://{MALL_ID}.cafe24api.com/api/v2/oauth/token" headers = { "Authorization": _get_basic_auth_header(), "Content-Type": "application/x-www-form-urlencoded" } data = { "grant_type": "authorization_code", "code": auth_code, "redirect_uri": redirect_uri } response = requests.post(url, headers=headers, data=data, timeout=REQUEST_TIMEOUT) if response.status_code == 200: _save_tokens(response.json()) return True else: raise Exception(f"카페24 인증 토큰 발급에 실패했습니다. (HTTP {response.status_code})") def refresh_access_token(refresh_token: str): url = f"https://{MALL_ID}.cafe24api.com/api/v2/oauth/token" headers = { "Authorization": _get_basic_auth_header(), "Content-Type": "application/x-www-form-urlencoded" } data = { "grant_type": "refresh_token", "refresh_token": refresh_token } response = requests.post(url, headers=headers, data=data, timeout=REQUEST_TIMEOUT) if response.status_code == 200: _save_tokens(response.json()) return True else: # Preserve the token file so a failed refresh never destroys diagnostics. raise Exception("카페24 인증이 만료되었습니다. 화면의 '재연동 필요' 버튼을 눌러 다시 연동해주세요.") def _save_tokens(token_data): # Cafe24 returns expires_at in string format like "2023-10-01T12:00:00.000" if 'expires_at' not in token_data: # Fallback if expires_at is not provided (Cafe24 provides expires_at) expires_in = token_data.get('expires_in', 7200) # usually 2 hours expires_at = datetime.now() + timedelta(seconds=expires_in - 60) # 1 min buffer token_data['expires_at'] = expires_at.isoformat() with open(TOKEN_FILE, 'w', encoding='utf-8') as f: json.dump(token_data, f) def _parse_cafe24_datetime(value): if not value: return None text = str(value).strip() try: return datetime.fromisoformat(text.replace("Z", "+00:00")) except ValueError: pass for date_format in ("%m/%d/%Y %H:%M:%S", "%Y/%m/%d %H:%M:%S"): try: return datetime.strptime(text, date_format) except ValueError: continue return None def _is_expired(value, *, leeway_seconds=0): expires_at = _parse_cafe24_datetime(value) if expires_at is None: return True now = datetime.now(expires_at.tzinfo) if expires_at.tzinfo else datetime.now() return now >= expires_at - timedelta(seconds=leeway_seconds) def get_token_status(): if not os.path.exists(TOKEN_FILE): return { "authenticated": False, "reauth_required": True, "message": "카페24 연동이 필요합니다.", } try: with open(TOKEN_FILE, "r", encoding="utf-8") as f: tokens = json.load(f) except (OSError, json.JSONDecodeError): return { "authenticated": False, "reauth_required": True, "message": "카페24 인증 파일을 읽을 수 없습니다. 다시 연동해주세요.", } refresh_expired = _is_expired(tokens.get("refresh_token_expires_at")) access_expired = _is_expired(tokens.get("expires_at"), leeway_seconds=60) authenticated = bool(tokens.get("access_token")) and not refresh_expired return { "authenticated": authenticated, "reauth_required": not authenticated, "access_expired": access_expired, "message": ( "카페24 인증이 만료되었습니다. 다시 연동해주세요." if not authenticated else "카페24가 연동되어 있습니다." ), } def get_valid_access_token(): if not os.path.exists(TOKEN_FILE): raise Exception("No tokens found. Please authenticate first.") with open(TOKEN_FILE, 'r', encoding='utf-8') as f: tokens = json.load(f) if _is_expired(tokens.get("expires_at"), leeway_seconds=60): if not tokens.get("refresh_token") or _is_expired(tokens.get("refresh_token_expires_at")): raise Exception("카페24 인증이 만료되었습니다. 화면의 '재연동 필요' 버튼을 눌러 다시 연동해주세요.") print("Cafe24 Access token expired, refreshing...") refresh_access_token(tokens.get('refresh_token')) return get_valid_access_token() return tokens.get('access_token') def get_cafe24_orders_count(status="N20"): token = get_valid_access_token() headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "X-Cafe24-Api-Version": "2026-03-01" } end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") url = f"https://{MALL_ID}.cafe24api.com/api/v2/admin/orders/count" params = { "start_date": start_date, "end_date": end_date, "order_status": status } response = requests.get(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT) if response.status_code == 200: return response.json().get('count', 0) return 0 def get_cafe24_orders(status="N20", progress_callback=None): """ Fetch orders with specific status (default N20: 배송준비중) """ token = get_valid_access_token() headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "X-Cafe24-Api-Version": "2026-03-01" } # Calculate search date range (e.g. last 7 days) end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") url = f"https://{MALL_ID}.cafe24api.com/api/v2/admin/orders" all_orders = [] limit = 100 offset = 0 while True: params = { "start_date": start_date, "end_date": end_date, "order_status": status, "limit": limit, "offset": offset, "embed": "receivers,items" # embed items and receiver addresses } response = requests.get(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT) if response.status_code != 200: raise Exception(f"Failed to fetch Cafe24 orders: {response.text}") json_data = response.json() orders = json_data.get('orders', []) all_orders.extend(orders) if progress_callback: progress_callback(len(all_orders)) if len(orders) < limit: break offset += limit time.sleep(0.5) # rate limit prevention return all_orders def update_cafe24_tracking(dispatch_list): """ dispatch_list is a list of dict: {"order_id": "...", "tracking_no": "...", "shipping_company_code": "0004"} Cafe24 API allows updating order shipments per order or item. Assuming we update the whole order (not item by item): PUT /api/v2/admin/orders/{order_id}/shipments """ token = get_valid_access_token() headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "X-Cafe24-Api-Version": "2026-03-01" } results = { "success": [], "fail": [] } for item in dispatch_list: order_id = item.get("order_id") tracking_no = item.get("tracking_no") company_code = item.get("shipping_company_code") url = f"https://{MALL_ID}.cafe24api.com/api/v2/admin/orders/{order_id}/shipments" payload = { "request": { "tracking_no": tracking_no, "shipping_company_code": company_code } } response = requests.put(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT) if response.status_code in [200, 201]: results["success"].append(order_id) else: reason = "Unknown Error" try: err_data = response.json() reason = err_data.get('error', {}).get('message', response.text) except: reason = response.text results["fail"].append({ "order_id": order_id, "reason": reason }) time.sleep(0.3) # API rate limit return results def get_product_details(product_no): """ Fetch product details to get custom product codes/seller codes """ token = get_valid_access_token() headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "X-Cafe24-Api-Version": "2026-03-01" } url = f"https://{MALL_ID}.cafe24api.com/api/v2/admin/products/{product_no}?embed=variants" response = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT) if response.status_code == 200: return response.json().get('product', {}) return {}