feat(dispatch): 배송사 배지를 로고 이미지로 표시

- store.courier_logo_url: 배송사명(부분 일치) → 로고 SVG URL, 없으면 텍스트 폴백
- Jinja 필터 courier_logo 등록(main.py)
- Ninja Van / J&T Express 내장 SVG 로고 추가(static/dispatch/courier/)
- 출고 작업 카드 + Kagayaku 전달 요약에 로고 렌더

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:54:23 +09:00
parent a189b93427
commit a65ca65a8b
6 changed files with 76 additions and 1 deletions
+5
View File
@@ -125,6 +125,11 @@ templates.env.loader = ChoiceLoader(
] ]
) )
# 배송사명 → 로고 URL(dispatch 모듈). 템플릿에서 {{ provider | courier_logo }}.
from .modules.dispatch.store import courier_logo_url as _courier_logo_url # noqa: E402
templates.env.filters["courier_logo"] = _courier_logo_url
oauth = build_google_oauth() oauth = build_google_oauth()
user_store = UserStore(DATA_DIR / "users.json") user_store = UserStore(DATA_DIR / "users.json")
+43
View File
@@ -203,6 +203,49 @@ def normalize_quantity(value: Any) -> int:
return n if n > 0 else 1 return n if n > 0 else 1
# ── 배송사 로고 ──
# 배송사명(부분 일치, 소문자) → 슬러그. 로고 SVG 가 있는 슬러그만 이미지로
# 렌더하고, 없으면 텍스트 배지로 폴백한다. 새 배송사 로고 추가 절차:
# 1) app/static/dispatch/courier/<슬러그>.svg 추가
# 2) 아래 _COURIER_KEYWORDS 에 (키워드, 슬러그) 추가
# 3) _COURIER_LOGO_AVAILABLE 에 슬러그 추가
_COURIER_KEYWORDS: tuple[tuple[str, str], ...] = (
("ninja", "ninjavan"),
("j&t", "jnt"),
("jnt", "jnt"),
("flash", "flash"),
("city", "citylink"),
("pos laju", "poslaju"),
("poslaju", "poslaju"),
("gdex", "gdex"),
("gd express", "gdex"),
("shopee", "spx"),
("spx", "spx"),
)
# 실제 SVG 파일이 있는 슬러그만(없는 건 텍스트 폴백 → 깨진 이미지 방지).
_COURIER_LOGO_AVAILABLE: frozenset[str] = frozenset({"ninjavan", "jnt"})
def courier_slug(provider: Any) -> str:
"""배송사명 → 로고 슬러그(부분 일치). 매칭 없으면 ""."""
name = clean_text(provider).lower()
if not name:
return ""
for keyword, slug in _COURIER_KEYWORDS:
if keyword in name:
return slug
return ""
def courier_logo_url(provider: Any) -> str:
"""배송사명 → 로고 SVG URL. SVG 없는 배송사는 ""(템플릿이 텍스트로 폴백)."""
slug = courier_slug(provider)
if slug in _COURIER_LOGO_AVAILABLE:
return f"/static/dispatch/courier/{slug}.svg"
return ""
def sku_summary(parcels: list[dict[str, Any]]) -> list[dict[str, Any]]: def sku_summary(parcels: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""전체 박스 기준 SKU별 총 수량(피킹 요약). seller_sku 오름차순.""" """전체 박스 기준 SKU별 총 수량(피킹 요약). seller_sku 오름차순."""
totals: dict[str, dict[str, Any]] = {} totals: dict[str, dict[str, Any]] = {}
@@ -21,6 +21,7 @@
.dsp-btn-status small { display:block;font-size:10px;font-weight:500;opacity:.8; } .dsp-btn-status small { display:block;font-size:10px;font-weight:500;opacity:.8; }
.dsp-btn-status.on { background:#16a34a;border-color:#16a34a;color:#fff; } .dsp-btn-status.on { background:#16a34a;border-color:#16a34a;color:#fff; }
.dsp-btn-status:disabled { opacity:.6;cursor:wait; } .dsp-btn-status:disabled { opacity:.6;cursor:wait; }
.dsp-courier { height:22px;width:auto;max-width:140px;display:block; }
.dsp-hidden { display:none !important; } .dsp-hidden { display:none !important; }
@media (max-width:480px){ .dsp-cards{grid-template-columns:1fr;} } @media (max-width:480px){ .dsp-cards{grid-template-columns:1fr;} }
</style> </style>
@@ -62,7 +63,12 @@
data-search="{{ (p.order_id ~ ' ' ~ p.package_id ~ ' ' ~ p.tracking_id ~ ' ' ~ (p['items']|map(attribute='seller_sku')|join(' ')))|lower }}"> data-search="{{ (p.order_id ~ ' ' ~ p.package_id ~ ' ' ~ p.tracking_id ~ ' ' ~ (p['items']|map(attribute='seller_sku')|join(' ')))|lower }}">
<div style="display:flex;justify-content:space-between;align-items:baseline;"> <div style="display:flex;justify-content:space-between;align-items:baseline;">
<span class="dsp-no">No. {{ p.seq }}</span> <span class="dsp-no">No. {{ p.seq }}</span>
{% if p.shipping_provider %}<span class="erp-badge">{{ p.shipping_provider }}</span>{% endif %} {% set logo = p.shipping_provider | courier_logo %}
{% if logo %}
<img class="dsp-courier" src="{{ logo }}" alt="{{ p.shipping_provider }}" title="{{ p.shipping_provider }}" />
{% elif p.shipping_provider %}
<span class="erp-badge">{{ p.shipping_provider }}</span>
{% endif %}
</div> </div>
<div class="dsp-meta"> <div class="dsp-meta">
<div><b>Order</b> {{ p.order_id or '—' }}</div> <div><b>Order</b> {{ p.order_id or '—' }}</div>
@@ -45,6 +45,8 @@
</div> </div>
{% for pv in handover.by_provider %} {% for pv in handover.by_provider %}
<div class="dsp-ho-stat"> <div class="dsp-ho-stat">
{% set logo = pv.provider | courier_logo %}
{% if logo %}<img src="{{ logo }}" alt="{{ pv.provider }}" style="height:20px;width:auto;max-width:130px;margin:0 auto 4px;display:block;" />{% endif %}
<div class="n">{{ pv.box_count }}</div> <div class="n">{{ pv.box_count }}</div>
<div class="l">{{ pv.provider }}</div> <div class="l">{{ pv.provider }}</div>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122 28" role="img" aria-label="J&T Express">
<rect x="0.6" y="0.6" width="120.8" height="26.8" rx="6" fill="#fff" stroke="#E2231A" stroke-width="1.2"/>
<text x="9" y="20" font-family="Arial, 'Helvetica Neue', sans-serif" font-size="16"
font-weight="800" fill="#E2231A" letter-spacing="0.2">J&amp;T</text>
<text x="42" y="19" font-family="Arial, 'Helvetica Neue', sans-serif" font-size="11"
font-weight="700" fill="#1f2937" letter-spacing="1">EXPRESS</text>
</svg>

After

Width:  |  Height:  |  Size: 539 B

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 138 28" role="img" aria-label="Ninja Van">
<rect x="0" y="0" width="138" height="28" rx="6" fill="#D6242C"/>
<!-- 간이 닌자 머리(흰 원 + 빨강 눈띠) -->
<g transform="translate(14,14)">
<circle r="9" fill="#fff"/>
<rect x="-9" y="-2.4" width="18" height="4.8" fill="#D6242C"/>
<rect x="-7" y="-1.1" width="4.2" height="2.2" rx="1.1" fill="#fff"/>
<rect x="2.8" y="-1.1" width="4.2" height="2.2" rx="1.1" fill="#fff"/>
</g>
<text x="29" y="19" font-family="Arial, 'Helvetica Neue', sans-serif" font-size="14"
font-weight="700" fill="#fff" letter-spacing="0.3">Ninja Van</text>
</svg>

After

Width:  |  Height:  |  Size: 681 B