feat(cafe24): 옵션/품목 편집을 별도 모달 창으로
380px 정보 패널은 옵션값·품목 표를 담기에 좁다. 패널에는 「팝업에서 관리」 버튼만 두고, 최대 1180px 모달(왼쪽 옵션 정의 / 오른쪽 품목 표: 이미지·옵션· 품목코드·자체코드·추가금액·진열·판매)에서 편집한다. - 모달은 페이지에 하나(패널 조각 교체와 무관), 열 때마다 카페24에서 새로 읽음. - 닫기: X · 배경 클릭 · Esc. 저장 안 한 변경(.is-dirty)이 있으면 확인창. - 다른 상품을 고르면 열려 있던 창은 닫힘. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -80,13 +80,17 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{# ── 옵션 / 품목 — 펼칠 때 JSON 으로 불러온다(카페24 호출 2회) ── #}
|
||||
<details class="cf24-side-sec cf24-side-details" id="cf24-options-details">
|
||||
<summary class="cf24-side-title">옵션 / 품목 <span class="cf24-muted" id="cf24-options-count"></span></summary>
|
||||
<div class="cf24-options-body" id="cf24-options-body">
|
||||
<p class="cf24-muted">불러오는 중…</p>
|
||||
{# ── 옵션 / 품목 — 패널이 좁아 별도 모달(products.html 의 #cf24-options-modal)에서
|
||||
편집한다. 버튼을 누를 때 JSON 으로 불러온다(카페24 호출 2회). ── #}
|
||||
<section class="cf24-side-sec">
|
||||
<div class="cf24-side-actions">
|
||||
<h4 class="cf24-side-title" style="margin:0;">옵션 / 품목 <span class="cf24-muted" id="cf24-options-count"></span></h4>
|
||||
<button type="button" class="erp-btn erp-btn-primary" id="cf24-options-open">팝업에서 관리</button>
|
||||
</div>
|
||||
</details>
|
||||
<p class="cf24-side-note" style="margin:8px 0 0;">
|
||||
옵션명·옵션값 이름/썸네일·표시방식, 품목별 자체코드·추가금액·진열/판매를 넓은 창에서 수정합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260918b" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260918c" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -109,6 +109,19 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{# ── 옵션/품목 편집 모달 — 오른쪽 패널(380px)은 품목 표를 담기에 좁아 별도 창에서
|
||||
편집한다. 페이지에 하나만 두고(패널 조각 교체와 무관), 열 때마다 카페24에서 새로
|
||||
읽는다. 내용은 products.html 의 JS(setupOptions → render) 가 그린다. #}
|
||||
<div class="cf24-modal" id="cf24-options-modal" hidden aria-hidden="true">
|
||||
<div class="cf24-modal-box" role="dialog" aria-modal="true" aria-labelledby="cf24-options-title">
|
||||
<div class="cf24-modal-head">
|
||||
<h3 class="cf24-modal-title">옵션 / 품목 — <span id="cf24-options-title"></span></h3>
|
||||
<button type="button" class="cf24-modal-close" id="cf24-options-close" aria-label="닫기">✕</button>
|
||||
</div>
|
||||
<div class="cf24-modal-body cf24-options-body" id="cf24-options-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -725,6 +738,46 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── 옵션/품목 모달 — 페이지에 하나. 열기/닫기만 여기서, 내용은 setupOptions 가 그린다.
|
||||
var optModal = (function () {
|
||||
var el = document.getElementById("cf24-options-modal");
|
||||
var body = el ? el.querySelector("#cf24-options-body") : null;
|
||||
var title = el ? el.querySelector("#cf24-options-title") : null;
|
||||
var api = {
|
||||
el: el, body: body,
|
||||
isOpen: function () { return !!el && !el.hidden; },
|
||||
open: function (name) {
|
||||
if (!el) return;
|
||||
if (title) title.textContent = name || "";
|
||||
el.hidden = false;
|
||||
el.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("cf24-modal-open");
|
||||
},
|
||||
close: function (force) {
|
||||
if (!el || el.hidden) return Promise.resolve(true);
|
||||
var dirty = !force && el.querySelector(".is-dirty");
|
||||
var ask = dirty ? window.erpConfirm("저장하지 않은 변경이 있습니다. 창을 닫을까요?") : Promise.resolve(true);
|
||||
return ask.then(function (ok) {
|
||||
if (!ok) return false;
|
||||
el.hidden = true;
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("cf24-modal-open");
|
||||
if (body) body.innerHTML = "";
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
if (el) {
|
||||
el.querySelector("#cf24-options-close").addEventListener("click", function () { api.close(false); });
|
||||
// 어두운 배경 클릭 = 닫기 (창 안 클릭은 무시)
|
||||
el.addEventListener("click", function (e) { if (e.target === el) api.close(false); });
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && api.isOpen()) api.close(false);
|
||||
});
|
||||
}
|
||||
return api;
|
||||
})();
|
||||
|
||||
window.cf24BindSide = function () {
|
||||
var sideRoot = document.getElementById("cf24-side-pane");
|
||||
var root = sideRoot && sideRoot.querySelector("#cf24-side");
|
||||
@@ -860,21 +913,22 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// ── 옵션 / 품목 — 펼칠 때 불러온다 ──
|
||||
// ── 옵션 / 품목 — 「팝업에서 관리」 버튼으로 모달을 열고 그때 불러온다 ──
|
||||
(function setupOptions() {
|
||||
var details = root.querySelector("#cf24-options-details");
|
||||
var body = root.querySelector("#cf24-options-body");
|
||||
var openBtn = root.querySelector("#cf24-options-open");
|
||||
var body = optModal.body;
|
||||
var count = root.querySelector("#cf24-options-count");
|
||||
if (!details || !body) return;
|
||||
var loaded = false;
|
||||
if (!openBtn || !body) return;
|
||||
var state = { option: null, variants: [], displayTypes: {} };
|
||||
|
||||
details.addEventListener("toggle", function () {
|
||||
if (details.open && !loaded) { loaded = true; load(); }
|
||||
openBtn.addEventListener("click", function () {
|
||||
var nameEl = document.getElementById("cf24-name-text");
|
||||
optModal.open((nameEl ? nameEl.textContent : "") + " (상품번호 " + no + ")");
|
||||
load();
|
||||
});
|
||||
|
||||
function load() {
|
||||
body.innerHTML = '<p class="cf24-muted">불러오는 중…</p>';
|
||||
body.innerHTML = '<p class="cf24-muted">카페24에서 불러오는 중…</p>';
|
||||
apiJson("GET", "/cafe24/products/" + no + "/options")
|
||||
.then(function (data) {
|
||||
state.option = data.option;
|
||||
@@ -883,10 +937,9 @@
|
||||
render();
|
||||
})
|
||||
.catch(function (err) {
|
||||
loaded = false;
|
||||
body.innerHTML = '<p class="cf24-err">옵션을 불러오지 못했습니다: ' + escHtml(err.message) +
|
||||
'</p><button type="button" class="erp-btn erp-btn-outline" id="cf24-opt-retry">다시 시도</button>';
|
||||
body.querySelector("#cf24-opt-retry").addEventListener("click", function () { loaded = true; load(); });
|
||||
body.querySelector("#cf24-opt-retry").addEventListener("click", load);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -953,8 +1006,10 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
// 옵션 정의 편집
|
||||
var html = '<form class="cf24-side-form" id="cf24-opt-form">';
|
||||
// 옵션 정의 편집 (모달 왼쪽 열)
|
||||
var html = '<div class="cf24-modal-grid"><section class="cf24-modal-col">' +
|
||||
'<h4 class="cf24-side-title">옵션 정의</h4>' +
|
||||
'<form class="cf24-side-form" id="cf24-opt-form">';
|
||||
opt.options.forEach(function (o, gi) {
|
||||
html += '<div class="cf24-opt-group" data-group="' + gi + '">' +
|
||||
'<div class="cf24-opt-head">' +
|
||||
@@ -981,35 +1036,37 @@
|
||||
html += '<p class="cf24-side-note">옵션값 추가·삭제는 카페24 API 가 지원하지 않습니다(옵션 삭제 후 다시 만들어야 합니다). 썸네일은 표시방식이 「미리보기(이미지)」일 때 쇼핑몰에 보입니다.</p>' +
|
||||
'<div class="cf24-side-actions"><span class="cf24-muted" id="cf24-opt-msg"></span>' +
|
||||
' <button class="erp-btn erp-btn-primary" type="submit">옵션 저장</button></div>' +
|
||||
"</form>";
|
||||
"</form>" +
|
||||
'<div class="cf24-opt-danger"><button type="button" class="erp-btn erp-btn-outline" id="cf24-opt-delete">옵션 전체 삭제</button></div>' +
|
||||
"</section>";
|
||||
|
||||
// 품목 — 패널 폭(380px)에 표는 좁다(실측: 이름이 이미지 아래로 꺾여 잘림).
|
||||
// 품목마다 2줄 카드: 1줄 아이콘 + 옵션값 이름, 2줄 자체코드·추가금액·진열·판매.
|
||||
html += '<div class="cf24-opt-group"><h5 class="cf24-side-title">품목 (' + variants.length + ')</h5>';
|
||||
// 품목 표 (모달 오른쪽 열) — 넓은 창이라 한 줄에 다 들어간다.
|
||||
html += '<section class="cf24-modal-col"><h4 class="cf24-side-title">품목 (' + variants.length + ')</h4>';
|
||||
if (!variants.length) {
|
||||
html += '<p class="cf24-muted">품목이 아직 조회되지 않았습니다. 잠시 뒤 「옵션 / 품목」을 접었다 펴서 다시 불러오세요.</p>';
|
||||
html += '<p class="cf24-muted">품목이 아직 조회되지 않았습니다. 잠시 뒤 창을 닫았다 다시 여세요.</p>';
|
||||
} else {
|
||||
html += '<div class="cf24-variants-wrap">';
|
||||
html += '<div class="cf24-variants-wrap"><table class="cf24-variants"><thead><tr>' +
|
||||
'<th class="cf24-v-imgcol"></th><th>옵션</th><th class="cf24-v-syscode">품목코드</th>' +
|
||||
'<th class="cf24-v-code">자체 품목코드</th><th class="cf24-v-amount">추가금액</th>' +
|
||||
'<th class="cf24-v-flag">진열</th><th class="cf24-v-flag">판매</th></tr></thead><tbody>';
|
||||
variants.forEach(function (v) {
|
||||
var label = v.options.map(function (o) { return o.value; }).join(" / ") || v.variant_code;
|
||||
var amount = String(parseInt(v.additional_amount || "0", 10) || 0);
|
||||
html += '<div class="cf24-v-item" data-code="' + escHtml(v.variant_code) + '">' +
|
||||
'<div class="cf24-v-head" title="' + escHtml(v.variant_code) + '">' +
|
||||
(v.image ? '<img class="cf24-v-img" src="' + escHtml(v.image) + '" alt="" />' : '<span class="cf24-v-img cf24-v-img-empty"></span>') +
|
||||
'<span class="cf24-v-name">' + escHtml(label) + "</span></div>" +
|
||||
'<div class="cf24-v-row">' +
|
||||
'<label class="cf24-v-code"><span>자체코드</span><input type="text" name="custom_variant_code" maxlength="40" value="' + escHtml(v.custom_variant_code) + '" /></label>' +
|
||||
'<label class="cf24-v-amount"><span>추가금액</span><input type="text" name="additional_amount" inputmode="numeric" value="' + escHtml(amount) + '" /></label>' +
|
||||
'<span class="cf24-v-flags">' +
|
||||
'<button type="button" class="cf24-v-toggle' + (v.display ? " is-on" : "") + '" data-flag="display" data-on="' + (v.display ? 1 : 0) + '" data-initial="' + (v.display ? 1 : 0) + '">' + (v.display ? "진열" : "미진열") + "</button>" +
|
||||
'<button type="button" class="cf24-v-toggle' + (v.selling ? " is-on" : "") + '" data-flag="selling" data-on="' + (v.selling ? 1 : 0) + '" data-initial="' + (v.selling ? 1 : 0) + '">' + (v.selling ? "판매" : "중지") + "</button>" +
|
||||
"</span></div></div>";
|
||||
html += '<tr class="cf24-v-item" data-code="' + escHtml(v.variant_code) + '">' +
|
||||
'<td class="cf24-v-imgcol">' + (v.image ? '<img class="cf24-v-img" src="' + escHtml(v.image) + '" alt="" />' : '<span class="cf24-v-img cf24-v-img-empty"></span>') + "</td>" +
|
||||
'<td class="cf24-v-name">' + escHtml(label) + "</td>" +
|
||||
'<td class="cf24-v-syscode"><code>' + escHtml(v.variant_code) + "</code></td>" +
|
||||
'<td class="cf24-v-code"><input type="text" name="custom_variant_code" maxlength="40" value="' + escHtml(v.custom_variant_code) + '" /></td>' +
|
||||
'<td class="cf24-v-amount"><input type="text" name="additional_amount" inputmode="numeric" value="' + escHtml(amount) + '" /></td>' +
|
||||
'<td class="cf24-v-flag"><button type="button" class="cf24-v-toggle' + (v.display ? " is-on" : "") + '" data-flag="display" data-on="' + (v.display ? 1 : 0) + '" data-initial="' + (v.display ? 1 : 0) + '">' + (v.display ? "진열" : "미진열") + "</button></td>" +
|
||||
'<td class="cf24-v-flag"><button type="button" class="cf24-v-toggle' + (v.selling ? " is-on" : "") + '" data-flag="selling" data-on="' + (v.selling ? 1 : 0) + '" data-initial="' + (v.selling ? 1 : 0) + '">' + (v.selling ? "판매" : "중지") + "</button></td>" +
|
||||
"</tr>";
|
||||
});
|
||||
html += "</div>" +
|
||||
html += "</tbody></table></div>" +
|
||||
'<div class="cf24-side-actions" style="margin-top:8px;"><span class="cf24-muted" id="cf24-var-msg"></span>' +
|
||||
' <button type="button" class="erp-btn erp-btn-primary" id="cf24-var-save">품목 저장</button></div>';
|
||||
}
|
||||
html += '<div class="cf24-opt-danger"><button type="button" class="erp-btn erp-btn-outline" id="cf24-opt-delete">옵션 전체 삭제</button></div></div>';
|
||||
html += "</section></div>";
|
||||
|
||||
body.innerHTML = html;
|
||||
var optForm = body.querySelector("#cf24-opt-form");
|
||||
@@ -1191,6 +1248,7 @@
|
||||
var sidePane = document.getElementById("cf24-side-pane");
|
||||
|
||||
function select(no, push) {
|
||||
optModal.close(true); // 다른 상품으로 넘어가면 열려 있던 옵션 창은 무효
|
||||
pane.innerHTML = '<p class="cf24-muted" style="padding:16px;">불러오는 중…</p>';
|
||||
if (sidePane) sidePane.innerHTML = "";
|
||||
var url = "/cafe24/products/" + no + "/pane" + (listQuery ? "?" + listQuery : "");
|
||||
|
||||
Reference in New Issue
Block a user