feat(dispatch): 한글/영문 토글(말레이시아 직원용)

- _nav 에 KO/EN 토글 버튼 + 공용 JS(data-ko/data-en 텍스트, data-*-ph placeholder
  교체, localStorage 저장 → 전 페이지 공통, dsp:lang 이벤트 발행)
- batches/new/detail/picking/handover 정적 라벨 이중언어화
- 동적 텍스트도 언어 반영: 완료 N/M 라벨, 일괄 토글 확인창, 업로드 안내/파일 라벨
- 카드 작업 버튼은 기존대로 한/영 동시 표기 유지

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 15:47:32 +09:00
parent 90524100ed
commit 1d3b97c8fc
6 changed files with 154 additions and 61 deletions
@@ -50,22 +50,27 @@
<div class="erp-card">
<div class="cpg-card-head" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<h2>출고 작업 박스 {{ parcels|length }}</h2>
<span class="erp-muted" id="dsp-done-label">완료 {{ done_count }} / {{ parcels|length }}</span>
<h2><span data-ko="출고 작업 · 박스" data-en="Dispatch Work · Boxes">출고 작업 · 박스</span> {{ parcels|length }}</h2>
<span class="erp-muted" id="dsp-done-label"
data-done="{{ done_count }}" data-total="{{ parcels|length }}">완료 {{ done_count }} / {{ parcels|length }}</span>
</div>
<div class="dsp-toolbar">
<div class="dsp-filter" id="dsp-filter">
<button class="erp-btn erp-btn-primary" data-filter="all" 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="handed_to_kagayaku" type="button">Kagayaku 전달</button>
<button class="erp-btn erp-btn-primary" data-filter="all" type="button" data-ko="전체" data-en="All">전체</button>
<button class="erp-btn erp-btn-outline" data-filter="incomplete" type="button" data-ko="미완료" data-en="Incomplete">미완료</button>
<button class="erp-btn erp-btn-outline" data-filter="label_attached" type="button" data-ko="라벨부착" data-en="Label Attached">라벨부착</button>
<button class="erp-btn erp-btn-outline" data-filter="handed_to_kagayaku" type="button" data-ko="Kagayaku 전달" data-en="Kagayaku Handover">Kagayaku 전달</button>
</div>
<input class="erp-input dsp-search" id="dsp-search" type="search"
data-ko-ph="검색: Order ID / Tracking ID / 받는 사람 / SKU"
data-en-ph="Search: Order ID / Tracking ID / Recipient / SKU"
placeholder="검색: Order ID / Tracking ID / 받는 사람 / SKU" />
<div class="dsp-bulk">
<button class="dsp-btn-status" type="button" data-bulk="label_attached" onclick="dspBulk(this,'label_attached')">모두 라벨 부착</button>
<button class="dsp-btn-status" type="button" data-bulk="handed_to_kagayaku" onclick="dspBulk(this,'handed_to_kagayaku')">모두 Kagayaku 전달</button>
<button class="dsp-btn-status" type="button" data-bulk="label_attached" onclick="dspBulk(this,'label_attached')"
data-ko="모두 라벨 부착" data-en="Label All">모두 라벨 부착</button>
<button class="dsp-btn-status" type="button" data-bulk="handed_to_kagayaku" onclick="dspBulk(this,'handed_to_kagayaku')"
data-ko="모두 Kagayaku 전달" data-en="Handover All">모두 Kagayaku 전달</button>
</div>
</div>
@@ -101,7 +106,7 @@
{% for it in p['items'] %}
<li>{{ it.seller_sku }} <span class="qty">× {{ it.quantity }}</span></li>
{% else %}
<li class="erp-muted" style="font-weight:400;">상품 없음</li>
<li class="erp-muted" style="font-weight:400;" data-ko="상품 없음" data-en="No items">상품 없음</li>
{% endfor %}
</ul>
<div class="dsp-status">
@@ -132,7 +137,10 @@
var BATCH_ID = document.querySelector('.dsp').getAttribute('data-batch');
// ── 일괄 토글 처리 (전부 완료면 해제, 아니면 완료) ──
var BULK_LABELS = { label_attached: '라벨 부착', handed_to_kagayaku: 'Kagayaku 전달' };
var BULK_LABELS = {
ko: { label_attached: '라벨 부착', handed_to_kagayaku: 'Kagayaku 전달' },
en: { label_attached: 'Label Attached', handed_to_kagayaku: 'Kagayaku Handover' },
};
function allOn(field) {
var cards = document.querySelectorAll('.dsp-card');
@@ -151,8 +159,12 @@
window.dspBulk = function (btn, field) {
var target = !allOn(field); // 전부 완료면 해제, 아니면 완료
var verb = target ? '완료로 표시' : '완료 해제';
if (!confirm('이 배치의 모든 박스를 "' + (BULK_LABELS[field] || field) + '" ' + verb + '할까요?')) return;
var en = (window.dspLang && window.dspLang() === 'en');
var label = BULK_LABELS[en ? 'en' : 'ko'][field] || field;
var msg = en
? ('Set ALL boxes "' + label + '" to ' + (target ? 'done' : 'not done') + '?')
: ('이 배치의 모든 박스를 "' + label + '" ' + (target ? '완료로 표시' : '완료 해제') + '할까요?');
if (!confirm(msg)) return;
document.querySelectorAll('.dsp-bulk button').forEach(function (b) { b.disabled = true; });
var body = new URLSearchParams();
body.set('field', field);
@@ -207,9 +219,16 @@
var cards = document.querySelectorAll('.dsp-card');
var done = document.querySelectorAll('.dsp-card.is-done').length;
var label = document.getElementById('dsp-done-label');
if (label) label.textContent = '완료 ' + done + ' / ' + cards.length;
if (!label) return;
label.setAttribute('data-done', done);
label.setAttribute('data-total', cards.length);
var prefix = (window.dspLang && window.dspLang() === 'en') ? 'Done ' : '완료 ';
label.textContent = prefix + done + ' / ' + cards.length;
}
// 언어 변경 시 동적 라벨(완료 N/M) 다시 그림.
document.addEventListener('dsp:lang', updateDoneLabel);
// ── 필터 ──
var currentFilter = 'all';
function matchesFilter(card) {