feat(cupang): 설정 화면 2열 개편 — itemcode 다중선택 등록 + 활성/삭제
- 수동 등록 카드 제거
- 왼쪽: itemcode_db 전체 목록(낱개+세트) 다중선택, 등록된 항목 진회색+흰글씨
/cupang/api/products/all + /cupang/products/bulk(JSON 일괄 upsert)
- 오른쪽: 등록 제품명 목록 — 제품명/코드/상태 + 활성·비활성 토글 + 완전삭제
/products/{id}/active (토글), /products/{id}/delete (hard delete)
- ItemcodeReader.list_all 추가
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,102 +9,140 @@
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/">← 달력</a>
|
||||
</div>
|
||||
|
||||
<!-- itemcode_db 검색 → 등록 -->
|
||||
<div class="erp-card cpg-form-card">
|
||||
<div class="cpg-card-head">
|
||||
<h2>itemcode_db 에서 제품 검색·등록</h2>
|
||||
<span class="erp-muted">
|
||||
{% if search_enabled %}제품코드/제품명으로 검색 후 "등록".{% else %}
|
||||
상품 검색 비활성: {{ search_reason }} — 아래에서 수동 등록하세요.{% endif %}
|
||||
</span>
|
||||
<div class="cpg-prod-layout">
|
||||
|
||||
<!-- ── 왼쪽: itemcode_db 상품 목록 (다중선택 → 등록) ── -->
|
||||
<div class="erp-card cpg-form-card">
|
||||
<div class="cpg-card-head">
|
||||
<h2>itemcode_db 상품</h2>
|
||||
<span class="erp-muted">
|
||||
{% if search_enabled %}선택(다중) 후 등록. 이미 등록된 항목은 진한 회색.{% else %}
|
||||
검색 비활성: {{ search_reason }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if search_enabled %}
|
||||
<div class="cpg-inline-form" style="margin-bottom:10px;">
|
||||
<input class="erp-input" type="text" id="cpg-prod-q" placeholder="이름/코드 필터" style="min-width:200px" />
|
||||
<button type="button" class="erp-btn erp-btn-primary" id="cpg-prod-register">선택 등록</button>
|
||||
</div>
|
||||
<div id="cpg-src-list" class="cpg-src-list"><p class="erp-muted">불러오는 중…</p></div>
|
||||
{% else %}
|
||||
<p class="erp-muted">ITEMCODE_DB_URL / ITEMCODE_SEARCH_SQL 설정 후 사용 가능합니다.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if search_enabled %}
|
||||
<div class="cpg-inline-form">
|
||||
<input class="erp-input" type="text" id="cpg-prod-q" placeholder="제품코드 또는 제품명" style="min-width:240px" />
|
||||
<button type="button" class="erp-btn erp-btn-outline" id="cpg-prod-search">검색</button>
|
||||
<!-- ── 오른쪽: 등록된 제품명 ── -->
|
||||
<div class="erp-card cpg-form-card">
|
||||
<div class="cpg-card-head"><h2>등록된 제품명 ({{ products|length }})</h2>
|
||||
<span class="erp-muted">폼의 제품명 드롭다운에 노출</span></div>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>제품명</th><th>제품코드</th><th>상태</th><th>동작</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr {% if not p.active %}style="opacity:.55"{% endif %}>
|
||||
<td>{{ p.product_name }}</td>
|
||||
<td>{{ p.product_code }}</td>
|
||||
<td>{% if p.active %}<span class="erp-badge erp-badge-success">활성</span>{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %}</td>
|
||||
<td>
|
||||
<div class="cpg-row-actions">
|
||||
{% if p.active %}
|
||||
<form method="post" action="/cupang/products/{{ p.id }}/active" class="cpg-inline-form">
|
||||
<input type="hidden" name="active" value="0" />
|
||||
<button type="submit" class="erp-btn erp-btn-outline">비활성</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="/cupang/products/{{ p.id }}/active" class="cpg-inline-form">
|
||||
<input type="hidden" name="active" value="1" />
|
||||
<button type="submit" class="erp-btn erp-btn-outline">활성</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/cupang/products/{{ p.id }}/delete" class="cpg-inline-form"
|
||||
onsubmit="return confirm('완전 삭제합니다. 계속할까요?');">
|
||||
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not products %}
|
||||
<tr><td colspan="4" class="erp-muted">등록된 제품명이 없습니다. 왼쪽에서 선택해 등록하세요.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cpg-prod-results" class="cpg-prod-results"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 수동 등록 -->
|
||||
<div class="erp-card cpg-form-card">
|
||||
<div class="cpg-card-head"><h2>제품명 수동 등록 / 수정</h2>
|
||||
<span class="erp-muted">같은 제품코드는 덮어씁니다.</span></div>
|
||||
<form method="post" action="/cupang/products" class="cpg-rule-grid">
|
||||
<label class="erp-field"><span>제품코드 *</span>
|
||||
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1001" /></label>
|
||||
<label class="erp-field"><span>제품명 *</span>
|
||||
<input class="erp-input" type="text" name="product_name" required placeholder="예: 미라네 1호세트" /></label>
|
||||
<label class="erp-field"><span>정렬</span>
|
||||
<input class="erp-input" type="number" name="sort_order" value="0" /></label>
|
||||
<div><button type="submit" class="erp-btn erp-btn-primary">등록</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 등록된 제품명 목록 -->
|
||||
<div class="erp-card cpg-form-card">
|
||||
<div class="cpg-card-head"><h2>등록된 제품명 ({{ products|length }})</h2>
|
||||
<span class="erp-muted">폼의 제품명 드롭다운에 노출됩니다.</span></div>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>제품명</th><th>제품코드</th><th>정렬</th><th>상태</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr {% if not p.active %}style="opacity:.55"{% endif %}>
|
||||
<td>{{ p.product_name }}</td>
|
||||
<td>{{ p.product_code }}</td>
|
||||
<td>{{ p.sort_order }}</td>
|
||||
<td>{% if p.active %}<span class="erp-badge erp-badge-success">활성</span>{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if p.active %}
|
||||
<form method="post" action="/cupang/products/{{ p.id }}/delete" class="cpg-inline-form"
|
||||
onsubmit="return confirm('비활성화합니다. 계속할까요?');">
|
||||
<button type="submit" class="erp-btn erp-btn-danger">비활성화</button>
|
||||
</form>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{% if search_enabled %}
|
||||
<script type="application/json" id="cpg-registered">{{ registered_codes | tojson }}</script>
|
||||
<script>
|
||||
(function () {
|
||||
var q = document.getElementById("cpg-prod-q");
|
||||
var btn = document.getElementById("cpg-prod-search");
|
||||
var box = document.getElementById("cpg-prod-results");
|
||||
var listBox = document.getElementById("cpg-src-list");
|
||||
var filter = document.getElementById("cpg-prod-q");
|
||||
var regBtn = document.getElementById("cpg-prod-register");
|
||||
var registered = {};
|
||||
try { (JSON.parse(document.getElementById("cpg-registered").textContent || "[]")).forEach(function(c){ registered[c]=true; }); } catch(e){}
|
||||
var all = [];
|
||||
var selected = {};
|
||||
|
||||
function esc(s){ var d=document.createElement("div"); d.textContent=s||""; return d.innerHTML; }
|
||||
function run() {
|
||||
var term = (q.value || "").trim();
|
||||
if (!term) { box.innerHTML = ""; return; }
|
||||
box.innerHTML = '<p class="erp-muted">검색중…</p>';
|
||||
fetch("/cupang/api/products/search?q=" + encodeURIComponent(term))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
var rows = (data && data.results) || [];
|
||||
if (!rows.length) { box.innerHTML = '<p class="erp-muted">결과 없음</p>'; return; }
|
||||
var html = '<table class="erp-table"><thead><tr><th>제품코드</th><th>제품명</th><th>구분</th><th></th></tr></thead><tbody>';
|
||||
rows.forEach(function (it) {
|
||||
html += '<tr><td>' + esc(it.code) + '</td><td>' + esc(it.name) + '</td><td>' + esc(it.type) + '</td>' +
|
||||
'<td><form method="post" action="/cupang/products" class="cpg-inline-form">' +
|
||||
'<input type="hidden" name="product_code" value="' + esc(it.code) + '">' +
|
||||
'<input type="hidden" name="product_name" value="' + esc(it.name) + '">' +
|
||||
'<button type="submit" class="erp-btn erp-btn-outline">등록</button></form></td></tr>';
|
||||
});
|
||||
html += "</tbody></table>";
|
||||
box.innerHTML = html;
|
||||
})
|
||||
.catch(function () { box.innerHTML = '<p class="erp-muted">검색 실패</p>'; });
|
||||
|
||||
function render() {
|
||||
var term = (filter.value || "").trim().toLowerCase();
|
||||
var rows = all.filter(function (it) {
|
||||
if (!term) return true;
|
||||
return (it.code + " " + it.name).toLowerCase().indexOf(term) >= 0;
|
||||
});
|
||||
if (!rows.length) { listBox.innerHTML = '<p class="erp-muted">결과 없음</p>'; return; }
|
||||
var html = "";
|
||||
rows.forEach(function (it) {
|
||||
var cls = "cpg-src-item";
|
||||
if (registered[it.code]) cls += " cpg-registered";
|
||||
if (selected[it.code]) cls += " is-selected";
|
||||
html += '<div class="' + cls + '" data-code="' + esc(it.code) + '">' +
|
||||
'<span class="cpg-src-name">' + esc(it.name) + '</span>' +
|
||||
'<span class="cpg-src-code">' + esc(it.code) + (it.type ? ' · ' + esc(it.type) : '') + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
listBox.innerHTML = html;
|
||||
}
|
||||
btn.addEventListener("click", run);
|
||||
q.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); run(); } });
|
||||
|
||||
listBox.addEventListener("click", function (e) {
|
||||
var item = e.target.closest(".cpg-src-item");
|
||||
if (!item) return;
|
||||
var code = item.getAttribute("data-code");
|
||||
if (selected[code]) { delete selected[code]; item.classList.remove("is-selected"); }
|
||||
else { selected[code] = true; item.classList.add("is-selected"); }
|
||||
});
|
||||
|
||||
filter.addEventListener("input", render);
|
||||
|
||||
regBtn.addEventListener("click", function () {
|
||||
var items = all.filter(function (it) { return selected[it.code]; })
|
||||
.map(function (it) { return { code: it.code, name: it.name }; });
|
||||
if (!items.length) { alert("등록할 상품을 선택하세요."); return; }
|
||||
regBtn.disabled = true;
|
||||
fetch("/cupang/products/bulk", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items })
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function () { location.reload(); })
|
||||
.catch(function () { regBtn.disabled = false; alert("등록 실패"); });
|
||||
});
|
||||
|
||||
fetch("/cupang/api/products/all")
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
all = (data && data.results) || [];
|
||||
if (!all.length) { listBox.innerHTML = '<p class="erp-muted">itemcode_db 결과 없음</p>'; return; }
|
||||
render();
|
||||
})
|
||||
.catch(function () { listBox.innerHTML = '<p class="erp-muted">목록 로드 실패</p>'; });
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user