fix(cafe24): 카페24에서 바뀐 소스가 화면에 반영되지 않던 문제 — 캐시 금지 + 다시 읽기
카페24 관리자에서 소스를 고쳤는데 우리 화면은 예전 것을 보여주는 문제.
편집기 조각(GET /products/{no}/pane)과 목록 화면 응답에 캐시 헤더가 없었다.
브라우저가 이전 응답을 재사용하면 카페24의 현재값이 아닌 예전 소스가 그려진다.
그 상태에서 편집·적용하면 카페24 관리자에서 한 수정을 덮어쓰게 되므로, 단순한
표시 문제가 아니라 데이터 손실로 이어질 수 있다.
응답에 Cache-Control: no-store 를 붙이고 조각을 가져가는 fetch 에도
cache: "no-store" 를 걸었다. 일괄수정 검사 조회도 같다.
편집기에 [다시 읽기] 버튼을 추가했다. 카페24 관리자에서 방금 고친 경우 목록을
다시 그리지 않고 그 상품의 현재 소스만 강제로 받아온다. 편집 중이면 저장 안 됨
경고를 먼저 띄운다.
카페24 API 가 쓰기 직후 잠시 예전 값을 돌려줄 가능성도 있다(읽기 지연). 그 경우도
[다시 읽기] 로 확인할 수 있게 했다.
검증: 유닛테스트 51개 통과. 렌더된 편집기 JS 를 브라우저에서 구문 검사(new Function)
통과, no-store·다시 읽기 반영 확인. 라우트 13개.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ from app.integrations.cafe24 import Cafe24Error, build_cafe24_api, products
|
|||||||
|
|
||||||
from . import store
|
from . import store
|
||||||
from .common import base_ctx, guard, require_store
|
from .common import base_ctx, guard, require_store
|
||||||
|
from .routes_products import _no_store
|
||||||
|
|
||||||
logger = logging.getLogger("cafe24.bulk")
|
logger = logging.getLogger("cafe24.bulk")
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ def bulk_page(request: Request) -> HTMLResponse:
|
|||||||
"error": error,
|
"error": error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return render_template(request, "cafe24/bulk.html", ctx)
|
return _no_store(render_template(request, "cafe24/bulk.html", ctx))
|
||||||
|
|
||||||
|
|
||||||
@bulk_router.get("/bulk/scan/{product_no}")
|
@bulk_router.get("/bulk/scan/{product_no}")
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ def _checked(request: Request, name: str) -> bool:
|
|||||||
return request.query_params.get(name) is not None
|
return request.query_params.get(name) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _no_store(response: Any) -> Any:
|
||||||
|
"""브라우저가 이 응답을 재사용하지 못하게 한다.
|
||||||
|
|
||||||
|
편집기는 카페24의 **현재** HTML 을 보여줘야 한다. 캐시된 화면이 다시 그려지면
|
||||||
|
카페24 관리자에서 값을 바꾼 뒤에도 예전 소스가 보이고, 그것을 그대로 편집하면
|
||||||
|
남의 수정을 덮어쓴다. 조각을 가져가는 fetch 에도 `cache: "no-store"` 를 건다.
|
||||||
|
"""
|
||||||
|
response.headers["Cache-Control"] = "no-store, must-revalidate"
|
||||||
|
response.headers["Pragma"] = "no-cache"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _short_dt(value: Any) -> str:
|
def _short_dt(value: Any) -> str:
|
||||||
"""'2026-08-14T11:38:18+09:00' → '2026-08-14 11:38'."""
|
"""'2026-08-14T11:38:18+09:00' → '2026-08-14 11:38'."""
|
||||||
text = str(value or "").strip()
|
text = str(value or "").strip()
|
||||||
@@ -181,7 +193,7 @@ def product_list(request: Request) -> HTMLResponse:
|
|||||||
)
|
)
|
||||||
if selected:
|
if selected:
|
||||||
ctx.update(_editor_ctx(st, selected))
|
ctx.update(_editor_ctx(st, selected))
|
||||||
return render_template(request, "cafe24/products.html", ctx)
|
return _no_store(render_template(request, "cafe24/products.html", ctx))
|
||||||
|
|
||||||
|
|
||||||
@products_router.get("/products/{product_no}/pane", response_class=HTMLResponse)
|
@products_router.get("/products/{product_no}/pane", response_class=HTMLResponse)
|
||||||
@@ -197,7 +209,7 @@ def product_pane(request: Request, product_no: int) -> HTMLResponse:
|
|||||||
ctx = base_ctx(request, user, active_tab="products")
|
ctx = base_ctx(request, user, active_tab="products")
|
||||||
ctx.update(_editor_ctx(st, product_no))
|
ctx.update(_editor_ctx(st, product_no))
|
||||||
ctx["list_query"] = _list_query(request)
|
ctx["list_query"] = _list_query(request)
|
||||||
return render_template(request, "cafe24/_editor.html", ctx)
|
return _no_store(render_template(request, "cafe24/_editor.html", ctx))
|
||||||
|
|
||||||
|
|
||||||
@products_router.get("/products/{product_no}")
|
@products_router.get("/products/{product_no}")
|
||||||
|
|||||||
@@ -60,6 +60,9 @@
|
|||||||
<input class="cf24-memo" type="text" name="memo" maxlength="200"
|
<input class="cf24-memo" type="text" name="memo" maxlength="200"
|
||||||
placeholder="변경 메모 (버전 이력에 남습니다)" />
|
placeholder="변경 메모 (버전 이력에 남습니다)" />
|
||||||
<button class="erp-btn erp-btn-outline" type="button" data-copy="cf24-html-pc">복사</button>
|
<button class="erp-btn erp-btn-outline" type="button" data-copy="cf24-html-pc">복사</button>
|
||||||
|
<button class="erp-btn erp-btn-outline" type="button"
|
||||||
|
data-reload="{{ product_no }}"
|
||||||
|
title="카페24에서 현재 소스를 다시 읽어옵니다. 카페24 관리자에서 방금 고쳤다면 이걸 누르세요.">다시 읽기</button>
|
||||||
<button class="erp-btn erp-btn-primary" type="submit">카페24에 적용</button>
|
<button class="erp-btn erp-btn-primary" type="submit">카페24에 적용</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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=20260814i" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
progress.textContent = "검사 중… 0 / " + rows.length;
|
progress.textContent = "검사 중… 0 / " + rows.length;
|
||||||
walk(rows, function (tr, next) {
|
walk(rows, function (tr, next) {
|
||||||
setState(tr, "검사 중…");
|
setState(tr, "검사 중…");
|
||||||
fetch("/cafe24/bulk/scan/" + tr.dataset.no, { credentials: "same-origin" })
|
fetch("/cafe24/bulk/scan/" + tr.dataset.no, { credentials: "same-origin", cache: "no-store" })
|
||||||
.then(function (res) { return res.ok ? res.json() : Promise.reject(res); })
|
.then(function (res) { return res.ok ? res.json() : Promise.reject(res); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
var pick = tr.querySelector(".cf24-pick");
|
var pick = tr.querySelector(".cf24-pick");
|
||||||
|
|||||||
@@ -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=20260814i" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -223,6 +223,15 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 「다시 읽기」 — 카페24 관리자에서 방금 고친 경우 현재 소스를 강제로 다시 받는다.
|
||||||
|
var reloadBtn = pane.querySelector("[data-reload]");
|
||||||
|
if (reloadBtn) {
|
||||||
|
reloadBtn.addEventListener("click", function () {
|
||||||
|
if (!confirmLeave()) return;
|
||||||
|
select(reloadBtn.dataset.reload, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var form = pane.querySelector(".cf24-editor-form");
|
var form = pane.querySelector(".cf24-editor-form");
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
@@ -240,7 +249,8 @@
|
|||||||
function select(no, push) {
|
function select(no, push) {
|
||||||
pane.innerHTML = '<p class="cf24-muted" style="padding:16px;">불러오는 중…</p>';
|
pane.innerHTML = '<p class="cf24-muted" style="padding:16px;">불러오는 중…</p>';
|
||||||
var url = "/cafe24/products/" + no + "/pane" + (listQuery ? "?" + listQuery : "");
|
var url = "/cafe24/products/" + no + "/pane" + (listQuery ? "?" + listQuery : "");
|
||||||
fetch(url, { credentials: "same-origin" })
|
// no-store: 캐시된 조각이 뜨면 카페24의 현재 소스가 아닌 예전 것을 편집하게 된다.
|
||||||
|
fetch(url, { credentials: "same-origin", cache: "no-store" })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.redirected) { window.location.href = res.url; return null; }
|
if (res.redirected) { window.location.href = res.url; return null; }
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
|||||||
@@ -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=20260814i" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -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=20260814i" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ app/modules/cafe24/ ← 상품관리 모듈
|
|||||||
- **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지
|
- **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지
|
||||||
않으므로 카페24 호출이 1회로 끝난다. JS 실패 시 각 행의 링크로 정상 동작한다.
|
않으므로 카페24 호출이 1회로 끝난다. JS 실패 시 각 행의 링크로 정상 동작한다.
|
||||||
- 편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 **저장 안 됨 경고**가 뜬다.
|
- 편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 **저장 안 됨 경고**가 뜬다.
|
||||||
|
- **캐시 금지.** 화면·조각 응답에 `Cache-Control: no-store` 를 붙이고 조각 fetch 에도
|
||||||
|
`cache: "no-store"` 를 건다. 캐시된 조각이 다시 그려지면 카페24 관리자에서 값을 바꾼
|
||||||
|
뒤에도 예전 소스가 보이고, 그것을 그대로 편집하면 남의 수정을 덮어쓴다.
|
||||||
|
편집기의 **[다시 읽기]** 버튼으로 언제든 현재값을 강제로 다시 받을 수 있다.
|
||||||
- `.erp-page` 의 `max-width` 를 이 화면에서만 풀어 편집 영역을 넓게 쓴다. 이때
|
- `.erp-page` 의 `max-width` 를 이 화면에서만 풀어 편집 영역을 넓게 쓴다. 이때
|
||||||
`box-sizing: border-box` 를 함께 줘야 한다(안 주면 padding 이 폭에 더해져 문서에
|
`box-sizing: border-box` 를 함께 줘야 한다(안 주면 padding 이 폭에 더해져 문서에
|
||||||
가로 스크롤이 생긴다).
|
가로 스크롤이 생긴다).
|
||||||
|
|||||||
Reference in New Issue
Block a user