fix(cafe24): 옵션 모달 — 옵션 열 축소·품목 표 확대, 창 바닥까지 사용, 품목코드 복사

- 옵션 정의 열 400 → 340px, 모달 폭 1180 → 1280px. 품목 표 열 폭을 줄여
  옵션값 이름이 한 줄(넘치면 말줄임 + title)에 들어가게.
- 모달 높이를 90vh 고정으로 바꾸고 두 열이 바닥까지 늘어나 각자 스크롤
  (품목은 표만 스크롤, 저장 버튼 고정). 첫 구현 때 남은 옛 규칙
  `.cf24-variants-wrap { max-height: 320px }` 가 표 높이를 막고 있었다
  (헤드리스 크롬 실측으로 발견) — 제거.
- 품목코드 클릭 = 클립보드 복사("복사됨" 표시, 비보안 컨텍스트는 execCommand).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 18:50:28 +09:00
parent e838e8ee15
commit be0d44ded6
3 changed files with 96 additions and 28 deletions
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/cafe24.css?v=20260918c" /> <link rel="stylesheet" href="/static/cafe24.css?v=20260918d" />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -1007,7 +1007,7 @@
} }
// 옵션 정의 편집 (모달 왼쪽 열) // 옵션 정의 편집 (모달 왼쪽 열)
var html = '<div class="cf24-modal-grid"><section class="cf24-modal-col">' + var html = '<div class="cf24-modal-grid"><section class="cf24-modal-col cf24-modal-col-options">' +
'<h4 class="cf24-side-title">옵션 정의</h4>' + '<h4 class="cf24-side-title">옵션 정의</h4>' +
'<form class="cf24-side-form" id="cf24-opt-form">'; '<form class="cf24-side-form" id="cf24-opt-form">';
opt.options.forEach(function (o, gi) { opt.options.forEach(function (o, gi) {
@@ -1041,7 +1041,7 @@
"</section>"; "</section>";
// 품목 표 (모달 오른쪽 열) — 넓은 창이라 한 줄에 다 들어간다. // 품목 표 (모달 오른쪽 열) — 넓은 창이라 한 줄에 다 들어간다.
html += '<section class="cf24-modal-col"><h4 class="cf24-side-title">품목 (' + variants.length + ')</h4>'; html += '<section class="cf24-modal-col cf24-modal-col-variants"><h4 class="cf24-side-title">품목 (' + variants.length + ')</h4>';
if (!variants.length) { if (!variants.length) {
html += '<p class="cf24-muted">품목이 아직 조회되지 않았습니다. 잠시 뒤 창을 닫았다 다시 여세요.</p>'; html += '<p class="cf24-muted">품목이 아직 조회되지 않았습니다. 잠시 뒤 창을 닫았다 다시 여세요.</p>';
} else { } else {
@@ -1054,8 +1054,8 @@
var amount = String(parseInt(v.additional_amount || "0", 10) || 0); var amount = String(parseInt(v.additional_amount || "0", 10) || 0);
html += '<tr class="cf24-v-item" data-code="' + escHtml(v.variant_code) + '">' + 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-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-name" title="' + escHtml(label) + '">' + escHtml(label) + "</td>" +
'<td class="cf24-v-syscode"><code>' + escHtml(v.variant_code) + "</code></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-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-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.display ? " is-on" : "") + '" data-flag="display" data-on="' + (v.display ? 1 : 0) + '" data-initial="' + (v.display ? 1 : 0) + '">' + (v.display ? "진열" : "미진열") + "</button></td>" +
@@ -1147,6 +1147,34 @@
if (!saveBtn) return; if (!saveBtn) return;
var wrap = body.querySelector(".cf24-variants-wrap"); var wrap = body.querySelector(".cf24-variants-wrap");
trackDirty(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) { wrap.querySelectorAll(".cf24-v-toggle").forEach(function (btn) {
btn.addEventListener("click", function () { btn.addEventListener("click", function () {
var on = btn.dataset.on !== "1"; var on = btn.dataset.on !== "1";
+62 -22
View File
@@ -1130,8 +1130,10 @@ body.cf24-modal-open {
.cf24-modal-box { .cf24-modal-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: min(1180px, 96vw); width: min(1280px, 96vw);
max-height: 90vh; /* max-height 가 아니라 height 고정 — 두 열이 창 바닥까지 늘어나고 각 열 안에서
스크롤한다(품목이 많을 때 표만 스크롤, 저장 버튼은 항상 보임). */
height: 90vh;
background: var(--color-canvas-white, #fff); background: var(--color-canvas-white, #fff);
border-radius: var(--r-xl, 14px); border-radius: var(--r-xl, 14px);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
@@ -1176,27 +1178,52 @@ body.cf24-modal-open {
} }
.cf24-modal-body { .cf24-modal-body {
padding: 16px 18px 20px; padding: 16px 18px 18px;
overflow: auto;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
display: flex;
flex-direction: column;
} }
/* 옵션 정의(왼쪽, 좁게) | 품목 표(오른쪽, 남는 폭 전부). 두 열 모두 창 바닥까지. */
.cf24-modal-grid { .cf24-modal-grid {
flex: 1 1 auto;
min-height: 0;
display: grid; display: grid;
grid-template-columns: 400px minmax(0, 1fr); grid-template-columns: 340px minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
gap: 24px; gap: 24px;
align-items: start; align-items: stretch;
} }
@media (max-width: 960px) { @media (max-width: 960px) {
.cf24-modal-grid { .cf24-modal-grid {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto;
} }
} }
.cf24-modal-col { .cf24-modal-col {
min-width: 0; min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
/* 왼쪽 열은 통째로 스크롤, 오른쪽 열은 표만 스크롤(제목·저장 버튼 고정) */
.cf24-modal-col-options {
overflow: auto;
}
.cf24-modal-col-variants > .cf24-variants-wrap {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
border-bottom: 1px solid var(--color-subtle-ash, #e5e5e5);
}
.cf24-modal-col-variants > .cf24-side-actions {
flex: 0 0 auto;
} }
/* 옵션이 없는 상품의 생성 폼 — 넓은 모달에서 끝까지 늘어나지 않게 */ /* 옵션이 없는 상품의 생성 폼 — 넓은 모달에서 끝까지 늘어나지 않게 */
@@ -1229,18 +1256,37 @@ body.cf24-modal-open {
background: var(--color-canvas-white, #fff); background: var(--color-canvas-white, #fff);
} }
.cf24-variants-wrap { /* 열 폭 — 옵션 이름이 한 줄에 들어가도록 나머지를 좁게 잡는다
max-height: 62vh; (1280px 모달: 340 옵션 + 24 간격 → 표 약 880px, 이름 칸 약 420px). */
overflow: auto; .cf24-v-imgcol { width: 40px; }
.cf24-v-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
.cf24-v-syscode { width: 104px; }
.cf24-v-code { width: 132px; }
.cf24-v-amount { width: 90px; }
.cf24-v-amount input { text-align: right; }
.cf24-v-flag { width: 56px; text-align: center; }
/* 품목코드 — 클릭하면 클립보드로. 눌리는 것임을 커서·밑줄로 알린다. */
.cf24-v-copy {
font-family: var(--font-geist-mono, ui-monospace, monospace);
font-size: 11px;
color: var(--color-midtone-gray, #737373);
background: transparent;
border: 0;
padding: 2px 4px;
border-radius: var(--r-sm, 4px);
cursor: copy;
white-space: nowrap;
} }
.cf24-v-imgcol { width: 40px; } .cf24-v-copy:hover {
.cf24-v-syscode { width: 110px; } background: var(--color-ghost-gray, #f2f2f2);
.cf24-v-syscode code { font-size: 11px; color: var(--color-midtone-gray, #737373); } color: var(--color-rich-black, #0a0a0a);
.cf24-v-code { width: 150px; } }
.cf24-v-amount { width: 96px; }
.cf24-v-amount input { text-align: right; } .cf24-v-copy.is-copied {
.cf24-v-flag { width: 58px; text-align: center; } color: var(--color-success-green, #10c22b);
}
.cf24-v-img { .cf24-v-img {
width: 32px; width: 32px;
@@ -1258,7 +1304,6 @@ body.cf24-modal-open {
.cf24-v-name { .cf24-v-name {
font-weight: 500; font-weight: 500;
word-break: break-word;
} }
.cf24-variants input[type="text"] { .cf24-variants input[type="text"] {
@@ -1292,11 +1337,6 @@ body.cf24-modal-open {
box-shadow: 0 0 0 2px #f0b429; box-shadow: 0 0 0 2px #f0b429;
} }
.cf24-variants-wrap {
max-height: 320px;
overflow: auto;
}
.cf24-opt-danger { .cf24-opt-danger {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
+1 -1
View File
@@ -149,7 +149,7 @@ Admin API(OAuth)로는 스킨 파일을 읽거나 쓸 방법이 없다(`config.p
상품 클릭 시 `/products/{no}/pane` 이 편집기 조각과 패널 조각을 **한 응답**(`_panes.html`, 상품 클릭 시 `/products/{no}/pane` 이 편집기 조각과 패널 조각을 **한 응답**(`_panes.html`,
`[data-pane=editor]`/`[data-pane=side]`)으로 돌려주고 JS 가 각각 끼운다 — 카페24 상품 `[data-pane=editor]`/`[data-pane=side]`)으로 돌려주고 JS 가 각각 끼운다 — 카페24 상품
조회는 1회. 옵션/품목은 패널의 「팝업에서 관리」 버튼으로 **모달**(`#cf24-options-modal`, 조회는 1회. 옵션/품목은 패널의 「팝업에서 관리」 버튼으로 **모달**(`#cf24-options-modal`,
최대 1180px — 왼쪽 옵션 정의 / 오른쪽 품목 표)을 열 때 JSON 으로 따로 불러온다(호출 2회 최대 1280px·높이 90vh — 왼쪽 옵션 정의(340px) / 오른쪽 품목 표, 두 열 모두 창 바닥까지 각자 스크롤. 품목코드 클릭 = 클립보드 복사)을 열 때 JSON 으로 따로 불러온다(호출 2회
절약). 380px 패널은 품목 표를 담기에 좁아(실측) 별도 창으로 뺐다. 모달은 페이지에 하나만 절약). 380px 패널은 품목 표를 담기에 좁아(실측) 별도 창으로 뺐다. 모달은 페이지에 하나만
두고(패널 조각 교체와 무관) 다른 상품을 고르면 닫힌다. 저장 안 한 변경(`.is-dirty`)이 두고(패널 조각 교체와 무관) 다른 상품을 고르면 닫힌다. 저장 안 한 변경(`.is-dirty`)이
있으면 닫을 때 확인창. 있으면 닫을 때 확인창.