/* 카페24 상품관리 — 옵션/품목 모달 (products.html 에서 window.cf24SetupOptions 로 호출). 통합 목록: 옵션이 1개(옵션명 하나)인 상품은 옵션값과 품목이 1:1 이므로 한 행에 썸네일 · 옵션값 이름 · 품목코드 · 자체 품목코드 · 추가금액 · 진열 · 판매 를 놓고 「저장」 한 번으로 옵션(PUT options) → 품목(PUT variants) 순서로 반영한다. 옵션이 없는 상품: 행을 자유롭게 추가/삭제해 두고 저장하면 옵션 생성(POST options, 카페24가 품목을 자동 생성) → 부여된 품목코드를 받아 → 자체코드/추가금액을 품목에 PUT → 썸네일이 있으면 옵션 PUT. 저장 뒤 다시 읽어 품목코드가 바로 보인다. 옵션이 2개 이상(조합)인 상품: 옵션값과 품목이 1:1 이 아니므로 예전 2열 화면 (왼쪽 옵션 정의 / 오른쪽 품목 표)을 그대로 쓴다. 순서 드래그: 품목의 display_order(카페24 문서 — 조합형 품목 진열순서)로 저장한다. 옵션값 배열 자체를 재배열해 PUT 하지 않는다 — 카페24는 original_options 와 options 를 **위치**로 짝지어 이름을 바꾸므로, 재배열하면 품목코드와 이름이 뒤바뀔 위험이 있다(품목코드는 주문 이력이 참조한다). 옵션값 불러오기: 탭/공백으로 나뉜 텍스트(이름 · 가격 · 추가금액 · 자체코드)를 붙여넣으면 행에 채운다. 가격 열은 무시한다. 화면은 언제나 **응답값**으로 다시 그린다(카페24 GET 은 쓰기 직후 예전 값을 돌려줄 수 있어, 서버가 스냅샷으로 보정한다). */ (function () { "use strict"; var DEFAULT_TYPE = "S"; window.cf24SetupOptions = function (ctx) { var root = ctx.root, no = ctx.productNo, modal = ctx.modal; var apiJson = ctx.apiJson, esc = ctx.escHtml, setMsg = ctx.setMsg; var trackDirty = ctx.trackDirty, commitValues = ctx.commitValues; var openBtn = root.querySelector("#cf24-options-open"); var body = modal.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"); modal.open((nameEl ? nameEl.textContent : "") + " (상품번호 " + no + ")"); load(); }); function load() { body.innerHTML = '

카페24에서 불러오는 중…

'; return 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 = '

옵션을 불러오지 못했습니다: ' + esc(err.message) + '

'; body.querySelector("#cf24-opt-retry").addEventListener("click", load); }); } /* ── 공통 조각 ── */ function typeSelect(current, name) { var out = '"; } function toggleBtn(flag, on) { var label = flag === "display" ? (on ? "진열" : "미진열") : (on ? "판매" : "중지"); return '"; } function paintToggle(btn, on, resetInitial) { btn.dataset.on = on ? "1" : "0"; if (resetInitial) btn.dataset.initial = btn.dataset.on; btn.classList.toggle("is-on", on); btn.classList.toggle("is-dirty", btn.dataset.on !== btn.dataset.initial); btn.textContent = btn.dataset.flag === "display" ? (on ? "진열" : "미진열") : (on ? "판매" : "중지"); } function bindToggles(container) { container.querySelectorAll(".cf24-v-toggle").forEach(function (btn) { btn.addEventListener("click", function () { paintToggle(btn, btn.dataset.on !== "1", false); }); }); } function copyText(text, done) { function fallback() { 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); } if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(done, fallback); } else { fallback(); } } function bindCopy(container) { container.querySelectorAll(".cf24-v-copy").forEach(function (btn) { btn.addEventListener("click", function () { copyText(btn.dataset.copyText || btn.textContent, function () { var old = btn.textContent; btn.textContent = "복사됨"; btn.classList.add("is-copied"); setTimeout(function () { btn.textContent = old; btn.classList.remove("is-copied"); }, 1200); }); }); }); } // 옵션값 썸네일 — 버튼 이미지 → 연결 이미지 → 그 옵션값을 가진 품목 image function variantImageFor(name, value) { for (var i = 0; i < state.variants.length; i++) { var v = state.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 ""; } // 카페24가 경로는 주지만 파일이 없는 경우가 있다(깨진 그림) — 실패하면 "없음"으로 바꾼다. var THUMB_EMPTY = '
없음
'; function thumbHtml(src, title) { return src ? '' : THUMB_EMPTY; } // 썸네일 파일 선택 → 업로드 → hidden 두 개(option_image_file, option_link_image)에 경로 function bindThumbUploads(container, msgEl) { container.querySelectorAll(".cf24-opt-value, .cf24-u-row").forEach(function (row) { var fileEl = row.querySelector('input[type="file"]'); if (!fileEl) return; fileEl.addEventListener("change", function () { var f = fileEl.files && fileEl.files[0]; if (!f) return; var slot = row.querySelector(".cf24-opt-thumb-slot"); setMsg(msgEl, "썸네일 업로드 중…", ""); 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 = ''; row.classList.add("is-image-dirty"); setMsg(msgEl, "썸네일을 올렸습니다. 「저장」을 눌러야 카페24에 반영됩니다.", "is-ok"); }) .catch(function (err) { setMsg(msgEl, "썸네일 업로드 실패: " + err.message, "is-err"); }) .then(function () { fileEl.value = ""; }); }); }); } function amountPlain(v) { return String(parseInt(String(v || "0").replace(/,/g, ""), 10) || 0); } /* ═══════════════════════════════════════════════════════════ 화면 분기 ═══════════════════════════════════════════════════════════ */ function render() { var opt = state.option || { has_option: false, options: [] }; if (count) count.textContent = opt.has_option ? "(" + state.variants.length + "품목)" : "(옵션 없음)"; if (!opt.has_option) return renderUnified(true); if (opt.options.length === 1) return renderUnified(false); return renderMulti(); } /* ═══════════════════════════════════════════════════════════ 통합 목록 — 옵션값 1 : 품목 1 ═══════════════════════════════════════════════════════════ */ // 행 순서는 **품목 목록 순서**(카페24가 진열순서대로 돌려준다 — 쇼핑몰에 보이는 순서)를 // 따른다. options 응답의 옵션값 순서는 등록 순서라 실제와 다를 수 있다(실물 확인). // index 는 options 응답 안 위치 — 옵션 PUT 은 이 위치로 짝을 맞춰야 하므로 유지한다. function unifiedRows() { var opt = state.option; if (!opt || !opt.has_option) return []; var group = opt.options[0]; var byText = {}; group.option_value.forEach(function (v, vi) { byText[v.option_text] = { index: vi, value: v, variant: null }; }); var rows = []; state.variants.forEach(function (variant) { if (!variant.options.length) return; var r = byText[variant.options[0].value]; if (!r || r.variant) return; // 옵션값이 없거나 이미 배정됨 r.variant = variant; rows.push(r); }); group.option_value.forEach(function (v) { var r = byText[v.option_text]; if (rows.indexOf(r) < 0) rows.push(r); // 품목이 안 잡힌 옵션값은 뒤에 }); return rows; } function rowHtml(r, isNew, canDrag, position) { var v = r.value || { option_text: "", option_image_file: "", option_link_image: "", option_color: "" }; var variant = r.variant; var thumbSrc = v.option_image_file || v.option_link_image || (state.option && state.option.has_option ? variantImageFor(state.option.options[0].option_name, v.option_text) : ""); var code = variant ? variant.variant_code : ""; // data-index: options 응답 안 위치(옵션 PUT 짝맞춤용) / data-pos: 처음 그린 순서(순서 변경 판정용) return '' + '' + (canDrag ? '⋮⋮' : "") + "" + '' + thumbHtml(thumbSrc, v.option_image_file ? "옵션 버튼 이미지" : v.option_link_image ? "옵션 연결 이미지" : "품목 이미지") + "" + '' + '' + '' + '' + '' + (code ? '" : '' + (isNew ? "저장 시 부여" : "—") + "") + "" + '' + '' + '' + toggleBtn("display", variant ? !!variant.display : true) + "" + '' + toggleBtn("selling", variant ? !!variant.selling : true) + "" + '' + (isNew ? '' : "") + "" + ""; } function renderUnified(isNew) { var opt = state.option; var group = isNew ? { option_name: "", option_display_type: DEFAULT_TYPE } : opt.options[0]; var rows = isNew ? [] : unifiedRows(); // 순서 변경은 조합형(T) 품목의 display_order 로만 저장할 수 있다. var canDrag = isNew || opt.option_type === "T"; var html = '
' + '
' + ' ' + ' " + ' ' + ' ' + (isNew ? ' ' : "") + " " + "
" + '
' + '' + '' + ""; rows.forEach(function (r, i) { html += rowHtml(r, false, canDrag, i); }); if (isNew) html += rowHtml({ index: null, value: null, variant: null }, true, true); html += "
썸네일옵션값 이름품목코드자체 품목코드추가금액진열판매
" + '

