From e838e8ee15f1a7796e7e9b98caf2f605ef71c208 Mon Sep 17 00:00:00 2001 From: king Date: Fri, 18 Sep 2026 18:31:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(cafe24):=20=EC=98=B5=EC=85=98/=ED=92=88?= =?UTF-8?q?=EB=AA=A9=20=ED=8E=B8=EC=A7=91=EC=9D=84=20=EB=B3=84=EB=8F=84=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=B0=BD=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 380px 정보 패널은 옵션값·품목 표를 담기에 좁다. 패널에는 「팝업에서 관리」 버튼만 두고, 최대 1180px 모달(왼쪽 옵션 정의 / 오른쪽 품목 표: 이미지·옵션· 품목코드·자체코드·추가금액·진열·판매)에서 편집한다. - 모달은 페이지에 하나(패널 조각 교체와 무관), 열 때마다 카페24에서 새로 읽음. - 닫기: X · 배경 클릭 · Esc. 저장 안 한 변경(.is-dirty)이 있으면 확인창. - 다른 상품을 고르면 열려 있던 창은 닫힘. Co-Authored-By: Claude Fable 5.1 --- .../cafe24/templates/cafe24/_side.html | 16 +- .../cafe24/templates/cafe24/products.html | 122 ++++++++---- app/static/cafe24.css | 180 +++++++++++++----- docs/CAFE24_MODULE.md | 10 +- 4 files changed, 242 insertions(+), 86 deletions(-) diff --git a/app/modules/cafe24/templates/cafe24/_side.html b/app/modules/cafe24/templates/cafe24/_side.html index 26096ea..cf79971 100644 --- a/app/modules/cafe24/templates/cafe24/_side.html +++ b/app/modules/cafe24/templates/cafe24/_side.html @@ -80,13 +80,17 @@ - {# ── 옵션 / 품목 — 펼칠 때 JSON 으로 불러온다(카페24 호출 2회) ── #} -
- 옵션 / 품목 -
-

불러오는 중…

+ {# ── 옵션 / 품목 — 패널이 좁아 별도 모달(products.html 의 #cf24-options-modal)에서 + 편집한다. 버튼을 누를 때 JSON 으로 불러온다(카페24 호출 2회). ── #} +
+
+

옵션 / 품목

+
-
+

+ 옵션명·옵션값 이름/썸네일·표시방식, 품목별 자체코드·추가금액·진열/판매를 넓은 창에서 수정합니다. +

+ {% else %} diff --git a/app/modules/cafe24/templates/cafe24/products.html b/app/modules/cafe24/templates/cafe24/products.html index 47fe3aa..4ce6cc3 100644 --- a/app/modules/cafe24/templates/cafe24/products.html +++ b/app/modules/cafe24/templates/cafe24/products.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} {% block content %} @@ -109,6 +109,19 @@ {% endif %} + +{# ── 옵션/품목 편집 모달 — 오른쪽 패널(380px)은 품목 표를 담기에 좁아 별도 창에서 + 편집한다. 페이지에 하나만 두고(패널 조각 교체와 무관), 열 때마다 카페24에서 새로 + 읽는다. 내용은 products.html 의 JS(setupOptions → render) 가 그린다. #} + {% 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 = '

불러오는 중…

'; + body.innerHTML = '

카페24에서 불러오는 중…

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

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

'; - 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 = '
'; + // 옵션 정의 편집 (모달 왼쪽 열) + var html = '
' + + '

옵션 정의

' + + ''; opt.options.forEach(function (o, gi) { html += '
' + '
' + @@ -981,35 +1036,37 @@ html += '

옵션값 추가·삭제는 카페24 API 가 지원하지 않습니다(옵션 삭제 후 다시 만들어야 합니다). 썸네일은 표시방식이 「미리보기(이미지)」일 때 쇼핑몰에 보입니다.

' + '
' + '
' + - ""; + "" + + '
' + + "
"; - // 품목 — 패널 폭(380px)에 표는 좁다(실측: 이름이 이미지 아래로 꺾여 잘림). - // 품목마다 2줄 카드: 1줄 아이콘 + 옵션값 이름, 2줄 자체코드·추가금액·진열·판매. - html += '
품목 (' + variants.length + ')
'; + // 품목 표 (모달 오른쪽 열) — 넓은 창이라 한 줄에 다 들어간다. + html += '

품목 (' + variants.length + ')

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

품목이 아직 조회되지 않았습니다. 잠시 뒤 「옵션 / 품목」을 접었다 펴서 다시 불러오세요.

'; + html += '

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

'; } else { - html += '
'; + html += '
' + + '' + + '' + + ''; 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 += '
' + - '
' + - (v.image ? '' : '') + - '' + escHtml(label) + "
" + - '
' + - '' + - '' + - '' + - '" + - '" + - "
"; + html += '' + + '" + + '" + + '" + + '' + + '' + + '" + + '" + + ""; }); - html += "" + + html += "
옵션품목코드자체 품목코드추가금액진열판매
' + (v.image ? '' : '') + "' + escHtml(label) + "' + escHtml(v.variant_code) + "
" + '
' + '
'; } - html += '
'; + html += "
"; 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 = '

불러오는 중…

'; if (sidePane) sidePane.innerHTML = ""; var url = "/cafe24/products/" + no + "/pane" + (listQuery ? "?" + listQuery : ""); diff --git a/app/static/cafe24.css b/app/static/cafe24.css index 9718ed4..f1d2c75 100644 --- a/app/static/cafe24.css +++ b/app/static/cafe24.css @@ -1106,24 +1106,143 @@ font-size: 12px; } -/* 품목 — 380px 패널에 표는 좁다(실측: 옵션값 이름이 아이콘 아래로 꺾여 "그/1개"로 - 잘렸다). 품목마다 2줄 카드로 그린다. 1줄: 아이콘 + 이름(남는 폭 전부), - 2줄: 자체코드 · 추가금액 · 진열/판매 토글. */ -.cf24-v-item { - padding: 6px 0; - border-bottom: 1px solid var(--color-subtle-ash, #e5e5e5); -} - -.cf24-v-head { +/* ── 옵션/품목 모달 — 380px 패널은 품목 표를 담기에 좁아(실측: 옵션값 이름이 + 아이콘 아래로 꺾여 잘렸다) 넓은 창에서 편집한다. 왼쪽 옵션 정의 / 오른쪽 품목 표. */ +.cf24-modal { + position: fixed; + inset: 0; + z-index: 1000; display: flex; align-items: center; - gap: 8px; - margin-bottom: 4px; + justify-content: center; + padding: 24px; + background: rgba(10, 10, 10, 0.45); +} + +.cf24-modal[hidden] { + display: none; +} + +body.cf24-modal-open { + overflow: hidden; /* 뒤 화면 스크롤 잠금 */ +} + +.cf24-modal-box { + display: flex; + flex-direction: column; + width: min(1180px, 96vw); + max-height: 90vh; + background: var(--color-canvas-white, #fff); + border-radius: var(--r-xl, 14px); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +.cf24-modal-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--sp-12, 12px); + padding: 14px 18px; + border-bottom: 1px solid var(--color-subtle-ash, #e5e5e5); + flex: 0 0 auto; +} + +.cf24-modal-title { + margin: 0; + font-size: var(--text-heading, 18px); + letter-spacing: -0.45px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cf24-modal-close { + flex: 0 0 auto; + width: 32px; + height: 32px; + border: 0; + border-radius: 50%; + background: transparent; + font-size: 16px; + cursor: pointer; + color: var(--color-midtone-gray, #737373); +} + +.cf24-modal-close:hover { + background: var(--color-ghost-gray, #f2f2f2); + color: var(--color-rich-black, #0a0a0a); +} + +.cf24-modal-body { + padding: 16px 18px 20px; + overflow: auto; + flex: 1 1 auto; + min-height: 0; +} + +.cf24-modal-grid { + display: grid; + grid-template-columns: 400px minmax(0, 1fr); + gap: 24px; + align-items: start; +} + +@media (max-width: 960px) { + .cf24-modal-grid { + grid-template-columns: minmax(0, 1fr); + } +} + +.cf24-modal-col { min-width: 0; } +/* 옵션이 없는 상품의 생성 폼 — 넓은 모달에서 끝까지 늘어나지 않게 */ +.cf24-modal-body .cf24-opt-create { + max-width: 560px; +} + +/* 품목 표 — 모달의 오른쪽 열(약 700px)에 맞춘다 */ +.cf24-variants { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.cf24-variants th, +.cf24-variants td { + padding: 6px 6px; + border-bottom: 1px solid var(--color-subtle-ash, #e5e5e5); + vertical-align: middle; + text-align: left; +} + +.cf24-variants th { + font-weight: 500; + font-size: 12px; + color: var(--color-midtone-gray, #737373); + white-space: nowrap; + position: sticky; + top: 0; + background: var(--color-canvas-white, #fff); +} + +.cf24-variants-wrap { + max-height: 62vh; + overflow: auto; +} + +.cf24-v-imgcol { width: 40px; } +.cf24-v-syscode { width: 110px; } +.cf24-v-syscode code { font-size: 11px; color: var(--color-midtone-gray, #737373); } +.cf24-v-code { width: 150px; } +.cf24-v-amount { width: 96px; } +.cf24-v-amount input { text-align: right; } +.cf24-v-flag { width: 58px; text-align: center; } + .cf24-v-img { - flex: 0 0 auto; width: 32px; height: 32px; object-fit: cover; @@ -1138,47 +1257,18 @@ } .cf24-v-name { - flex: 1 1 auto; - min-width: 0; - font-size: 13px; font-weight: 500; word-break: break-word; } -.cf24-v-row { - display: flex; - align-items: flex-end; - gap: 6px; -} - -.cf24-v-row label { - display: flex; - flex-direction: column; - gap: 2px; - font-size: 11px; - color: var(--color-midtone-gray, #737373); - min-width: 0; -} - -.cf24-v-row input[type="text"] { - height: 28px; +.cf24-variants input[type="text"] { + height: 30px; box-sizing: border-box; width: 100%; - padding: 0 6px; + padding: 0 8px; border: 1px solid var(--color-subtle-ash, #e5e5e5); border-radius: var(--r-sm, 4px); - font-size: 12px; -} - -.cf24-v-code { flex: 1 1 110px; } -.cf24-v-amount { flex: 0 0 84px; } -.cf24-v-amount input { text-align: right; } - -.cf24-v-flags { - flex: 0 0 auto; - display: flex; - gap: 4px; - padding-bottom: 3px; /* 토글을 입력칸 세로 중심에 맞춘다 */ + font-size: 13px; } /* 진열/판매 토글 — 저장 전까지는 화면만 바뀐다(배지와 같은 색 규칙) */ diff --git a/docs/CAFE24_MODULE.md b/docs/CAFE24_MODULE.md index ac67ace..3425779 100644 --- a/docs/CAFE24_MODULE.md +++ b/docs/CAFE24_MODULE.md @@ -148,7 +148,11 @@ Admin API(OAuth)로는 스킨 파일을 읽거나 쓸 방법이 없다(`config.p - 오른쪽 **상품 정보 패널**(`_side.html`)은 "소스 수정 외 기능"을 모아둔 별도 공간이다. 상품 클릭 시 `/products/{no}/pane` 이 편집기 조각과 패널 조각을 **한 응답**(`_panes.html`, `[data-pane=editor]`/`[data-pane=side]`)으로 돌려주고 JS 가 각각 끼운다 — 카페24 상품 - 조회는 1회. 옵션/품목은 `
` 를 펼칠 때 JSON 으로 따로 불러온다(호출 2회 절약). + 조회는 1회. 옵션/품목은 패널의 「팝업에서 관리」 버튼으로 **모달**(`#cf24-options-modal`, + 최대 1180px — 왼쪽 옵션 정의 / 오른쪽 품목 표)을 열 때 JSON 으로 따로 불러온다(호출 2회 + 절약). 380px 패널은 품목 표를 담기에 좁아(실측) 별도 창으로 뺐다. 모달은 페이지에 하나만 + 두고(패널 조각 교체와 무관) 다른 상품을 고르면 닫힌다. 저장 안 한 변경(`.is-dirty`)이 + 있으면 닫을 때 확인창. - 1360px 이하에서는 패널이 편집기 아래로 내려가고(높이 38vh, 안에서 스크롤), 1100px 이하는 한 열이다. - 패널의 모든 쓰기는 JSON API(`routes_product_info.py`)이며 화면은 **응답값**으로 다시 @@ -460,8 +464,8 @@ cafe24_oauth_tokens 저장 화면의 옵션값 썸네일은 **`option_image_file` → `option_link_image` → 그 옵션값을 가진 품목의 `image`** 순으로 있는 것을 보여준다. 카페24 관리자에서 "옵션 연결 이미지"로만 등록한 상품은 `option_image_file` 이 비어 있고 품목 `image` 에만 나타난다(실물 — - 버튼 이미지만 보면 "없음"으로 오인했던 사고). 품목은 380px 패널에 표가 좁아 - 품목마다 2줄 카드(아이콘+이름 / 코드·금액·진열·판매)로 그린다. "옵션별 상품코드/추가금액/진열" 은 품목 + 버튼 이미지만 보면 "없음"으로 오인했던 사고). 품목은 모달 오른쪽 열에 표(이미지 · + 옵션 · 품목코드 · 자체코드 · 추가금액 · 진열 · 판매)로 그린다. "옵션별 상품코드/추가금액/진열" 은 품목 (`custom_variant_code`/`additional_amount`/`display`)이다 — 카페24 모델상 둘은 다른 리소스라 화면에서도 「옵션 저장」과 「품목 저장」이 따로 있다.