feat(dispatch): Shopee 플랫폼 추가 + 받는 사람 정보 저장/출고 엑셀

- 플랫폼 TikTok/Shopee 분리: 업로드 화면이 선택에 따라 파일 안내 변경
  (TikTok=Order Export xlsx, Shopee=Packing List xlsx / 라벨 PDF 선택)
- TikTok Picking List.pdf 업로드 제거(피킹 요약은 데이터 엑셀로 생성)
- 파서 공용화 + 헤더 행 자동탐지(Shopee 제목행 대응), 두 포맷 헤더 별칭 통합
- 받는 사람 이름/전화/주소를 박스 단위로 dispatch_db 에 저장(개인정보)
  · init SQL 갱신 + 기존 DB용 멱등 마이그레이션 dispatch_add_recipient_columns.sql
- 출고 작업 카드: Package 제거, 받는 사람 이름(크게)+주소(여러 줄)+전화 표시
- 배치 다운로드 zip 에 취합 출고 엑셀 포함
  파일명 YYYY.MM.DD(Ddd)_tictoc.xlsx / _shopee.xlsx (export.py)
- 문서(CLAUDE.md, DISPATCH_MODULE.md) PII 보관·Shopee 반영

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 14:16:11 +09:00
parent c0a661db82
commit f1097b85dc
13 changed files with 388 additions and 86 deletions
+39 -9
View File
@@ -37,16 +37,33 @@ STATUS_LABELS: dict[str, dict[str, str]] = {
"courier_scanned": {"ko": "택배스캔 확인", "en": "Courier Scanned"},
}
# ── 플랫폼 ──
# 주문처(엑셀 '주문처' 컬럼)이자 업로드 슬롯/파서 선택의 기준값.
PLATFORM_TIKTOK = "TikTok"
PLATFORM_SHOPEE = "Shopee"
PLATFORMS: tuple[str, ...] = (PLATFORM_TIKTOK, PLATFORM_SHOPEE)
# ── 엑셀 컬럼명(앞뒤 공백 제거 후, 소문자 비교). 표준키 → 가능한 헤더들 ──
# 개인정보 컬럼은 의도적으로 매핑하지 않는다(이름/주소/전화는 버린다).
# TikTok(03_TikTok_Order_Export.xlsx)·Shopee(Packing List.Doorstep Delivery.xlsx)
# 두 포맷의 헤더를 한 테이블에 모은다(매칭되는 것만 사용). 헤더가 다르면
# 해당 표준키 줄에 별칭 1개만 추가하면 된다.
# 받는 사람(recipient_*) 정보는 작업 카드 표시 + 출고 엑셀 생성을 위해 보관한다.
COLUMN_ALIASES: dict[str, tuple[str, ...]] = {
"order_id": ("order id", "order_id", "주문번호"),
"package_id": ("package id", "package_id", "패키지번호"),
"tracking_id": ("tracking id", "tracking_id", "tracking number", "송장번호"),
"shipping_provider": ("shipping provider name", "shipping provider", "shipping_provider_name", "배송사", "택배사"),
"seller_sku": ("seller sku", "seller_sku", "sku", "판매자 sku"),
"product_name": ("product name", "product_name", "상품명"),
"quantity": ("quantity", "qty", "수량"),
"order_id": ("order id", "order_id", "order sn", "ordersn", "order no.", "order number", "주문번호"),
"package_id": ("package id", "package_id", "package number", "패키지번호"),
"tracking_id": ("tracking id", "tracking_id", "tracking number", "tracking no", "tracking no.", "awb", "awb no.", "송장번호"),
"shipping_provider": ("shipping provider name", "shipping provider", "shipping_provider_name",
"shipping option", "courier", "carrier", "logistics", "배송사", "택배사"),
"seller_sku": ("seller sku", "seller_sku", "sku", "sku reference no.", "sku reference no",
"parent sku reference no.", "판매자 sku", "상품코드"),
"product_name": ("product name", "product_name", "item name", "product", "상품명"),
"quantity": ("quantity", "qty", "수량", "수량(개)"),
"recipient_name": ("recipient", "recipient name", "buyer name", "consignee", "받는 사람", "받는사람", "수취인", "수령인"),
"recipient_phone": ("phone #", "phone#", "phone", "phone number", "contact number", "contact",
"전화", "전화번호", "연락처"),
"recipient_address": ("detail address", "detailed address", "address", "delivery address",
"shipping address", "full address", "주소", "배송지", "배송주소", "수령지 주소"),
}
# 사용자에게 보여줄 컬럼 이름(누락 안내 메시지용). 비교키는 소문자라 별도 표기.
@@ -58,8 +75,14 @@ COLUMN_DISPLAY: dict[str, str] = {
"seller_sku": "Seller SKU",
"product_name": "Product Name",
"quantity": "Quantity",
"recipient_name": "Recipient",
"recipient_phone": "Phone",
"recipient_address": "Address",
}
# 박스 단위로 보존하는 받는 사람 필드(같은 박스의 첫 비어있지 않은 값 사용).
RECIPIENT_FIELDS: tuple[str, ...] = ("recipient_name", "recipient_phone", "recipient_address")
# 박스 묶음 기준 우선순위. 앞에서부터 비어있지 않은 첫 값으로 묶는다.
BOX_KEY_PRIORITY: tuple[str, ...] = ("package_id", "tracking_id", "order_id")
@@ -153,12 +176,16 @@ def group_parcels(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
"package_id": (row.get("package_id") or "").strip(),
"tracking_id": (row.get("tracking_id") or "").strip(),
"shipping_provider": (row.get("shipping_provider") or "").strip(),
"recipient_name": (row.get("recipient_name") or "").strip(),
"recipient_phone": (row.get("recipient_phone") or "").strip(),
"recipient_address": (row.get("recipient_address") or "").strip(),
"_items": {}, # sku -> {product_name, quantity}
}
boxes[key] = box
else:
# 같은 박스인데 식별값이 빈 경우 뒤늦게 채운다(누락 보강).
for field in ("order_id", "package_id", "tracking_id", "shipping_provider"):
for field in ("order_id", "package_id", "tracking_id", "shipping_provider",
"recipient_name", "recipient_phone", "recipient_address"):
if not box[field] and (row.get(field) or "").strip():
box[field] = (row.get(field) or "").strip()
@@ -188,6 +215,9 @@ def group_parcels(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
"package_id": box["package_id"],
"tracking_id": box["tracking_id"],
"shipping_provider": box["shipping_provider"],
"recipient_name": box["recipient_name"],
"recipient_phone": box["recipient_phone"],
"recipient_address": box["recipient_address"],
"items": items,
}
)