feat(cupang): 제품명 카탈로그 설정 + 달력 공휴일 색상 + 폼 UI 개선

- 설정 화면 추가: /cupang/products — itemcode_db 검색 등록 + 수동 등록.
  제품명 드롭다운 소스. cupang_products 테이블 신규(init.sql 멱등 재실행 필요)
- 폼 품목라인: 제품명 드롭다운 선택 → 제품코드 자동 입력
- 라인 컬럼 순서 제품명·제품코드·수량·입수량·박스계산·메모
- 제품코드 140px, 수량·입수량 60px 폭 고정
- 공통 헤더 입력란 겹침 수정(min-width:0, width:100%, box-sizing)
- 달력: 일/공휴일 빨강, 토 파랑, 글자 크기 확대. 한국 공휴일 모듈(holidays.py)
- document_no null → "None" 출력 버그 수정

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 03:24:05 +09:00
parent 9e707be5ac
commit 34c7e2f964
9 changed files with 457 additions and 106 deletions
+33 -12
View File
@@ -27,27 +27,33 @@
gap: 6px;
}
.cpg-cal-wd {
text-align: center; font-size: 12px; font-weight: 600;
color: var(--color-midtone-gray); padding: 4px 0;
text-align: center; font-size: 15px; font-weight: 600;
color: var(--color-midtone-gray); padding: 6px 0;
}
.cpg-sun { color: var(--color-callout-red); }
.cpg-sat { color: #2b5bc2; }
.cpg-cal-cell {
display: flex; flex-direction: column; gap: 4px;
min-height: 84px; padding: 6px;
min-height: 96px; padding: 8px;
border: 1px solid var(--color-subtle-ash);
border-radius: 10px;
background: #fff;
transition: border-color .12s, box-shadow .12s;
}
.cpg-cal-cell:hover { border-color: var(--color-midtone-gray); }
.cpg-out { background: var(--color-ghost-gray); color: var(--color-midtone-gray); }
.cpg-out { background: var(--color-ghost-gray); }
.cpg-out .cpg-cal-day { color: var(--color-midtone-gray); }
.cpg-today { border-color: var(--color-deep-black); }
.cpg-selected { box-shadow: 0 0 0 2px var(--color-deep-black); border-color: var(--color-deep-black); }
.cpg-cal-day { font-size: 13px; font-weight: 600; }
.cpg-cal-day { font-size: 17px; font-weight: 600; }
/* 일요일/공휴일 빨강, 토요일 파랑 (당월 셀 우선, 전후월은 옅게) */
.cpg-red .cpg-cal-day { color: var(--color-callout-red); }
.cpg-blue .cpg-cal-day { color: #2b5bc2; }
.cpg-out.cpg-red .cpg-cal-day { color: #e3a59b; }
.cpg-out.cpg-blue .cpg-cal-day { color: #9db4dd; }
.cpg-cal-badges { display: flex; flex-direction: column; gap: 3px; }
.cpg-mini { font-size: 11px; padding: 1px 6px; align-self: flex-start; }
.cpg-mini { font-size: 12px; padding: 2px 7px; align-self: flex-start; }
/* ── 오른쪽 리스트 ── */
.cpg-list-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }
@@ -68,24 +74,39 @@
.cpg-header-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
gap: 12px 16px;
}
.cpg-rule-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px; align-items: end;
gap: 12px 16px; align-items: end;
}
.erp-field { display: flex; flex-direction: column; gap: 4px; }
.erp-field > span { font-size: 12px; color: var(--color-midtone-gray); }
/* 그리드 항목 겹침 방지: 칸이 줄어들 때 내용이 밖으로 넘치지 않게 */
.cpg-header-grid .erp-field,
.cpg-rule-grid .erp-field { min-width: 0; }
.cpg .erp-field { display: flex; flex-direction: column; gap: 4px; margin: 0; }
.cpg .erp-field > span { font-size: 12px; color: var(--color-midtone-gray); }
/* 입력란이 칸 너비를 넘지 않도록 (date/select 포함) */
.cpg .erp-field > .erp-input,
.cpg .erp-field > .erp-select,
.cpg .erp-field > textarea.erp-input { width: 100%; max-width: 100%; box-sizing: border-box; }
.cpg-full { grid-column: 1 / -1; }
/* 검정 배경 버튼 글자는 항상 흰색 (안전 보강) */
.cpg .erp-btn-primary, .cpg .erp-btn-primary:visited { color: var(--color-canvas-white); }
.cpg-inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
.cpg-row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
/* ── 라인 테이블 ── */
.cpg-lines td { vertical-align: middle; }
.cpg-lines input { width: 100%; }
.cpg-line-calc { font-size: 12px; color: var(--color-midtone-gray); white-space: nowrap; }
.cpg-lines .cpg-name-sel { width: 100%; min-width: 160px; }
/* 폭 고정: 제품코드 140px, 수량·입수량 60px */
.cpg-lines .cpg-code { width: 140px; min-width: 140px; box-sizing: border-box; }
.cpg-lines .cpg-qty,
.cpg-lines .cpg-upb { width: 60px; min-width: 60px; box-sizing: border-box; }
.cpg-lines .cpg-memo { width: 100%; min-width: 120px; box-sizing: border-box; }
.cpg-line-calc { font-size: 13px; color: var(--color-midtone-gray); white-space: nowrap; }
.cpg-line-calc.cpg-warn { color: var(--color-callout-red); }
.cpg-line-del { cursor: pointer; }
+83 -88
View File
@@ -1,5 +1,6 @@
/* 쿠팡 밀크런 — 출고 폼 동적 라인 / 상품검색 / 박스계산.
서버가 저장 시 박스 수를 재계산하므로(store.compute_boxes) 여기 계산은 미리보기용. */
/* 쿠팡 밀크런 — 출고 폼 동적 라인.
제품명 드롭다운(설정에서 등록한 카탈로그) 선택 → 제품코드 자동 입력.
수량 입력 시 박스 수 미리보기(서버가 저장 시 store.compute_boxes 로 재계산). */
(function () {
"use strict";
var CFG = window.CPG || {};
@@ -14,6 +15,12 @@
rules.forEach(function (r) { if (r.active !== false) ruleMap[r.product_code] = r.units_per_box; });
} catch (e) {}
// 제품 카탈로그: [{product_code, product_name}]
var products = [];
try { products = JSON.parse(document.getElementById("cpg-products").textContent || "[]"); } catch (e) {}
var nameByCode = {};
products.forEach(function (p) { nameByCode[p.product_code] = p.product_name; });
var initLines = [];
try { initLines = JSON.parse(document.getElementById("cpg-init-lines").textContent || "[]"); } catch (e) {}
@@ -21,56 +28,85 @@
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 upb = parseInt(row.querySelector(".cpg-upb").value, 10);
var cell = row.querySelector(".cpg-line-calc");
if (!upb || upb <= 0) {
cell.textContent = "미설정";
cell.classList.add("cpg-warn");
return;
}
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;
var boxes = ceilDiv(qty, upb), rem = qty % upb;
cell.textContent = boxes + "박스" + (rem ? " +" + rem : " (딱맞음)");
}
function buildNameSelect(selectedCode) {
var sel = document.createElement("select");
sel.className = "erp-select cpg-name-sel";
var opt0 = document.createElement("option");
opt0.value = ""; opt0.textContent = "— 제품명 선택 —";
sel.appendChild(opt0);
var found = false;
products.forEach(function (p) {
var o = document.createElement("option");
o.value = p.product_code;
o.textContent = p.product_name;
o.setAttribute("data-name", p.product_name);
if (p.product_code === selectedCode) { o.selected = true; found = true; }
sel.appendChild(o);
});
// 카탈로그에 없는 기존 라인 코드 → 임시 옵션으로 표시
if (selectedCode && !found) {
var o = document.createElement("option");
o.value = selectedCode;
o.textContent = (nameByCode[selectedCode] || selectedCode) + " (미등록)";
o.setAttribute("data-name", nameByCode[selectedCode] || selectedCode);
o.selected = true;
sel.appendChild(o);
}
return sel;
}
function makeRow(data) {
data = data || {};
var tr = document.createElement("tr");
// 컬럼: # / 제품명(select) / 제품코드 / 수량 / 입수량 / 박스계산 / 라인메모 / 삭제
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 class="cpg-cell-name"></td>' +
'<td><input class="erp-input cpg-code" type="text" placeholder="제품코드" /></td>' +
'<td><input class="erp-input cpg-qty" type="number" min="1" /></td>' +
'<td><input class="erp-input cpg-upb" type="number" min="1" 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 code = data.product_code || "";
var nameSel = buildNameSelect(code);
tr.querySelector(".cpg-cell-name").appendChild(nameSel);
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.value = code;
tr.querySelector(".cpg-qty").value = data.quantity || "";
var upb = data.units_per_box;
if (upb == null && code && ruleMap[code] != null) upb = ruleMap[code];
tr.querySelector(".cpg-upb").value = (upb != null ? upb : "");
// 제품명 선택 → 코드 자동 입력 + 입수량 자동
nameSel.addEventListener("change", function () {
var c = nameSel.value;
codeInput.value = c;
if (c && ruleMap[c] != null) tr.querySelector(".cpg-upb").value = ruleMap[c];
recalc(tr);
});
codeInput.addEventListener("blur", function () { setTimeout(hidePop, 150); });
// 코드 직접 입력 시 입수량 규칙 자동
codeInput.addEventListener("input", function () {
var c = codeInput.value.trim();
if (c && ruleMap[c] != null && !tr.querySelector(".cpg-upb").value) {
tr.querySelector(".cpg-upb").value = ruleMap[c];
}
});
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); });
body.appendChild(tr);
recalc(tr);
@@ -84,68 +120,28 @@
});
}
// ── 상품 검색 드롭다운 ──
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; // 빈 라인 스킵 (서버도 스킵)
if (!code || qty <= 0) return;
var upb = parseInt(tr.querySelector(".cpg-upb").value, 10);
var sel = tr.querySelector(".cpg-name-sel");
var name = "";
if (sel && sel.selectedIndex >= 0) {
var opt = sel.options[sel.selectedIndex];
name = opt ? (opt.getAttribute("data-name") || "") : "";
}
if (!name) name = nameByCode[code] || code;
lines.push({
product_code: code,
product_name_snapshot: tr.querySelector(".cpg-name").value.trim(),
product_name_snapshot: name,
quantity: qty,
units_per_box: (upb > 0 ? upb : null),
memo: tr.querySelector(".cpg-memo").value.trim()
@@ -153,13 +149,13 @@
});
if (!lines.length) {
e.preventDefault();
alert("품목 라인을 최소 1개 입력하세요 (제품코드 + 수량).");
alert("품목 라인을 최소 1개 입력하세요 (제품명/코드 + 수량).");
return;
}
document.getElementById("cpg-lines-json").value = JSON.stringify(lines);
});
// ── 입고센터 select → 스냅샷 hidden 동기화 ──
// 입고센터 select → 스냅샷 hidden 동기화
var centerSel = document.getElementById("cpg-center-select");
var centerName = document.getElementById("cpg-center-name");
if (centerSel && centerName) {
@@ -168,7 +164,6 @@
if (opt && opt.value) centerName.value = opt.getAttribute("data-name") || opt.text;
}
centerSel.addEventListener("change", syncCenter);
// 신규일 때만 자동 동기화 (수정 시 기존 스냅샷 보존)
if (!centerName.value) syncCenter();
}
})();