' + (isNew ? "저장하면 옵션(조합형)을 만들고 카페24가 품목을 자동 생성합니다. 부여된 품목코드를 받아 자체 품목코드·추가금액·썸네일까지 함께 반영합니다." : "옵션값 추가·삭제는 카페24 API 가 지원하지 않습니다(옵션 전체 삭제 후 다시 만들어야 합니다). " + (canDrag ? "행을 드래그하면 품목 진열순서로 저장됩니다. " : "") + "썸네일은 표시방식이 「미리보기(이미지)」일 때 쇼핑몰에 보입니다.") + "

" + '
' + ' ' + (isNew ? "" : ' ') + '
' + "
"; body.innerHTML = html; var wrap = body.querySelector(".cf24-u-wrap"); var tbody = body.querySelector("#cf24-u-body"); var msg = body.querySelector("#cf24-u-msg"); trackDirty(wrap); bindToggles(tbody); bindCopy(tbody); bindThumbUploads(tbody, msg); if (canDrag) bindDrag(tbody); if (isNew) { body.querySelector("#cf24-u-add").addEventListener("click", function () { tbody.insertAdjacentHTML("beforeend", rowHtml({ index: null, value: null, variant: null }, true, true)); var tr = tbody.lastElementChild; bindRowNew(tr, tbody, msg); tr.querySelector('[name="option_text"]').focus(); }); tbody.querySelectorAll("tr").forEach(function (tr) { bindRowNew(tr, tbody, msg); }); } body.querySelector("#cf24-u-import").addEventListener("click", function () { openImport(isNew, tbody, msg); }); body.querySelector("#cf24-u-save").addEventListener("click", function () { saveUnified(isNew, tbody, msg); }); var del = body.querySelector("#cf24-opt-delete"); if (del) del.addEventListener("click", deleteOptions); } function bindRowNew(tr, tbody, msg) { var rm = tr.querySelector(".cf24-u-remove"); if (rm && !rm.dataset.bound) { rm.dataset.bound = "1"; rm.addEventListener("click", function () { if (tbody.children.length <= 1) { tr.querySelector('[name="option_text"]').value = ""; return; } tr.remove(); }); } trackDirty(tr); bindToggles(tr); bindThumbUploads(tr, msg); } /* ── 드래그 정렬 (HTML5 DnD, 핸들에서만 시작) ── */ function bindDrag(tbody) { var dragging = null; tbody.querySelectorAll(".cf24-drag-handle").forEach(function (h) { h.addEventListener("dragstart", function (e) { dragging = h.closest("tr"); dragging.classList.add("is-dragging"); e.dataTransfer.effectAllowed = "move"; try { e.dataTransfer.setData("text/plain", dragging.dataset.code || "row"); } catch (err) { /* IE */ } }); h.addEventListener("dragend", function () { if (dragging) dragging.classList.remove("is-dragging"); dragging = null; markOrder(tbody); }); }); tbody.addEventListener("dragover", function (e) { if (!dragging) return; e.preventDefault(); var over = e.target.closest("tr"); if (!over || over === dragging) return; var rect = over.getBoundingClientRect(); var after = (e.clientY - rect.top) > rect.height / 2; tbody.insertBefore(dragging, after ? over.nextSibling : over); }); tbody.addEventListener("drop", function (e) { e.preventDefault(); }); } // 순서가 처음과 달라졌는지 표시(저장 시 display_order 전송 여부) function markOrder(tbody) { var changed = false; Array.prototype.forEach.call(tbody.children, function (tr, i) { var orig = tr.dataset.pos === "" || tr.dataset.pos == null ? null : parseInt(tr.dataset.pos, 10); if (orig !== null && orig !== i) changed = true; }); tbody.classList.toggle("is-order-dirty", changed); Array.prototype.forEach.call(tbody.children, function (tr) { var h = tr.querySelector(".cf24-drag-handle"); if (h) h.classList.toggle("is-dirty", changed); }); } /* ── 옵션값 불러오기 (텍스트 붙여넣기) ── */ function parseImport(text) { var out = []; String(text || "").split(/\r?\n/).forEach(function (line) { if (!line.trim()) return; var cols = line.indexOf("\t") >= 0 ? line.split("\t") : line.split(/\s{2,}/); cols = cols.map(function (c) { return c.trim(); }); while (cols.length && !cols[cols.length - 1]) cols.pop(); if (!cols.length) return; var name = cols.shift(); if (!name) return; var code = ""; if (cols.length && /[A-Za-z가-힣]/.test(cols[cols.length - 1])) code = cols.pop(); var nums = cols.map(function (c) { var t = c.replace(/[,\s원]/g, ""); if (t === "-" || t === "") return 0; var n = parseInt(t, 10); return isNaN(n) ? null : n; }).filter(function (n) { return n !== null; }); // 열이 2개 이상이면 첫 번째는 판매가(무시), 마지막이 추가금액 var amount = nums.length >= 2 ? nums[nums.length - 1] : (nums.length === 1 ? nums[0] : null); out.push({ name: name, amount: amount, code: code }); }); return out; } function openImport(isNew, tbody, msg) { var dlg = document.createElement("div"); dlg.className = "cf24-modal cf24-import"; dlg.innerHTML = '"; document.body.appendChild(dlg); var ta = dlg.querySelector("textarea"); var preview = dlg.querySelector(".cf24-import-preview"); function close() { dlg.remove(); } dlg.querySelector(".cf24-modal-close").addEventListener("click", close); dlg.querySelector(".cf24-import-cancel").addEventListener("click", close); dlg.addEventListener("click", function (e) { if (e.target === dlg) close(); }); ta.addEventListener("input", function () { var parsed = parseImport(ta.value); preview.textContent = parsed.length ? parsed.length + "행 인식 — 예: " + parsed[0].name + " / 추가금액 " + (parsed[0].amount == null ? "(없음)" : parsed[0].amount.toLocaleString("ko-KR")) + " / 코드 " + (parsed[0].code || "(없음)") : ""; }); dlg.querySelector(".cf24-import-apply").addEventListener("click", function () { var parsed = parseImport(ta.value); if (!parsed.length) { window.erpAlert("인식된 줄이 없습니다."); return; } applyImport(parsed, isNew, tbody, msg); close(); }); ta.focus(); } function setRowValues(tr, item) { var nameEl = tr.querySelector('[name="option_text"]'); var codeEl = tr.querySelector('[name="custom_variant_code"]'); var amtEl = tr.querySelector('[name="additional_amount"]'); if (item.name != null && nameEl) { nameEl.value = item.name; nameEl.dispatchEvent(new Event("input")); } if (item.code && codeEl) { codeEl.value = item.code; codeEl.dispatchEvent(new Event("input")); } if (item.amount != null && amtEl) { amtEl.value = String(item.amount); amtEl.dispatchEvent(new Event("input")); } } function norm(s) { return String(s || "").replace(/\s+/g, "").toLowerCase(); } function applyImport(parsed, isNew, tbody, msg) { if (isNew) { // 비어 있는 행만 있으면 교체, 내용이 있으면 뒤에 덧붙인다 var blank = Array.prototype.every.call(tbody.children, function (tr) { return !tr.querySelector('[name="option_text"]').value.trim(); }); if (blank) tbody.innerHTML = ""; parsed.forEach(function (item) { tbody.insertAdjacentHTML("beforeend", rowHtml({ index: null, value: null, variant: null }, true, true)); var tr = tbody.lastElementChild; bindRowNew(tr, tbody, msg); setRowValues(tr, item); }); setMsg(msg, parsed.length + "행을 채웠습니다. 「저장」을 누르면 카페24에 반영됩니다.", "is-ok"); return; } var rows = Array.prototype.slice.call(tbody.children); var matched = 0, missing = []; parsed.forEach(function (item) { var tr = null; for (var i = 0; i < rows.length; i++) { if (norm(rows[i].querySelector('[name="option_text"]').value) === norm(item.name)) { tr = rows[i]; break; } } if (!tr) { missing.push(item.name); return; } setRowValues(tr, { code: item.code, amount: item.amount }); matched++; }); var text = matched + "행에 자체코드·추가금액을 채웠습니다."; if (missing.length) text += " 일치하는 옵션값이 없어 건너뜀: " + missing.join(", ") + " (옵션값 추가는 API 미지원)"; setMsg(msg, text, missing.length ? "is-err" : "is-ok"); } /* ── 저장 (통합) ── */ function collectUnified(tbody) { return Array.prototype.map.call(tbody.children, function (tr, i) { return { tr: tr, position: i, index: tr.dataset.index === "" ? null : parseInt(tr.dataset.index, 10), code: tr.dataset.code || "", text: tr.querySelector('[name="option_text"]').value.trim(), image: tr.querySelector('[name="option_image_file"]').value, link: tr.querySelector('[name="option_link_image"]').value, custom: tr.querySelector('[name="custom_variant_code"]'), amount: tr.querySelector('[name="additional_amount"]'), display: tr.querySelector('.cf24-v-toggle[data-flag="display"]'), selling: tr.querySelector('.cf24-v-toggle[data-flag="selling"]') }; }); } function validateNames(rows) { var seen = {}; for (var i = 0; i < rows.length; i++) { if (!rows[i].text) return "옵션값 이름이 비어 있는 행이 있습니다."; if (seen[rows[i].text]) return "옵션값 이름이 중복됩니다: " + rows[i].text; seen[rows[i].text] = true; } return ""; } function variantPatch(r, withOrder) { var patch = { variant_code: r.code }; var any = false; if (r.custom.value !== r.custom.defaultValue) { patch.custom_variant_code = r.custom.value.trim(); any = true; } if (r.amount.value !== r.amount.defaultValue) { patch.additional_amount = r.amount.value.trim() || "0"; any = true; } if (r.display.dataset.on !== r.display.dataset.initial) { patch.display = r.display.dataset.on === "1" ? "on" : "off"; any = true; } if (r.selling.dataset.on !== r.selling.dataset.initial) { patch.selling = r.selling.dataset.on === "1" ? "on" : "off"; any = true; } if (withOrder) { patch.display_order = r.position + 1; any = true; } return any ? patch : null; } function saveUnified(isNew, tbody, msg) { var rows = collectUnified(tbody); var problem = validateNames(rows); if (problem) { window.erpAlert(problem); return; } var optName = body.querySelector('[name="option_name"]').value.trim(); var optType = body.querySelector('[name="option_display_type"]').value; if (!optName) { window.erpAlert("옵션명을 입력하세요."); return; } var saveBtn = body.querySelector("#cf24-u-save"); if (isNew) { window.erpConfirm("옵션 「" + optName + "」과 옵션값 " + rows.length + "개를 만들고 품목을 생성합니다.\n카페24에 바로 반영됩니다. 계속할까요?") .then(function (ok) { if (ok) createFlow(); }); return; } // 기존 옵션 — 옵션(PUT) 은 **원래 순서**로 짝을 맞춰 보낸다(재배열은 display_order 로). var group = state.option.options[0]; var optionsChanged = optName !== group.option_name || optType !== (group.option_display_type || DEFAULT_TYPE); var edited = group.option_value.map(function (ov, vi) { var r = null; for (var i = 0; i < rows.length; i++) if (rows[i].index === vi) { r = rows[i]; break; } if (!r) return { option_text: ov.option_text, option_image_file: ov.option_image_file, option_link_image: ov.option_link_image, option_color: ov.option_color }; if (r.text !== ov.option_text || r.image !== (ov.option_image_file || "") || r.link !== (ov.option_link_image || "")) optionsChanged = true; return { option_text: r.text, option_image_file: r.image, option_link_image: r.link, option_color: ov.option_color || "" }; }); var orderChanged = tbody.classList.contains("is-order-dirty"); var patches = []; rows.forEach(function (r) { if (!r.code) return; var p = variantPatch(r, orderChanged); if (p) patches.push(p); }); if (!optionsChanged && !patches.length) { setMsg(msg, "바뀐 값이 없습니다.", ""); return; } var lines = []; if (optionsChanged) lines.push("옵션명·옵션값 이름/썸네일/표시방식"); if (patches.length) lines.push("품목 " + patches.length + "건 (자체코드·추가금액·진열/판매" + (orderChanged ? "·순서" : "") + ")"); window.erpConfirm("카페24에 바로 반영합니다. 계속할까요?\n\n- " + lines.join("\n- ")).then(function (ok) { if (!ok) return; saveBtn.disabled = true; setMsg(msg, "저장 중…", ""); var chain = Promise.resolve(); if (optionsChanged) { chain = chain.then(function () { return apiJson("PUT", "/cafe24/products/" + no + "/options", { options: [{ option_name: optName, option_display_type: optType, option_value: edited }], option_list_type: state.option.option_list_type }).then(function (data) { state.option = data.option; if (data.variants) state.variants = data.variants; }); }); } if (patches.length) { chain = chain.then(function () { return apiJson("PUT", "/cafe24/products/" + no + "/variants", { rows: patches }).then(function (data) { var updated = data.updated || {}; state.variants.forEach(function (v) { var u = updated[v.variant_code]; if (!u) 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; }); if (orderChanged) { // 화면 순서를 상태에도 반영(다시 그려도 지금 순서 유지) var byCode = {}; state.variants.forEach(function (v) { byCode[v.variant_code] = v; }); var reordered = []; rows.forEach(function (r) { if (byCode[r.code]) reordered.push(byCode[r.code]); }); state.variants.forEach(function (v) { if (reordered.indexOf(v) < 0) reordered.push(v); }); state.variants = reordered; var g = state.option.options[0]; var vals = []; rows.forEach(function (r) { if (r.index !== null && g.option_value[r.index]) vals.push(g.option_value[r.index]); }); g.option_value.forEach(function (ov) { if (vals.indexOf(ov) < 0) vals.push(ov); }); g.option_value = vals; } }); }); } chain .then(function () { render(); setMsg(body.querySelector("#cf24-u-msg"), "카페24에 반영했습니다.", "is-ok"); if (count) count.textContent = "(" + state.variants.length + "품목)"; }) .catch(function (err) { setMsg(msg, "실패: " + err.message + " — 화면을 다시 불러옵니다.", "is-err"); setTimeout(load, 1500); }) .then(function () { saveBtn.disabled = false; }); }); function createFlow() { saveBtn.disabled = true; setMsg(msg, "옵션 생성 중… (카페24가 품목을 만들고 코드를 부여합니다)", ""); var names = rows.map(function (r) { return r.text; }); apiJson("POST", "/cafe24/products/" + no + "/options", { option_name: optName, values: names, display_type: optType }) .then(function (data) { state.option = data.option; state.variants = data.variants || []; // 부여된 품목코드를 옵션값 이름으로 짝지어 자체코드·추가금액·진열/판매를 보낸다 var byValue = {}; state.variants.forEach(function (v) { if (v.options.length) byValue[v.options[0].value] = v.variant_code; }); var patches = []; var unmatched = 0; rows.forEach(function (r) { var code = byValue[r.text]; if (!code) { unmatched++; return; } var p = { variant_code: code }; var any = false; if (r.custom.value.trim()) { p.custom_variant_code = r.custom.value.trim(); any = true; } if (amountPlain(r.amount.value) !== "0") { p.additional_amount = r.amount.value.trim(); any = true; } if (r.display.dataset.on !== "1") { p.display = "off"; any = true; } if (r.selling.dataset.on !== "1") { p.selling = "off"; any = true; } if (any) patches.push(p); }); var chain = Promise.resolve(); if (patches.length) { setMsg(msg, "품목 " + patches.length + "건에 자체코드·추가금액 반영 중…", ""); chain = chain.then(function () { return apiJson("PUT", "/cafe24/products/" + no + "/variants", { rows: patches }); }); } var images = rows.filter(function (r) { return r.image || r.link; }); if (images.length) { chain = chain.then(function () { setMsg(msg, "썸네일 반영 중…", ""); var edited = state.option.options[0].option_value.map(function (ov) { var r = null; for (var i = 0; i < rows.length; i++) if (rows[i].text === ov.option_text) { r = rows[i]; break; } return { option_text: ov.option_text, option_image_file: r ? r.image : "", option_link_image: r ? r.link : "", option_color: "" }; }); return apiJson("PUT", "/cafe24/products/" + no + "/options", { options: [{ option_name: optName, option_display_type: optType, option_value: edited }], option_list_type: state.option.option_list_type }); }); } return chain.then(function () { return unmatched; }); }) .then(function (unmatched) { return load().then(function () { var m = body.querySelector("#cf24-u-msg"); setMsg(m, unmatched ? "옵션을 만들었습니다. 품목 " + unmatched + "건은 카페24 품목 조회가 늦어 자체코드/추가금액을 넣지 못했습니다 — 다시 저장하세요." : "옵션·품목을 만들고 카페24에 반영했습니다.", unmatched ? "is-err" : "is-ok"); }); }) .catch(function (err) { setMsg(msg, "실패: " + err.message, "is-err"); saveBtn.disabled = false; }); } } /* ═══════════════════════════════════════════════════════════ 옵션 2개 이상(조합) — 예전 2열 화면 ═══════════════════════════════════════════════════════════ */ function renderMulti() { var opt = state.option; var variants = state.variants; var html = '
' + '

