feat(dispatch): 작업 2단계로 축소 + J&T 조회 URL 수정 + Order ID 확대

- 택배스캔 단계 제거 → 라벨부착 / Kagayaku 전달 2단계
- 버튼 라벨에서 '완료' 제거(라벨부착 / Kagayaku 전달)
- 완료 판정/배치목록 완료수를 label_attached AND handed_to_kagayaku 기준으로
- J&T 송장 조회 딥링크를 gzquery.html?bills= 로 교체(billcode 미입력 문제)
- Order ID 27px 로 확대, 송장 링크 밑줄 강조(굵기/오프셋)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 19:09:16 +09:00
parent 1c14c748dd
commit 4a07ef139d
4 changed files with 17 additions and 17 deletions
+5 -3
View File
@@ -126,8 +126,10 @@ class DispatchStore:
rows = conn.execute( rows = conn.execute(
""" """
SELECT b.*, SELECT b.*,
COUNT(p.id) AS parcel_count, COUNT(p.id) AS parcel_count,
COUNT(p.id) FILTER (WHERE p.courier_scanned) AS scanned_count COUNT(p.id) FILTER (
WHERE p.label_attached AND p.handed_to_kagayaku
) AS done_count
FROM dispatch_batches b FROM dispatch_batches b
LEFT JOIN dispatch_parcels p ON p.batch_id = b.id LEFT JOIN dispatch_parcels p ON p.batch_id = b.id
GROUP BY b.id GROUP BY b.id
@@ -301,7 +303,7 @@ class DispatchStore:
elif isinstance(v, date): elif isinstance(v, date):
out[k] = v.isoformat() out[k] = v.isoformat()
for k in ("id", "batch_id", "parcel_id", "seq", "quantity", for k in ("id", "batch_id", "parcel_id", "seq", "quantity",
"parcel_count", "scanned_count", "total_qty", "box_count"): "parcel_count", "done_count", "total_qty", "box_count"):
if k in out and out[k] is not None: if k in out and out[k] is not None:
try: try:
out[k] = int(out[k]) out[k] = int(out[k])
+4 -5
View File
@@ -27,14 +27,13 @@ from urllib.parse import quote
STATUS_FIELDS: tuple[str, ...] = ( STATUS_FIELDS: tuple[str, ...] = (
"label_attached", "label_attached",
"handed_to_kagayaku", "handed_to_kagayaku",
"courier_scanned",
) )
STATUS_LABELS: dict[str, dict[str, str]] = { STATUS_LABELS: dict[str, dict[str, str]] = {
"product_ready": {"ko": "상품준비 완료", "en": "Product Ready"}, "product_ready": {"ko": "상품준비", "en": "Product Ready"},
"packed": {"ko": "포장완료", "en": "Packed"}, "packed": {"ko": "포장완료", "en": "Packed"},
"label_attached": {"ko": "라벨부착 완료", "en": "Label Attached"}, "label_attached": {"ko": "라벨부착", "en": "Label Attached"},
"handed_to_kagayaku": {"ko": "Kagayaku 전달 완료", "en": "Handed to Kagayaku"}, "handed_to_kagayaku": {"ko": "Kagayaku 전달", "en": "Handed to Kagayaku"},
"courier_scanned": {"ko": "택배스캔 확인", "en": "Courier Scanned"}, "courier_scanned": {"ko": "택배스캔 확인", "en": "Courier Scanned"},
} }
@@ -265,7 +264,7 @@ def courier_logo_url(provider: Any) -> str:
# 매핑 없는 배송사는 링크 없이 텍스트로 표시(courier_tracking_url 가 "" 반환). # 매핑 없는 배송사는 링크 없이 텍스트로 표시(courier_tracking_url 가 "" 반환).
_COURIER_TRACK_URL: dict[str, str] = { _COURIER_TRACK_URL: dict[str, str] = {
"ninjavan": "https://www.ninjavan.co/en-my/tracking?id={t}", "ninjavan": "https://www.ninjavan.co/en-my/tracking?id={t}",
"jnt": "https://www.jtexpress.my/tracking?billcode={t}", "jnt": "https://www.jtexpress.my/index/query/gzquery.html?bills={t}",
"flash": "https://www.flashexpress.my/fle/tracking?se={t}", "flash": "https://www.flashexpress.my/fle/tracking?se={t}",
"citylink": "https://www.citylinkexpress.com/tracking-result/?track_no={t}", "citylink": "https://www.citylinkexpress.com/tracking-result/?track_no={t}",
"poslaju": "https://track.pos.com.my/postal-services/quick-access/?track-trace&trackingNo03={t}", "poslaju": "https://track.pos.com.my/postal-services/quick-access/?track-trace&trackingNo03={t}",
@@ -15,7 +15,7 @@
<tr> <tr>
<th>날짜</th><th>플랫폼</th><th>배치명</th> <th>날짜</th><th>플랫폼</th><th>배치명</th>
<th style="text-align:right">박스</th> <th style="text-align:right">박스</th>
<th style="text-align:right">스캔완료</th> <th style="text-align:right">완료</th>
<th>생성시각</th><th></th> <th>생성시각</th><th></th>
</tr> </tr>
</thead> </thead>
@@ -27,9 +27,9 @@
<td><a href="/dispatch/batches/{{ b.id }}" style="font-weight:600;">{{ b.batch_name or '(이름없음)' }}</a></td> <td><a href="/dispatch/batches/{{ b.id }}" style="font-weight:600;">{{ b.batch_name or '(이름없음)' }}</a></td>
<td style="text-align:right;">{{ b.parcel_count }}</td> <td style="text-align:right;">{{ b.parcel_count }}</td>
<td style="text-align:right;"> <td style="text-align:right;">
{% if b.parcel_count and b.scanned_count == b.parcel_count %} {% if b.parcel_count and b.done_count == b.parcel_count %}
<span style="color:#16a34a;font-weight:600;">{{ b.scanned_count }} ✓</span> <span style="color:#16a34a;font-weight:600;">{{ b.done_count }} ✓</span>
{% else %}{{ b.scanned_count }}{% endif %} {% else %}{{ b.done_count }}{% endif %}
</td> </td>
<td class="erp-muted" style="white-space:nowrap;">{{ b.created_at }}</td> <td class="erp-muted" style="white-space:nowrap;">{{ b.created_at }}</td>
<td style="text-align:right;white-space:nowrap;"> <td style="text-align:right;white-space:nowrap;">
@@ -15,8 +15,9 @@
.dsp-meta b { color:#0f172a; } .dsp-meta b { color:#0f172a; }
.dsp-order { font-size:15px;line-height:1.3;color:#334155;word-break:break-all; } .dsp-order { font-size:15px;line-height:1.3;color:#334155;word-break:break-all; }
.dsp-order b { display:block;font-size:11px;font-weight:600;color:#64748b;letter-spacing:.5px; } .dsp-order b { display:block;font-size:11px;font-weight:600;color:#64748b;letter-spacing:.5px; }
.dsp-order span { font-size:22px;font-weight:800;color:#0f172a; } .dsp-order span { font-size:27px;font-weight:800;color:#0f172a;letter-spacing:.2px; }
.dsp-track-link { color:#2563eb;font-weight:700;text-decoration:underline;text-underline-offset:2px; } .dsp-track-link { color:#2563eb;font-weight:700;text-decoration:underline;
text-decoration-thickness:2px;text-underline-offset:3px;cursor:pointer; }
.dsp-items { list-style:none;margin:0;padding:8px 0;border-top:1px dashed #e2e8f0;border-bottom:1px dashed #e2e8f0; } .dsp-items { list-style:none;margin:0;padding:8px 0;border-top:1px dashed #e2e8f0;border-bottom:1px dashed #e2e8f0; }
.dsp-items li { font-size:16px;font-weight:600;color:#0f172a;padding:2px 0; } .dsp-items li { font-size:16px;font-weight:600;color:#0f172a;padding:2px 0; }
.dsp-items .qty { color:#2563eb; } .dsp-items .qty { color:#2563eb; }
@@ -49,7 +50,6 @@
<button class="erp-btn erp-btn-outline" data-filter="incomplete" type="button">미완료</button> <button class="erp-btn erp-btn-outline" data-filter="incomplete" type="button">미완료</button>
<button class="erp-btn erp-btn-outline" data-filter="label_attached" type="button">라벨부착</button> <button class="erp-btn erp-btn-outline" data-filter="label_attached" type="button">라벨부착</button>
<button class="erp-btn erp-btn-outline" data-filter="handed_to_kagayaku" type="button">Kagayaku 전달</button> <button class="erp-btn erp-btn-outline" data-filter="handed_to_kagayaku" type="button">Kagayaku 전달</button>
<button class="erp-btn erp-btn-outline" data-filter="courier_scanned" type="button">택배스캔 확인</button>
</div> </div>
<input class="erp-input dsp-search" id="dsp-search" type="search" <input class="erp-input dsp-search" id="dsp-search" type="search"
placeholder="검색: Order ID / Tracking ID / Package ID / SKU" /> placeholder="검색: Order ID / Tracking ID / Package ID / SKU" />
@@ -57,12 +57,11 @@
<div class="dsp-cards" id="dsp-cards"> <div class="dsp-cards" id="dsp-cards">
{% for p in parcels %} {% for p in parcels %}
{% set is_done = p.label_attached and p.handed_to_kagayaku and p.courier_scanned %} {% set is_done = p.label_attached and p.handed_to_kagayaku %}
<article class="dsp-card {% if is_done %}is-done{% endif %}" <article class="dsp-card {% if is_done %}is-done{% endif %}"
data-parcel="{{ p.id }}" data-parcel="{{ p.id }}"
data-label_attached="{{ p.label_attached|lower }}" data-label_attached="{{ p.label_attached|lower }}"
data-handed_to_kagayaku="{{ p.handed_to_kagayaku|lower }}" data-handed_to_kagayaku="{{ p.handed_to_kagayaku|lower }}"
data-courier_scanned="{{ p.courier_scanned|lower }}"
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 }}">
{% set logo = p.shipping_provider | courier_logo %} {% set logo = p.shipping_provider | courier_logo %}
{% if logo %} {% if logo %}