Files
dbx-main/app/static/cupang.js
T
king 9e707be5ac feat(cupang): 쿠팡 밀크런 모듈 추가 (출고 달력/박스계산/입고센터)
- app/modules/cupang: router/db/store/itemcode + 템플릿 5종
- cupang_db 전용(JSON 폴백 없음). CUPANG_DB_URL 미설정 시 안내 페이지
- 월간 달력 + 출고 묶음(헤더+라인), 박스 입수량 자동계산(서버 재계산)
- 입고센터 관리(사용중 soft delete), 박스규칙 upsert, 엑셀 내보내기
- 상품 검색은 itemcode_db 읽기 전용(미설정 시 수동 입력)
- MODULE_KEYS/메뉴/아이콘/라벨 동기화, scripts/sql/cupang_db_init.sql(멱등)
- app/data/ gitignore 추가(PII)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:47:32 +09:00

175 lines
7.3 KiB
JavaScript

/* 쿠팡 밀크런 — 출고 폼 동적 라인 / 상품검색 / 박스계산.
서버가 저장 시 박스 수를 재계산하므로(store.compute_boxes) 여기 계산은 미리보기용. */
(function () {
"use strict";
var CFG = window.CPG || {};
var body = document.getElementById("cpg-lines-body");
var form = document.getElementById("cpg-form");
if (!body || !form) return;
// 입수량 규칙: product_code -> units_per_box
var ruleMap = {};
try {
var rules = JSON.parse(document.getElementById("cpg-box-rules").textContent || "[]");
rules.forEach(function (r) { if (r.active !== false) ruleMap[r.product_code] = r.units_per_box; });
} catch (e) {}
var initLines = [];
try { initLines = JSON.parse(document.getElementById("cpg-init-lines").textContent || "[]"); } catch (e) {}
function ceilDiv(a, b) { return Math.ceil(a / b); }
function recalc(row) {
var qty = parseInt(row.querySelector(".cpg-qty").value, 10) || 0;
var upbInput = row.querySelector(".cpg-upb");
var upb = parseInt(upbInput.value, 10);
var cell = row.querySelector(".cpg-line-calc");
if (!upb || upb <= 0) {
cell.textContent = "미설정";
cell.classList.add("cpg-warn");
return;
}
cell.classList.remove("cpg-warn");
if (qty <= 0) { cell.textContent = "—"; return; }
var boxes = ceilDiv(qty, upb);
var rem = qty % upb;
cell.textContent = boxes + "박스" + (rem ? " +" + rem : " (딱맞음)");
}
function makeRow(data) {
data = data || {};
var tr = document.createElement("tr");
tr.innerHTML =
'<td class="cpg-line-no"></td>' +
'<td><input class="erp-input cpg-code" type="text" autocomplete="off" placeholder="제품코드" /></td>' +
'<td><input class="erp-input cpg-name" type="text" placeholder="제품명" /></td>' +
'<td><input class="erp-input cpg-qty" type="number" min="1" style="max-width:90px" /></td>' +
'<td><input class="erp-input cpg-upb" type="number" min="1" style="max-width:90px" placeholder="입수량" /></td>' +
'<td><span class="cpg-line-calc">—</span></td>' +
'<td><input class="erp-input cpg-memo" type="text" /></td>' +
'<td><button type="button" class="erp-btn erp-btn-outline cpg-line-del">✕</button></td>';
tr.querySelector(".cpg-code").value = data.product_code || "";
tr.querySelector(".cpg-name").value = data.product_name_snapshot || data.product_name || "";
tr.querySelector(".cpg-qty").value = data.quantity || "";
var upb = data.units_per_box;
if (upb == null && data.product_code && ruleMap[data.product_code] != null) upb = ruleMap[data.product_code];
tr.querySelector(".cpg-upb").value = (upb != null ? upb : "");
tr.querySelector(".cpg-line-del").addEventListener("click", function () {
tr.remove(); renumber();
});
tr.querySelector(".cpg-qty").addEventListener("input", function () { recalc(tr); });
tr.querySelector(".cpg-upb").addEventListener("input", function () { recalc(tr); });
var codeInput = tr.querySelector(".cpg-code");
codeInput.addEventListener("input", function () {
// 입수량 규칙 자동 채움 (수동값 없을 때)
var code = codeInput.value.trim();
if (code && ruleMap[code] != null && !tr.querySelector(".cpg-upb").value) {
tr.querySelector(".cpg-upb").value = ruleMap[code];
}
if (CFG.searchEnabled) scheduleSearch(codeInput, tr);
});
codeInput.addEventListener("blur", function () { setTimeout(hidePop, 150); });
body.appendChild(tr);
recalc(tr);
renumber();
return tr;
}
function renumber() {
Array.prototype.forEach.call(body.querySelectorAll("tr"), function (tr, i) {
tr.querySelector(".cpg-line-no").textContent = i + 1;
});
}
// ── 상품 검색 드롭다운 ──
var pop = document.getElementById("cpg-search-pop");
var searchTimer = null;
function hidePop() { if (pop) { pop.hidden = true; pop.innerHTML = ""; } }
function scheduleSearch(input, tr) {
if (!pop) return;
clearTimeout(searchTimer);
var q = input.value.trim();
if (q.length < 1) { hidePop(); return; }
searchTimer = setTimeout(function () { doSearch(q, input, tr); }, 220);
}
function doSearch(q, input, tr) {
fetch(CFG.searchUrl + "?q=" + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (data) {
var results = (data && data.results) || [];
if (!results.length) { hidePop(); return; }
pop.innerHTML = "";
results.forEach(function (item) {
var div = document.createElement("div");
div.className = "cpg-search-item";
div.innerHTML = '<span class="cpg-sc"></span><span class="cpg-sn"></span>';
div.querySelector(".cpg-sc").textContent = item.code;
div.querySelector(".cpg-sn").textContent = item.name + (item.type ? " · " + item.type : "");
div.addEventListener("mousedown", function (e) {
e.preventDefault();
input.value = item.code;
tr.querySelector(".cpg-name").value = item.name || "";
var upb = item.units_per_box != null ? item.units_per_box
: (ruleMap[item.code] != null ? ruleMap[item.code] : "");
if (upb !== "" && !tr.querySelector(".cpg-upb").value) tr.querySelector(".cpg-upb").value = upb;
recalc(tr);
hidePop();
});
pop.appendChild(div);
});
var rect = input.getBoundingClientRect();
pop.style.left = (window.scrollX + rect.left) + "px";
pop.style.top = (window.scrollY + rect.bottom + 2) + "px";
pop.hidden = false;
})
.catch(hidePop);
}
// ── 초기 라인 ──
if (initLines.length) { initLines.forEach(makeRow); } else { makeRow(); }
document.getElementById("cpg-add-line").addEventListener("click", function () { makeRow(); });
// ── 제출: 라인 직렬화 ──
form.addEventListener("submit", function (e) {
var lines = [];
Array.prototype.forEach.call(body.querySelectorAll("tr"), function (tr) {
var code = tr.querySelector(".cpg-code").value.trim();
var qty = parseInt(tr.querySelector(".cpg-qty").value, 10) || 0;
if (!code || qty <= 0) return; // 빈 라인 스킵 (서버도 스킵)
var upb = parseInt(tr.querySelector(".cpg-upb").value, 10);
lines.push({
product_code: code,
product_name_snapshot: tr.querySelector(".cpg-name").value.trim(),
quantity: qty,
units_per_box: (upb > 0 ? upb : null),
memo: tr.querySelector(".cpg-memo").value.trim()
});
});
if (!lines.length) {
e.preventDefault();
alert("품목 라인을 최소 1개 입력하세요 (제품코드 + 수량).");
return;
}
document.getElementById("cpg-lines-json").value = JSON.stringify(lines);
});
// ── 입고센터 select → 스냅샷 hidden 동기화 ──
var centerSel = document.getElementById("cpg-center-select");
var centerName = document.getElementById("cpg-center-name");
if (centerSel && centerName) {
function syncCenter() {
var opt = centerSel.options[centerSel.selectedIndex];
if (opt && opt.value) centerName.value = opt.getAttribute("data-name") || opt.text;
}
centerSel.addEventListener("change", syncCenter);
// 신규일 때만 자동 동기화 (수정 시 기존 스냅샷 보존)
if (!centerName.value) syncCenter();
}
})();