|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
{% extends "erp_base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block head_extra %}
|
|
|
|
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260918d" />
|
|
|
|
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260918e" />
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
@@ -125,6 +125,7 @@
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block scripts %}
|
|
|
|
|
<script src="/static/cafe24-options.js?v=20260918e"></script>
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
var pane = document.getElementById("cf24-editor-pane");
|
|
|
|
@@ -913,355 +914,15 @@
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
// ── 옵션 / 품목 — 「팝업에서 관리」 버튼으로 모달을 열고 그때 불러온다 ──
|
|
|
|
|
(function setupOptions() {
|
|
|
|
|
var openBtn = root.querySelector("#cf24-options-open");
|
|
|
|
|
var body = optModal.body;
|
|
|
|
|
var count = root.querySelector("#cf24-options-count");
|
|
|
|
|
if (!openBtn || !body) return;
|
|
|
|
|
var state = { option: null, variants: [], displayTypes: {} };
|
|
|
|
|
|
|
|
|
|
openBtn.addEventListener("click", function () {
|
|
|
|
|
var nameEl = document.getElementById("cf24-name-text");
|
|
|
|
|
optModal.open((nameEl ? nameEl.textContent : "") + " (상품번호 " + no + ")");
|
|
|
|
|
load();
|
|
|
|
|
// ── 옵션 / 품목 — 모달 로직은 /static/cafe24-options.js 에 있다(크기 때문에 분리).
|
|
|
|
|
// 통합 목록(옵션값+품목 한 행) · 드래그 순서 · 옵션값 불러오기 · 저장 한 번에 처리.
|
|
|
|
|
if (window.cf24SetupOptions) {
|
|
|
|
|
window.cf24SetupOptions({
|
|
|
|
|
root: root, productNo: no, modal: optModal,
|
|
|
|
|
apiJson: apiJson, escHtml: escHtml, setMsg: setMsg,
|
|
|
|
|
trackDirty: trackDirty, commitValues: commitValues
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function load() {
|
|
|
|
|
body.innerHTML = '<p class="cf24-muted">카페24에서 불러오는 중…</p>';
|
|
|
|
|
apiJson("GET", "/cafe24/products/" + no + "/options")
|
|
|
|
|
.then(function (data) {
|
|
|
|
|
state.option = data.option;
|
|
|
|
|
state.variants = data.variants || [];
|
|
|
|
|
state.displayTypes = data.display_types || {};
|
|
|
|
|
render();
|
|
|
|
|
})
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
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", load);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function typeSelect(current, name) {
|
|
|
|
|
var out = '<select name="' + name + '">';
|
|
|
|
|
Object.keys(state.displayTypes).forEach(function (k) {
|
|
|
|
|
out += '<option value="' + k + '"' + (k === current ? " selected" : "") + ">" + escHtml(state.displayTypes[k]) + "</option>";
|
|
|
|
|
});
|
|
|
|
|
return out + "</select>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
|
var opt = state.option || { has_option: false, options: [] };
|
|
|
|
|
var variants = state.variants || [];
|
|
|
|
|
if (count) count.textContent = opt.has_option ? "(" + variants.length + "품목)" : "(옵션 없음)";
|
|
|
|
|
|
|
|
|
|
if (!opt.has_option) {
|
|
|
|
|
body.innerHTML =
|
|
|
|
|
'<p class="cf24-side-note">이 상품에는 옵션이 없습니다. 옵션명과 옵션값을 넣어 조합형 옵션을 만들면 품목이 자동으로 생성됩니다.</p>' +
|
|
|
|
|
'<form class="cf24-side-form cf24-opt-create" id="cf24-opt-create">' +
|
|
|
|
|
' <label class="cf24-side-field"><span>옵션명</span><input type="text" name="option_name" placeholder="예: 색상" /></label>' +
|
|
|
|
|
' <label class="cf24-side-field"><span>옵션값 (쉼표 또는 줄바꿈으로 구분)</span><textarea name="values" placeholder="예: 빨강, 파랑, 노랑"></textarea></label>' +
|
|
|
|
|
' <label class="cf24-side-field cf24-side-field-type"><span>표시방식</span>' + typeSelect("S", "display_type") + "</label>" +
|
|
|
|
|
' <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>";
|
|
|
|
|
var form = body.querySelector("#cf24-opt-create");
|
|
|
|
|
form.addEventListener("submit", function (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
var payload = {
|
|
|
|
|
option_name: form.querySelector('[name="option_name"]').value.trim(),
|
|
|
|
|
values: form.querySelector('[name="values"]').value,
|
|
|
|
|
display_type: form.querySelector('[name="display_type"]').value
|
|
|
|
|
};
|
|
|
|
|
if (!payload.option_name || !payload.values.trim()) {
|
|
|
|
|
window.erpAlert("옵션명과 옵션값을 입력하세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
window.erpConfirm("옵션 「" + payload.option_name + "」을(를) 만들고 품목을 생성합니다.\n카페24에 바로 반영됩니다. 계속할까요?")
|
|
|
|
|
.then(function (ok) {
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
var msg = form.querySelector("#cf24-opt-msg");
|
|
|
|
|
setMsg(msg, "생성 중…", "");
|
|
|
|
|
apiJson("POST", "/cafe24/products/" + no + "/options", payload)
|
|
|
|
|
.then(function (data) { state.option = data.option; state.variants = data.variants || []; render(); })
|
|
|
|
|
.catch(function (err) { setMsg(msg, "실패: " + err.message, "is-err"); });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 옵션값 썸네일은 세 곳 중 있는 것을 보여준다 — 옵션 버튼 이미지(option_image_file)
|
|
|
|
|
// → 옵션 연결 이미지(option_link_image) → 그 옵션값을 가진 품목의 image.
|
|
|
|
|
// 카페24 관리자에서 "옵션 연결 이미지"로만 등록한 상품은 option_image_file 이
|
|
|
|
|
// 비어 있고 품목 image 에만 나타난다(실물 — 버튼 이미지만 보면 "없음"으로 오인).
|
|
|
|
|
function variantImageFor(name, value) {
|
|
|
|
|
for (var i = 0; i < variants.length; i++) {
|
|
|
|
|
var v = variants[i];
|
|
|
|
|
if (!v.image) continue;
|
|
|
|
|
for (var j = 0; j < v.options.length; j++) {
|
|
|
|
|
if (v.options[j].name === name && v.options[j].value === value) return v.image;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 옵션 정의 편집 (모달 왼쪽 열)
|
|
|
|
|
var html = '<div class="cf24-modal-grid"><section class="cf24-modal-col cf24-modal-col-options">' +
|
|
|
|
|
'<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">' +
|
|
|
|
|
' <label class="cf24-side-field"><span>옵션명</span><input type="text" name="option_name" value="' + escHtml(o.option_name) + '" /></label>' +
|
|
|
|
|
' <label class="cf24-side-field cf24-side-field-type"><span>표시방식</span>' + typeSelect(o.option_display_type || "S", "option_display_type") + "</label>" +
|
|
|
|
|
"</div>";
|
|
|
|
|
o.option_value.forEach(function (v, vi) {
|
|
|
|
|
var thumbSrc = v.option_image_file || v.option_link_image || variantImageFor(o.option_name, v.option_text);
|
|
|
|
|
var thumb = thumbSrc
|
|
|
|
|
? '<img class="cf24-opt-thumb" src="' + escHtml(thumbSrc) + '" alt="" title="' +
|
|
|
|
|
(v.option_image_file ? "옵션 버튼 이미지" : v.option_link_image ? "옵션 연결 이미지" : "품목 이미지") + '" />'
|
|
|
|
|
: '<div class="cf24-opt-thumb-empty">없음</div>';
|
|
|
|
|
html += '<div class="cf24-opt-value" data-value="' + vi + '">' +
|
|
|
|
|
'<span class="cf24-opt-thumb-slot">' + thumb + "</span>" +
|
|
|
|
|
'<input type="text" name="option_text" value="' + escHtml(v.option_text) + '" placeholder="옵션값 이름" />' +
|
|
|
|
|
'<input type="hidden" name="option_image_file" value="' + escHtml(v.option_image_file) + '" />' +
|
|
|
|
|
'<input type="hidden" name="option_link_image" value="' + escHtml(v.option_link_image) + '" />' +
|
|
|
|
|
'<label class="cf24-file-pick"><input type="file" accept="image/jpeg,image/png,image/gif,image/webp" />' +
|
|
|
|
|
'<span class="erp-btn erp-btn-outline">썸네일</span></label>' +
|
|
|
|
|
"</div>";
|
|
|
|
|
});
|
|
|
|
|
html += "</div>";
|
|
|
|
|
});
|
|
|
|
|
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>" +
|
|
|
|
|
'<div class="cf24-opt-danger"><button type="button" class="erp-btn erp-btn-outline" id="cf24-opt-delete">옵션 전체 삭제</button></div>' +
|
|
|
|
|
"</section>";
|
|
|
|
|
|
|
|
|
|
// 품목 표 (모달 오른쪽 열) — 넓은 창이라 한 줄에 다 들어간다.
|
|
|
|
|
html += '<section class="cf24-modal-col cf24-modal-col-variants"><h4 class="cf24-side-title">품목 (' + variants.length + ')</h4>';
|
|
|
|
|
if (!variants.length) {
|
|
|
|
|
html += '<p class="cf24-muted">품목이 아직 조회되지 않았습니다. 잠시 뒤 창을 닫았다 다시 여세요.</p>';
|
|
|
|
|
} else {
|
|
|
|
|
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 += '<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" title="' + escHtml(label) + '">' + escHtml(label) + "</td>" +
|
|
|
|
|
'<td class="cf24-v-syscode"><button type="button" class="cf24-v-copy" data-copy-text="' + escHtml(v.variant_code) + '" title="클릭하면 품목코드를 복사합니다">' + escHtml(v.variant_code) + "</button></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 += "</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 += "</section></div>";
|
|
|
|
|
|
|
|
|
|
body.innerHTML = html;
|
|
|
|
|
var optForm = body.querySelector("#cf24-opt-form");
|
|
|
|
|
trackDirty(optForm);
|
|
|
|
|
bindOptionImages(optForm);
|
|
|
|
|
optForm.addEventListener("submit", function (e) { e.preventDefault(); saveOptions(optForm); });
|
|
|
|
|
bindVariants();
|
|
|
|
|
body.querySelector("#cf24-opt-delete").addEventListener("click", deleteOptions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindOptionImages(optForm) {
|
|
|
|
|
optForm.querySelectorAll(".cf24-opt-value").forEach(function (row) {
|
|
|
|
|
var fileEl = row.querySelector('input[type="file"]');
|
|
|
|
|
fileEl.addEventListener("change", function () {
|
|
|
|
|
var f = fileEl.files && fileEl.files[0];
|
|
|
|
|
if (!f) return;
|
|
|
|
|
var slot = row.querySelector(".cf24-opt-thumb-slot");
|
|
|
|
|
var msg = optForm.querySelector("#cf24-opt-msg");
|
|
|
|
|
setMsg(msg, "썸네일 업로드 중…", "");
|
|
|
|
|
var fd = new FormData();
|
|
|
|
|
fd.append("file", f);
|
|
|
|
|
apiJson("POST", "/cafe24/products/" + no + "/options/image", fd)
|
|
|
|
|
.then(function (data) {
|
|
|
|
|
row.querySelector('[name="option_image_file"]').value = data.path;
|
|
|
|
|
row.querySelector('[name="option_link_image"]').value = data.path;
|
|
|
|
|
slot.innerHTML = '<img class="cf24-opt-thumb is-dirty" src="' + escHtml(data.path) + '" alt="" />';
|
|
|
|
|
setMsg(msg, "썸네일을 올렸습니다. 「옵션 저장」을 눌러야 카페24에 반영됩니다.", "is-ok");
|
|
|
|
|
})
|
|
|
|
|
.catch(function (err) { setMsg(msg, "썸네일 업로드 실패: " + err.message, "is-err"); })
|
|
|
|
|
.then(function () { fileEl.value = ""; });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveOptions(optForm) {
|
|
|
|
|
var msg = optForm.querySelector("#cf24-opt-msg");
|
|
|
|
|
var edited = [];
|
|
|
|
|
var changed = false;
|
|
|
|
|
optForm.querySelectorAll(".cf24-opt-group[data-group]").forEach(function (g, gi) {
|
|
|
|
|
var orig = state.option.options[gi];
|
|
|
|
|
var entry = {
|
|
|
|
|
option_name: g.querySelector('[name="option_name"]').value.trim(),
|
|
|
|
|
option_display_type: g.querySelector('[name="option_display_type"]').value,
|
|
|
|
|
option_value: []
|
|
|
|
|
};
|
|
|
|
|
if (entry.option_name !== orig.option_name || entry.option_display_type !== (orig.option_display_type || "S")) changed = true;
|
|
|
|
|
g.querySelectorAll(".cf24-opt-value").forEach(function (row, vi) {
|
|
|
|
|
var ov = orig.option_value[vi] || {};
|
|
|
|
|
var item = {
|
|
|
|
|
option_text: row.querySelector('[name="option_text"]').value.trim(),
|
|
|
|
|
option_image_file: row.querySelector('[name="option_image_file"]').value,
|
|
|
|
|
option_link_image: row.querySelector('[name="option_link_image"]').value,
|
|
|
|
|
option_color: ov.option_color || ""
|
|
|
|
|
};
|
|
|
|
|
if (item.option_text !== ov.option_text || item.option_image_file !== (ov.option_image_file || "") ||
|
|
|
|
|
item.option_link_image !== (ov.option_link_image || "")) changed = true;
|
|
|
|
|
entry.option_value.push(item);
|
|
|
|
|
});
|
|
|
|
|
edited.push(entry);
|
|
|
|
|
});
|
|
|
|
|
if (!changed) { setMsg(msg, "바뀐 값이 없습니다.", ""); return; }
|
|
|
|
|
window.erpConfirm("옵션명·옵션값을 카페24에 바로 반영합니다. 계속할까요?").then(function (ok) {
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
setMsg(msg, "저장 중…", "");
|
|
|
|
|
apiJson("PUT", "/cafe24/products/" + no + "/options", { options: edited, option_list_type: state.option.option_list_type })
|
|
|
|
|
.then(function (data) {
|
|
|
|
|
state.option = data.option;
|
|
|
|
|
state.variants = data.variants || state.variants;
|
|
|
|
|
render();
|
|
|
|
|
setMsg(body.querySelector("#cf24-opt-msg"), "카페24에 반영했습니다.", "is-ok");
|
|
|
|
|
})
|
|
|
|
|
.catch(function (err) { setMsg(msg, "실패: " + err.message, "is-err"); });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindVariants() {
|
|
|
|
|
var saveBtn = body.querySelector("#cf24-var-save");
|
|
|
|
|
if (!saveBtn) return;
|
|
|
|
|
var wrap = body.querySelector(".cf24-variants-wrap");
|
|
|
|
|
trackDirty(wrap);
|
|
|
|
|
// 품목코드 클릭 = 클립보드 복사 (HTTPS 가 아니면 execCommand 로 대체)
|
|
|
|
|
wrap.querySelectorAll(".cf24-v-copy").forEach(function (btn) {
|
|
|
|
|
btn.addEventListener("click", function () {
|
|
|
|
|
var text = btn.dataset.copyText || btn.textContent;
|
|
|
|
|
var done = function () {
|
|
|
|
|
var old = btn.textContent;
|
|
|
|
|
btn.textContent = "복사됨";
|
|
|
|
|
btn.classList.add("is-copied");
|
|
|
|
|
setTimeout(function () { btn.textContent = old; btn.classList.remove("is-copied"); }, 1200);
|
|
|
|
|
};
|
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
|
|
|
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text, done); });
|
|
|
|
|
} else {
|
|
|
|
|
fallbackCopy(text, done);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
function fallbackCopy(text, done) {
|
|
|
|
|
var ta = document.createElement("textarea");
|
|
|
|
|
ta.value = text;
|
|
|
|
|
ta.setAttribute("readonly", "");
|
|
|
|
|
ta.style.position = "fixed";
|
|
|
|
|
ta.style.opacity = "0";
|
|
|
|
|
document.body.appendChild(ta);
|
|
|
|
|
ta.select();
|
|
|
|
|
try { if (document.execCommand("copy")) done(); } catch (e) { /* 복사 불가 — 조용히 무시 */ }
|
|
|
|
|
document.body.removeChild(ta);
|
|
|
|
|
}
|
|
|
|
|
wrap.querySelectorAll(".cf24-v-toggle").forEach(function (btn) {
|
|
|
|
|
btn.addEventListener("click", function () {
|
|
|
|
|
var on = btn.dataset.on !== "1";
|
|
|
|
|
btn.dataset.on = on ? "1" : "0";
|
|
|
|
|
btn.classList.toggle("is-on", on);
|
|
|
|
|
btn.textContent = btn.dataset.flag === "display" ? (on ? "진열" : "미진열") : (on ? "판매" : "중지");
|
|
|
|
|
btn.classList.toggle("is-dirty", btn.dataset.on !== btn.dataset.initial);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
saveBtn.addEventListener("click", function () {
|
|
|
|
|
var msg = body.querySelector("#cf24-var-msg");
|
|
|
|
|
var rows = [];
|
|
|
|
|
var lines = [];
|
|
|
|
|
wrap.querySelectorAll(".cf24-v-item[data-code]").forEach(function (tr) {
|
|
|
|
|
var row = { variant_code: tr.dataset.code };
|
|
|
|
|
var any = false;
|
|
|
|
|
var code = tr.querySelector('[name="custom_variant_code"]');
|
|
|
|
|
var amount = tr.querySelector('[name="additional_amount"]');
|
|
|
|
|
if (code.value !== code.defaultValue) { row.custom_variant_code = code.value.trim(); any = true; }
|
|
|
|
|
if (amount.value !== amount.defaultValue) { row.additional_amount = amount.value.trim() || "0"; any = true; }
|
|
|
|
|
tr.querySelectorAll(".cf24-v-toggle").forEach(function (btn) {
|
|
|
|
|
if (btn.dataset.on !== btn.dataset.initial) { row[btn.dataset.flag] = btn.dataset.on === "1" ? "on" : "off"; any = true; }
|
|
|
|
|
});
|
|
|
|
|
if (any) {
|
|
|
|
|
rows.push(row);
|
|
|
|
|
lines.push(tr.querySelector(".cf24-v-name").textContent.trim());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (!rows.length) { setMsg(msg, "바뀐 품목이 없습니다.", ""); return; }
|
|
|
|
|
window.erpConfirm("품목 " + rows.length + "건을 카페24에 바로 반영합니다. 계속할까요?\n\n" + lines.slice(0, 8).join("\n") + (lines.length > 8 ? "\n…" : ""))
|
|
|
|
|
.then(function (ok) {
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
saveBtn.disabled = true;
|
|
|
|
|
setMsg(msg, "저장 중…", "");
|
|
|
|
|
apiJson("PUT", "/cafe24/products/" + no + "/variants", { rows: rows })
|
|
|
|
|
.then(function (data) {
|
|
|
|
|
var updated = data.updated || {};
|
|
|
|
|
Object.keys(updated).forEach(function (code) {
|
|
|
|
|
var u = updated[code];
|
|
|
|
|
var tr = wrap.querySelector('.cf24-v-item[data-code="' + code + '"]');
|
|
|
|
|
if (!tr) return;
|
|
|
|
|
var codeEl = tr.querySelector('[name="custom_variant_code"]');
|
|
|
|
|
var amountEl = tr.querySelector('[name="additional_amount"]');
|
|
|
|
|
if (u.custom_variant_code !== undefined) codeEl.value = u.custom_variant_code;
|
|
|
|
|
if (u.additional_amount !== undefined) amountEl.value = String(parseInt(u.additional_amount, 10) || 0);
|
|
|
|
|
tr.querySelectorAll(".cf24-v-toggle").forEach(function (btn) {
|
|
|
|
|
var flag = btn.dataset.flag;
|
|
|
|
|
if (u[flag] === undefined) return;
|
|
|
|
|
var on = !!u[flag];
|
|
|
|
|
btn.dataset.on = on ? "1" : "0";
|
|
|
|
|
btn.dataset.initial = btn.dataset.on;
|
|
|
|
|
btn.classList.toggle("is-on", on);
|
|
|
|
|
btn.classList.remove("is-dirty");
|
|
|
|
|
btn.textContent = flag === "display" ? (on ? "진열" : "미진열") : (on ? "판매" : "중지");
|
|
|
|
|
});
|
|
|
|
|
// 상태 보관 — 접었다 펴도 응답값이 유지되게
|
|
|
|
|
state.variants.forEach(function (v) {
|
|
|
|
|
if (v.variant_code !== code) return;
|
|
|
|
|
if (u.custom_variant_code !== undefined) v.custom_variant_code = u.custom_variant_code;
|
|
|
|
|
if (u.additional_amount !== undefined) v.additional_amount = u.additional_amount;
|
|
|
|
|
if (u.display !== undefined) v.display = !!u.display;
|
|
|
|
|
if (u.selling !== undefined) v.selling = !!u.selling;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
commitValues(wrap);
|
|
|
|
|
setMsg(msg, "카페24에 반영했습니다.", "is-ok");
|
|
|
|
|
})
|
|
|
|
|
.catch(function (err) { setMsg(msg, "실패: " + err.message, "is-err"); })
|
|
|
|
|
.then(function () { saveBtn.disabled = false; });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteOptions() {
|
|
|
|
|
window.erpConfirm("옵션을 전부 삭제합니다. 카페24가 이 상품의 **품목도 모두 삭제**하며 되돌릴 수 없습니다.\n정말 삭제할까요?")
|
|
|
|
|
.then(function (ok) {
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
body.innerHTML = '<p class="cf24-muted">삭제 중…</p>';
|
|
|
|
|
apiJson("DELETE", "/cafe24/products/" + no + "/options")
|
|
|
|
|
.then(function (data) { state.option = data.option; state.variants = []; render(); })
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
body.innerHTML = '<p class="cf24-err">삭제 실패: ' + escHtml(err.message) + "</p>";
|
|
|
|
|
loaded = false;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 공용 확인창(erp-dialog.js)은 비동기다 — 기본 confirm() 과 달리 Promise 를
|
|
|
|
|