옵션 정의

'; opt.options.forEach(function (o, gi) { html += '
' + '' + '
"; o.option_value.forEach(function (v, vi) { var src = v.option_image_file || v.option_link_image || variantImageFor(o.option_name, v.option_text); html += '
' + thumbHtml(src) + "" + '' + '' + '' + '
'; }); html += "
"; }); html += '

옵션이 2개 이상인 조합 상품입니다. 옵션값 추가·삭제는 카페24 API 가 지원하지 않습니다.

' + '
' + '
'; html += '

품목 (' + variants.length + ')

'; if (!variants.length) { html += '

품목이 아직 조회되지 않았습니다. 잠시 뒤 창을 닫았다 다시 여세요.

'; } else { html += '
' + ''; variants.forEach(function (v) { var label = v.options.map(function (o) { return o.value; }).join(" / ") || v.variant_code; html += '' + '" + '" + '" + '' + '' + '"; }); html += "
옵션품목코드자체 품목코드추가금액진열판매
' + (v.image ? '' : '') + "' + esc(label) + "' + toggleBtn("display", !!v.display) + "" + toggleBtn("selling", !!v.selling) + "
" + '
'; } html += "
"; body.innerHTML = html; var optForm = body.querySelector("#cf24-opt-form"); trackDirty(optForm); bindThumbUploads(optForm, optForm.querySelector("#cf24-opt-msg")); optForm.addEventListener("submit", function (e) { e.preventDefault(); saveMultiOptions(optForm); }); body.querySelector("#cf24-opt-delete").addEventListener("click", deleteOptions); var wrap = body.querySelector(".cf24-variants-wrap"); if (wrap) { trackDirty(wrap); bindToggles(wrap); bindCopy(wrap); body.querySelector("#cf24-var-save").addEventListener("click", function () { saveMultiVariants(wrap); }); } } function saveMultiOptions(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 || DEFAULT_TYPE)) 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 saveMultiVariants(wrap) { var msg = body.querySelector("#cf24-var-msg"); var saveBtn = body.querySelector("#cf24-var-save"); var rows = []; wrap.querySelectorAll("tr[data-code]").forEach(function (tr) { var r = { code: tr.dataset.code, custom: tr.querySelector('[name="custom_variant_code"]'), amount: tr.querySelector('[name="additional_amount"]'), display: tr.querySelector('.cf24-v-toggle[data-flag="display"]'), selling: tr.querySelector('.cf24-v-toggle[data-flag="selling"]') }; var p = variantPatch(r, false); if (p) rows.push(p); }); if (!rows.length) { setMsg(msg, "바뀐 품목이 없습니다.", ""); return; } window.erpConfirm("품목 " + rows.length + "건을 카페24에 바로 반영합니다. 계속할까요?").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('tr[data-code="' + code + '"]'); if (!tr) return; if (u.custom_variant_code !== undefined) tr.querySelector('[name="custom_variant_code"]').value = u.custom_variant_code; if (u.additional_amount !== undefined) tr.querySelector('[name="additional_amount"]').value = amountPlain(u.additional_amount); tr.querySelectorAll(".cf24-v-toggle").forEach(function (btn) { if (u[btn.dataset.flag] !== undefined) paintToggle(btn, !!u[btn.dataset.flag], true); }); 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 = '

삭제 중…

'; apiJson("DELETE", "/cafe24/products/" + no + "/options") .then(function (data) { state.option = data.option; state.variants = []; render(); }) .catch(function (err) { body.innerHTML = '

삭제 실패: ' + esc(err.message) + "

"; }); }); } }; })();