feat(cafe24): 상품관리를 좌우 2분할로 — 목록(좁게) | 상세페이지 편집(넓게)
왼쪽에서 상품을 클릭하면 오른쪽에 편집기가 바로 열린다. 목록을 다시 받지 않고
오른쪽 조각만 교체한다(GET /products/{no}/pane → JS 삽입). 목록까지 다시 그리면
클릭마다 카페24 호출이 2회 더 늘어나기 때문이다. JS 가 실패하거나 없으면 각 행의
링크(/cafe24/?selected=)로 그대로 동작한다.
목록은 페이지를 없애고 전체를 한 번에 받는다(list_all_products, 1회 100개·상한
1000개). 필터·정렬을 한 페이지에만 적용하면 다음 페이지에 있는 상품이 빠져
"진열중만 보기" 가 거짓이 된다. 현재 87개라 1회 호출로 끝난다.
컬럼은 요청대로 번호·상품명·진열·판매·수정 5개다. 좁은 칸에 맞춰 진열/판매는
배지 대신 점, 수정일은 월-일만 표시하고 전체 값은 title 로 둔다. 긴 상품명은
2줄로 제한해 행 높이를 고르게 유지한다(전체 이름은 title·편집기 제목에서 확인).
진열중/판매중 체크박스는 중복 선택이 되며 둘 다 켜면 AND 다. 문서에 없는 API
필터 파라미터에 기대지 않고 받아온 뒤 파이썬에서 걸러낸다. 제목행 클릭은
오름↔내림 토글이며 한글 정렬은 localeCompare(ko) 를 쓴다.
편집 영역을 넓게 쓰려고 이 화면에서만 .erp-page 의 max-width 를 풀었다. 이때
box-sizing:border-box 를 함께 줘야 한다 — width:100% + padding:24px 이라
max-width 만 풀면 문서 전체에 가로 스크롤이 생긴다(측정으로 확인 후 수정).
편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 저장 안 됨 경고를 띄운다.
옛 단독 화면(product.html)은 제거하고 /products/{no} 는 2분할 화면으로
리다이렉트한다. 편집기 조각을 두 곳에서 함께 쓰도록 _editor.html 로 분리했다.
검증: 유닛테스트 33개 통과(신규 3개 — 전체 조회의 페이지 순회·상한 처리·1회
종료). 상한 처리는 테스트가 잡아서 고쳤다(요청한 만큼 받았는지로 판정). 가짜
데이터로 렌더해 브라우저에서 실측: 왼쪽 360px·오른쪽 940px, 각 칸 독립 스크롤,
분할 영역이 화면 높이에 맞고, 가로 스크롤 없음, 정렬 오름/내림 동작 확인.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
{# 오른쪽 편집기 조각.
|
||||
전체 페이지(products.html)가 include 하고, JS 가 /products/{no}/pane 으로
|
||||
같은 조각만 다시 받아 끼워 넣는다. 그래서 여기에는 <script> 를 두지 않는다
|
||||
(innerHTML 로 삽입된 script 는 실행되지 않는다 — JS 는 products.html 에 있고
|
||||
삽입 후 cf24BindEditor() 로 다시 연결한다). #}
|
||||
|
||||
{% if editor_error %}
|
||||
<div class="cf24-flash cf24-flash-err">
|
||||
카페24 조회에 실패했습니다: {{ editor_error }}<br />
|
||||
<a href="/cafe24/system">시스템 화면에서 연결 상태를 확인하세요.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="cf24-editor-head">
|
||||
<div>
|
||||
<h3 class="cf24-editor-title">{{ info.product_name or '상품' }}</h3>
|
||||
<p class="cf24-editor-sub">
|
||||
상품번호 {{ product_no }}
|
||||
{% if info.product_code %}· <code>{{ info.product_code }}</code>{% endif %}
|
||||
{% if info.price %}· {{ info.price }}{% endif %}
|
||||
{% if info.updated_date %}· 최근 수정 {{ info.updated_date }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="cf24-editor-badges">
|
||||
{% if info.display %}<span class="erp-badge cf24-badge-ok">진열</span>
|
||||
{% else %}<span class="erp-badge cf24-badge-off">미진열</span>{% endif %}
|
||||
{% if info.selling %}<span class="erp-badge cf24-badge-ok">판매</span>
|
||||
{% else %}<span class="erp-badge cf24-badge-off">중지</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if desc %}
|
||||
<p class="cf24-note">
|
||||
{% if desc.separated_mobile %}
|
||||
<strong>PC/모바일 분리 사용 상품입니다.</strong> 아래 적용은 PC 만 바꿉니다 —
|
||||
모바일({{ desc.mobile_description | length }}자)은 카페24 관리자에서 따로 반영해야 합니다.
|
||||
{% else %}
|
||||
모바일은 PC와 동일 설정이라 적용 시 <strong>함께 반영</strong>됩니다.
|
||||
{% endif %}
|
||||
{% if desc.mobile_differs %}<span class="cf24-warn">현재 PC/모바일 내용이 다릅니다.</span>{% endif %}
|
||||
이미지 경로의 한글 파일명은 카페24에 <code>%EC%9A%A9…</code> 로 저장되어 있습니다.
|
||||
여기서는 읽기 쉽게 한글로 보여주고, 적용할 때 원래 형식으로 되돌립니다.
|
||||
</p>
|
||||
|
||||
<form class="cf24-editor-form" method="post"
|
||||
action="/cafe24/products/{{ product_no }}/apply"
|
||||
data-confirm="카페24 쇼핑몰에 바로 반영됩니다. 적용할까요? 직전 내용은 자동으로 백업되어 되돌릴 수 있습니다.">
|
||||
<input type="hidden" name="base_fingerprint" value="{{ fingerprint }}" />
|
||||
<input type="hidden" name="list_query" value="{{ list_query }}" />
|
||||
|
||||
<div class="cf24-editor-bar">
|
||||
<span class="cf24-muted">PC 상세설명 HTML · {{ desc.description | length }}자</span>
|
||||
<span class="cf24-editor-bar-right">
|
||||
<input class="cf24-memo" type="text" name="memo" maxlength="200"
|
||||
placeholder="변경 메모 (버전 이력에 남습니다)" />
|
||||
<button class="erp-btn erp-btn-outline" type="button" data-copy="cf24-html-pc">복사</button>
|
||||
<button class="erp-btn erp-btn-primary" type="submit">카페24에 적용</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<textarea id="cf24-html-pc" class="cf24-html cf24-html-main" name="html"
|
||||
spellcheck="false">{{ html_pc }}</textarea>
|
||||
</form>
|
||||
|
||||
{% if desc.separated_mobile or desc.mobile_differs %}
|
||||
<details class="cf24-details">
|
||||
<summary>모바일 상세설명 HTML 보기 (읽기 전용 · {{ desc.mobile_description | length }}자)</summary>
|
||||
<textarea id="cf24-html-mo" class="cf24-html" rows="12" readonly
|
||||
spellcheck="false">{{ html_mobile }}</textarea>
|
||||
<div class="cf24-actions">
|
||||
<button type="button" class="erp-btn erp-btn-outline" data-copy="cf24-html-mo">복사</button>
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<details class="cf24-details">
|
||||
<summary>버전 이력 {% if revisions %}({{ revisions | length }}건){% endif %}</summary>
|
||||
{% if revisions %}
|
||||
<div class="cf24-scroll">
|
||||
<table class="erp-table cf24-compact">
|
||||
<thead>
|
||||
<tr><th>시각</th><th>유형</th><th>길이</th><th>작업자</th><th>메모</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rev in revisions %}
|
||||
<tr>
|
||||
<td class="cf24-nowrap">{{ rev.created_at }}</td>
|
||||
<td class="cf24-nowrap"><code>{{ rev.revision_type }}</code></td>
|
||||
<td class="cf24-nowrap">{{ rev.html_length }}자</td>
|
||||
<td class="cf24-nowrap">{{ rev.created_by or '—' }}</td>
|
||||
<td>{{ rev.memo or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="cf24-muted">버전 선택 복원은 Phase 6 에서 붙습니다. 내용은 모두 보관됩니다.</p>
|
||||
{% else %}
|
||||
<p class="cf24-muted">아직 이 상품의 변경 이력이 없습니다.</p>
|
||||
{% endif %}
|
||||
</details>
|
||||
{% endif %}
|
||||
@@ -1,180 +0,0 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814d" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "cafe24/_nav.html" %}
|
||||
|
||||
{% if flash %}<div class="cf24-flash cf24-flash-ok">{{ flash }}</div>{% endif %}
|
||||
{% if flash_error %}<div class="cf24-flash cf24-flash-err">{{ flash_error }}</div>{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="cf24-flash cf24-flash-err">
|
||||
카페24 조회에 실패했습니다: {{ error }}<br />
|
||||
<a href="/cafe24/system">시스템 화면에서 연결 상태를 확인하세요.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── 기본 정보 ─────────────────────────────────────────────── #}
|
||||
<div class="erp-card cf24-card">
|
||||
<div class="cf24-card-head">
|
||||
<h3>{{ info.product_name or '상품' }}</h3>
|
||||
<span class="cf24-muted">상품번호 {{ product_no }}</span>
|
||||
</div>
|
||||
|
||||
<table class="erp-table cf24-kv">
|
||||
<tbody>
|
||||
<tr><th>상품코드</th><td><code>{{ info.product_code or '—' }}</code></td></tr>
|
||||
<tr><th>판매가</th><td>{{ info.price or '—' }}</td></tr>
|
||||
<tr>
|
||||
<th>진열 / 판매</th>
|
||||
<td>
|
||||
{% if info.display %}<span class="erp-badge cf24-badge-ok">진열</span>
|
||||
{% else %}<span class="erp-badge cf24-badge-off">미진열</span>{% endif %}
|
||||
{% if info.selling %}<span class="erp-badge cf24-badge-ok">판매</span>
|
||||
{% else %}<span class="erp-badge cf24-badge-off">중지</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr><th>최근 수정</th><td>{{ info.updated_date or '—' }}</td></tr>
|
||||
<tr><th>요약설명</th><td>{{ info.summary_description or '—' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="cf24-actions">
|
||||
<a class="erp-btn erp-btn-outline" href="/cafe24/">← 상품 목록</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── 상세설명 편집 ─────────────────────────────────────────── #}
|
||||
{% if desc %}
|
||||
<div class="erp-card cf24-card">
|
||||
<div class="cf24-card-head">
|
||||
<h3>상세페이지 HTML 편집</h3>
|
||||
<span class="cf24-muted">적용하면 쇼핑몰에 바로 반영됩니다</span>
|
||||
</div>
|
||||
|
||||
<table class="erp-table cf24-kv">
|
||||
<tbody>
|
||||
<tr><th>PC 상세설명</th><td>{{ desc.description | length }}자</td></tr>
|
||||
<tr>
|
||||
<th>모바일 상세설명</th>
|
||||
<td>
|
||||
{{ desc.mobile_description | length }}자 ·
|
||||
{% if desc.separated_mobile %}
|
||||
<span class="cf24-warn">PC와 분리 사용</span> — 아래 적용은 PC 만 바꿉니다.
|
||||
모바일은 카페24 관리자에서 따로 반영해야 합니다.
|
||||
{% else %}
|
||||
<span class="cf24-muted">PC와 동일 설정</span> — 적용 시 모바일도 같은 내용으로 함께 반영됩니다.
|
||||
{% endif %}
|
||||
{% if desc.mobile_differs %}<span class="cf24-warn">현재 내용 불일치</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="cf24-note">
|
||||
이미지 경로의 한글 파일명은 카페24에 <code>%EC%9A%A9…</code> 형태로 저장되어 있습니다.
|
||||
편집기에서는 읽기 쉽게 <strong>한글로 풀어서</strong> 보여주고, 적용할 때 원래 형식으로
|
||||
되돌려 저장합니다. 경로를 직접 고칠 때도 한글로 그냥 쓰시면 됩니다.
|
||||
</p>
|
||||
|
||||
<form method="post" action="/cafe24/products/{{ product_no }}/apply"
|
||||
onsubmit="return confirm('카페24 쇼핑몰에 바로 반영됩니다. 적용할까요?\n\n직전 내용은 자동으로 백업되어 되돌릴 수 있습니다.');">
|
||||
<input type="hidden" name="base_fingerprint" value="{{ fingerprint }}" />
|
||||
|
||||
<label class="cf24-label" for="cf24-html-pc">PC 상세설명 HTML</label>
|
||||
<textarea id="cf24-html-pc" class="cf24-html" name="html" rows="22"
|
||||
spellcheck="false">{{ html_pc }}</textarea>
|
||||
|
||||
<div class="cf24-toolbar" style="margin-top:12px;">
|
||||
<input class="cf24-search" style="flex:1 1 320px;" type="text" name="memo" maxlength="200"
|
||||
placeholder="변경 메모 (버전 이력에 남습니다 — 예: 8월 프로모션 배너 교체)" />
|
||||
<button class="erp-btn erp-btn-primary" type="submit">카페24에 적용</button>
|
||||
<button class="erp-btn erp-btn-outline" type="button" data-copy="cf24-html-pc">HTML 복사</button>
|
||||
<a class="erp-btn erp-btn-outline" href="/cafe24/products/{{ product_no }}">되돌리기(새로고침)</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if desc.separated_mobile or desc.mobile_differs %}
|
||||
<label class="cf24-label" for="cf24-html-mo">모바일 상세설명 HTML (읽기 전용)</label>
|
||||
<textarea id="cf24-html-mo" class="cf24-html" rows="12" readonly spellcheck="false"
|
||||
>{{ html_mobile }}</textarea>
|
||||
<div class="cf24-actions">
|
||||
<button type="button" class="erp-btn erp-btn-outline" data-copy="cf24-html-mo">HTML 복사</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── 버전 이력 ─────────────────────────────────────────────── #}
|
||||
<div class="erp-card cf24-card">
|
||||
<div class="cf24-card-head">
|
||||
<h3>버전 이력</h3>
|
||||
<span class="cf24-muted">최근 20건 · 적용 직전 내용은 BACKUP 으로 자동 보관</span>
|
||||
</div>
|
||||
{% if revisions %}
|
||||
<div class="cf24-scroll">
|
||||
<table class="erp-table">
|
||||
<thead>
|
||||
<tr><th>시각</th><th>유형</th><th>길이</th><th>작업자</th><th>메모</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rev in revisions %}
|
||||
<tr>
|
||||
<td class="cf24-nowrap">{{ rev.created_at }}</td>
|
||||
<td class="cf24-nowrap"><code>{{ rev.revision_type }}</code></td>
|
||||
<td class="cf24-nowrap">{{ rev.html_length }}자</td>
|
||||
<td class="cf24-nowrap">{{ rev.created_by or '—' }}</td>
|
||||
<td>{{ rev.memo or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="cf24-muted">되돌리기(버전 선택 복원)는 Phase 6 에서 화면에 붙습니다. 지금도 내용은 모두 보관됩니다.</p>
|
||||
{% else %}
|
||||
<p class="cf24-muted">아직 이 상품의 변경 이력이 없습니다.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// HTML 복사 — clipboard API 가 막히면 textarea 선택으로 대체한다.
|
||||
document.querySelectorAll("[data-copy]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var box = document.getElementById(btn.dataset.copy);
|
||||
if (!box) return;
|
||||
var done = function () {
|
||||
var old = btn.textContent;
|
||||
btn.textContent = "복사했습니다";
|
||||
setTimeout(function () { btn.textContent = old; }, 1500);
|
||||
};
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(box.value).then(done, function () { box.select(); });
|
||||
} else {
|
||||
box.select();
|
||||
try { document.execCommand("copy"); done(); } catch (e) { /* 사용자가 직접 복사 */ }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 편집 중 실수로 페이지를 벗어나는 것 방지.
|
||||
(function () {
|
||||
var box = document.getElementById("cf24-html-pc");
|
||||
if (!box || box.readOnly) return;
|
||||
var initial = box.value;
|
||||
var form = box.form;
|
||||
var submitting = false;
|
||||
if (form) form.addEventListener("submit", function () { submitting = true; });
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
if (!submitting && box.value !== initial) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,11 +1,15 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814d" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814e" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "cafe24/_nav.html" %}
|
||||
{% set qs = (list_query ~ '&') if list_query else '' %}
|
||||
|
||||
{% if flash %}<div class="cf24-flash cf24-flash-ok">{{ flash }}</div>{% endif %}
|
||||
{% if flash_error %}<div class="cf24-flash cf24-flash-err">{{ flash_error }}</div>{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="cf24-flash cf24-flash-err">
|
||||
@@ -14,74 +18,217 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="erp-card cf24-card">
|
||||
<div class="cf24-card-head">
|
||||
<h3>상품 목록</h3>
|
||||
<span class="cf24-muted">
|
||||
{% if keyword %}“{{ keyword }}” 검색 결과 {{ total }}건{% else %}전체 {{ total }}건{% endif %}
|
||||
· {{ page }} / {{ last_page }} 페이지
|
||||
</span>
|
||||
</div>
|
||||
<div class="cf24-split">
|
||||
|
||||
<form class="cf24-toolbar" method="get" action="/cafe24/">
|
||||
<input class="cf24-search" type="search" name="q" value="{{ keyword }}"
|
||||
placeholder="상품명으로 검색 (부분일치)" />
|
||||
<button class="erp-btn erp-btn-primary" type="submit">검색</button>
|
||||
{% if keyword %}<a class="erp-btn erp-btn-outline" href="/cafe24/">전체보기</a>{% endif %}
|
||||
</form>
|
||||
{# ── 왼쪽: 상품 목록 ──────────────────────────────────────── #}
|
||||
<aside class="erp-card cf24-pane cf24-pane-list">
|
||||
<form class="cf24-filters" method="get" action="/cafe24/" id="cf24-filter-form">
|
||||
{% if selected %}<input type="hidden" name="selected" value="{{ selected }}" />{% endif %}
|
||||
<input class="cf24-search" type="search" name="q" value="{{ keyword }}"
|
||||
placeholder="상품명 검색" />
|
||||
<div class="cf24-checks">
|
||||
<label><input type="checkbox" name="display" value="1"
|
||||
{% if only_display %}checked{% endif %} /> 진열중</label>
|
||||
<label><input type="checkbox" name="selling" value="1"
|
||||
{% if only_selling %}checked{% endif %} /> 판매중</label>
|
||||
</div>
|
||||
<div class="cf24-list-count">
|
||||
{{ shown }}건{% if shown != total %} / 전체 {{ total }}건{% endif %}
|
||||
{% if truncated %}<span class="cf24-warn">(상한 도달)</span>{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if rows %}
|
||||
<div class="cf24-scroll">
|
||||
<table class="erp-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>상품번호</th><th>상품코드</th><th>상품명</th>
|
||||
<th>진열</th><th>판매</th><th>판매가</th><th>최근 수정</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td class="cf24-nowrap">{{ r.product_no }}</td>
|
||||
<td class="cf24-nowrap"><code>{{ r.product_code }}</code></td>
|
||||
<td>{{ r.product_name }}</td>
|
||||
<td class="cf24-nowrap">
|
||||
{% if r.display %}<span class="erp-badge cf24-badge-ok">진열</span>
|
||||
{% else %}<span class="erp-badge cf24-badge-off">미진열</span>{% endif %}
|
||||
</td>
|
||||
<td class="cf24-nowrap">
|
||||
{% if r.selling %}<span class="erp-badge cf24-badge-ok">판매</span>
|
||||
{% else %}<span class="erp-badge cf24-badge-off">중지</span>{% endif %}
|
||||
</td>
|
||||
<td class="cf24-nowrap">{{ r.price }}</td>
|
||||
<td class="cf24-nowrap">{{ r.updated_date }}</td>
|
||||
<td class="cf24-nowrap">
|
||||
<a class="erp-btn erp-btn-outline" href="/cafe24/products/{{ r.product_no }}">상세페이지</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="cf24-list-scroll">
|
||||
<table class="erp-table cf24-list-table" id="cf24-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cf24-col-no" data-sort-key="no" data-sort-type="num">번호</th>
|
||||
<th class="cf24-col-name" data-sort-key="name" data-sort-type="text">상품명</th>
|
||||
<th class="cf24-col-flag" data-sort-key="display" data-sort-type="num">진열</th>
|
||||
<th class="cf24-col-flag" data-sort-key="selling" data-sort-type="num">판매</th>
|
||||
<th class="cf24-col-date" data-sort-key="updated" data-sort-type="text">수정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr class="cf24-row {% if selected == r.product_no %}is-active{% endif %}"
|
||||
data-no="{{ r.product_no }}"
|
||||
data-name="{{ r.product_name }}"
|
||||
data-display="{{ 1 if r.display else 0 }}"
|
||||
data-selling="{{ 1 if r.selling else 0 }}"
|
||||
data-updated="{{ r.updated_date }}">
|
||||
<td class="cf24-col-no">{{ r.product_no }}</td>
|
||||
<td class="cf24-col-name" title="{{ r.product_name }}">
|
||||
<a href="/cafe24/?{{ qs }}selected={{ r.product_no }}">{{ r.product_name }}</a>
|
||||
</td>
|
||||
<td class="cf24-col-flag">
|
||||
{% if r.display %}<span class="cf24-dot cf24-dot-on" title="진열중"></span>
|
||||
{% else %}<span class="cf24-dot" title="미진열"></span>{% endif %}
|
||||
</td>
|
||||
<td class="cf24-col-flag">
|
||||
{% if r.selling %}<span class="cf24-dot cf24-dot-on" title="판매중"></span>
|
||||
{% else %}<span class="cf24-dot" title="판매중지"></span>{% endif %}
|
||||
</td>
|
||||
{# 좁은 칸이라 월-일만. 전체 값은 title 로 확인 #}
|
||||
<td class="cf24-col-date" title="{{ r.updated_date }}">{{ r.updated_date[5:10] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not rows and not error %}
|
||||
<p class="cf24-muted" style="padding:12px;">조건에 맞는 상품이 없습니다.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{% if last_page > 1 %}
|
||||
<div class="cf24-pager">
|
||||
{% if page > 1 %}
|
||||
<a class="erp-btn erp-btn-outline"
|
||||
href="/cafe24/?q={{ keyword | urlencode }}&page={{ page - 1 }}">← 이전</a>
|
||||
{# ── 오른쪽: 상세페이지 편집 ──────────────────────────────── #}
|
||||
<section class="erp-card cf24-pane cf24-pane-editor" id="cf24-editor-pane">
|
||||
{% if selected %}
|
||||
{% include "cafe24/_editor.html" %}
|
||||
{% else %}
|
||||
<div class="cf24-empty-pane">
|
||||
<h3>왼쪽에서 상품을 선택하세요.</h3>
|
||||
<p class="cf24-muted">
|
||||
선택한 상품의 상세페이지 HTML 을 여기서 바로 편집하고 카페24에 적용할 수 있습니다.<br />
|
||||
적용 직전 내용은 자동으로 백업되어 되돌릴 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="cf24-muted">{{ page }} / {{ last_page }}</span>
|
||||
{% if page < last_page %}
|
||||
<a class="erp-btn erp-btn-outline"
|
||||
href="/cafe24/?q={{ keyword | urlencode }}&page={{ page + 1 }}">다음 →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif not error %}
|
||||
<p class="cf24-muted">
|
||||
{% if keyword %}“{{ keyword }}” 로 찾은 상품이 없습니다.{% else %}표시할 상품이 없습니다.{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
var pane = document.getElementById("cf24-editor-pane");
|
||||
var listQuery = {{ list_query | tojson }};
|
||||
var dirty = false;
|
||||
|
||||
// ── 편집기 조각을 새로 끼워 넣은 뒤 다시 연결 ──
|
||||
window.cf24BindEditor = function () {
|
||||
dirty = false;
|
||||
|
||||
pane.querySelectorAll("[data-copy]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var box = document.getElementById(btn.dataset.copy);
|
||||
if (!box) return;
|
||||
var done = function () {
|
||||
var old = btn.textContent;
|
||||
btn.textContent = "복사됨";
|
||||
setTimeout(function () { btn.textContent = old; }, 1500);
|
||||
};
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(box.value).then(done, function () { box.select(); });
|
||||
} else {
|
||||
box.select();
|
||||
try { document.execCommand("copy"); done(); } catch (e) { /* 직접 복사 */ }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var form = pane.querySelector(".cf24-editor-form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
if (!window.confirm(form.dataset.confirm)) { e.preventDefault(); return; }
|
||||
dirty = false;
|
||||
});
|
||||
}
|
||||
var box = document.getElementById("cf24-html-pc");
|
||||
if (box && !box.readOnly) {
|
||||
box.addEventListener("input", function () { dirty = true; });
|
||||
}
|
||||
};
|
||||
|
||||
function confirmLeave() {
|
||||
return !dirty || window.confirm("편집한 내용이 저장되지 않았습니다. 이동할까요?");
|
||||
}
|
||||
|
||||
// ── 목록 클릭 → 오른쪽만 교체 ──
|
||||
function select(no, push) {
|
||||
pane.innerHTML = '<p class="cf24-muted" style="padding:16px;">불러오는 중…</p>';
|
||||
var url = "/cafe24/products/" + no + "/pane" + (listQuery ? "?" + listQuery : "");
|
||||
fetch(url, { credentials: "same-origin" })
|
||||
.then(function (res) {
|
||||
if (res.redirected) { window.location.href = res.url; return null; }
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.text();
|
||||
})
|
||||
.then(function (htmlText) {
|
||||
if (htmlText === null) return;
|
||||
pane.innerHTML = htmlText;
|
||||
window.cf24BindEditor();
|
||||
pane.scrollTop = 0;
|
||||
if (push) {
|
||||
var target = "/cafe24/?" + (listQuery ? listQuery + "&" : "") + "selected=" + no;
|
||||
history.pushState({ no: no }, "", target);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
// 조각 로드가 실패하면 평범한 페이지 이동으로 대체한다.
|
||||
window.location.href = "/cafe24/?" + (listQuery ? listQuery + "&" : "") + "selected=" + no;
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("#cf24-list tbody tr.cf24-row").forEach(function (tr) {
|
||||
tr.addEventListener("click", function (e) {
|
||||
if (e.target.tagName === "A") e.preventDefault();
|
||||
if (!confirmLeave()) return;
|
||||
document.querySelectorAll("#cf24-list tr.is-active").forEach(function (el) {
|
||||
el.classList.remove("is-active");
|
||||
});
|
||||
tr.classList.add("is-active");
|
||||
select(tr.dataset.no, true);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", function () {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// ── 필터 체크박스는 즉시 적용 ──
|
||||
var filterForm = document.getElementById("cf24-filter-form");
|
||||
filterForm.querySelectorAll('input[type="checkbox"]').forEach(function (cb) {
|
||||
cb.addEventListener("change", function () {
|
||||
if (confirmLeave()) filterForm.submit();
|
||||
});
|
||||
});
|
||||
|
||||
// ── 제목행 클릭 정렬(오름/내림 토글) ──
|
||||
var table = document.getElementById("cf24-list");
|
||||
var tbody = table.querySelector("tbody");
|
||||
var sortKey = null, sortAsc = true;
|
||||
|
||||
table.querySelectorAll("th[data-sort-key]").forEach(function (th) {
|
||||
th.classList.add("cf24-sortable");
|
||||
th.addEventListener("click", function () {
|
||||
var key = th.dataset.sortKey;
|
||||
sortAsc = key === sortKey ? !sortAsc : true;
|
||||
sortKey = key;
|
||||
|
||||
table.querySelectorAll("th[data-sort-key]").forEach(function (other) {
|
||||
other.classList.remove("is-asc", "is-desc");
|
||||
});
|
||||
th.classList.add(sortAsc ? "is-asc" : "is-desc");
|
||||
|
||||
var numeric = th.dataset.sortType === "num";
|
||||
var rows = Array.prototype.slice.call(tbody.querySelectorAll("tr.cf24-row"));
|
||||
rows.sort(function (a, b) {
|
||||
var x = a.dataset[key] || "", y = b.dataset[key] || "";
|
||||
var cmp = numeric
|
||||
? (parseFloat(x) || 0) - (parseFloat(y) || 0)
|
||||
: x.localeCompare(y, "ko");
|
||||
return sortAsc ? cmp : -cmp;
|
||||
});
|
||||
rows.forEach(function (tr) { tbody.appendChild(tr); });
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
if (dirty) { e.preventDefault(); e.returnValue = ""; }
|
||||
});
|
||||
|
||||
if (pane.querySelector(".cf24-editor-form")) window.cf24BindEditor();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user