From 9b9bafdf9529b1e2b874458e9aa66d090257e7ea Mon Sep 17 00:00:00 2001 From: king Date: Fri, 14 Aug 2026 14:46:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(cafe24):=20=EC=B9=B4=ED=8E=9824=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=94=EB=80=90=20=EC=86=8C=EC=8A=A4=EA=B0=80=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=20=EB=B0=98=EC=98=81=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=E2=80=94?= =?UTF-8?q?=20=EC=BA=90=EC=8B=9C=20=EA=B8=88=EC=A7=80=20+=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=BD=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카페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 --- app/modules/cafe24/routes_bulk.py | 3 ++- app/modules/cafe24/routes_products.py | 16 ++++++++++++++-- app/modules/cafe24/templates/cafe24/_editor.html | 3 +++ app/modules/cafe24/templates/cafe24/bulk.html | 4 ++-- .../cafe24/templates/cafe24/products.html | 14 ++++++++++++-- .../cafe24/templates/cafe24/schedules.html | 2 +- app/modules/cafe24/templates/cafe24/system.html | 2 +- docs/CAFE24_MODULE.md | 4 ++++ 8 files changed, 39 insertions(+), 9 deletions(-) diff --git a/app/modules/cafe24/routes_bulk.py b/app/modules/cafe24/routes_bulk.py index 2888f03..95a91bd 100644 --- a/app/modules/cafe24/routes_bulk.py +++ b/app/modules/cafe24/routes_bulk.py @@ -29,6 +29,7 @@ from app.integrations.cafe24 import Cafe24Error, build_cafe24_api, products from . import store from .common import base_ctx, guard, require_store +from .routes_products import _no_store logger = logging.getLogger("cafe24.bulk") @@ -92,7 +93,7 @@ def bulk_page(request: Request) -> HTMLResponse: "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}") diff --git a/app/modules/cafe24/routes_products.py b/app/modules/cafe24/routes_products.py index be2ef10..9e8ceb6 100644 --- a/app/modules/cafe24/routes_products.py +++ b/app/modules/cafe24/routes_products.py @@ -49,6 +49,18 @@ def _checked(request: Request, name: str) -> bool: 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: """'2026-08-14T11:38:18+09:00' → '2026-08-14 11:38'.""" text = str(value or "").strip() @@ -181,7 +193,7 @@ def product_list(request: Request) -> HTMLResponse: ) if 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) @@ -197,7 +209,7 @@ def product_pane(request: Request, product_no: int) -> HTMLResponse: ctx = base_ctx(request, user, active_tab="products") ctx.update(_editor_ctx(st, product_no)) 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}") diff --git a/app/modules/cafe24/templates/cafe24/_editor.html b/app/modules/cafe24/templates/cafe24/_editor.html index 5e71d05..6eaf2f4 100644 --- a/app/modules/cafe24/templates/cafe24/_editor.html +++ b/app/modules/cafe24/templates/cafe24/_editor.html @@ -60,6 +60,9 @@ + diff --git a/app/modules/cafe24/templates/cafe24/bulk.html b/app/modules/cafe24/templates/cafe24/bulk.html index 4cfe02e..883a216 100644 --- a/app/modules/cafe24/templates/cafe24/bulk.html +++ b/app/modules/cafe24/templates/cafe24/bulk.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} {% block content %} @@ -107,7 +107,7 @@ progress.textContent = "검사 중… 0 / " + rows.length; walk(rows, function (tr, next) { 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 (data) { var pick = tr.querySelector(".cf24-pick"); diff --git a/app/modules/cafe24/templates/cafe24/products.html b/app/modules/cafe24/templates/cafe24/products.html index 03b30b9..16625a3 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 %} @@ -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"); if (form) { form.addEventListener("submit", function (e) { @@ -240,7 +249,8 @@ function select(no, push) { pane.innerHTML = '

불러오는 중…

'; 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) { if (res.redirected) { window.location.href = res.url; return null; } if (!res.ok) throw new Error("HTTP " + res.status); diff --git a/app/modules/cafe24/templates/cafe24/schedules.html b/app/modules/cafe24/templates/cafe24/schedules.html index 31df825..8b70005 100644 --- a/app/modules/cafe24/templates/cafe24/schedules.html +++ b/app/modules/cafe24/templates/cafe24/schedules.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} {% block content %} diff --git a/app/modules/cafe24/templates/cafe24/system.html b/app/modules/cafe24/templates/cafe24/system.html index a40c35b..8adaae7 100644 --- a/app/modules/cafe24/templates/cafe24/system.html +++ b/app/modules/cafe24/templates/cafe24/system.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} {% block content %} diff --git a/docs/CAFE24_MODULE.md b/docs/CAFE24_MODULE.md index b39a180..b6ddda6 100644 --- a/docs/CAFE24_MODULE.md +++ b/docs/CAFE24_MODULE.md @@ -93,6 +93,10 @@ app/modules/cafe24/ ← 상품관리 모듈 - **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지 않으므로 카페24 호출이 1회로 끝난다. JS 실패 시 각 행의 링크로 정상 동작한다. - 편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 **저장 안 됨 경고**가 뜬다. +- **캐시 금지.** 화면·조각 응답에 `Cache-Control: no-store` 를 붙이고 조각 fetch 에도 + `cache: "no-store"` 를 건다. 캐시된 조각이 다시 그려지면 카페24 관리자에서 값을 바꾼 + 뒤에도 예전 소스가 보이고, 그것을 그대로 편집하면 남의 수정을 덮어쓴다. + 편집기의 **[다시 읽기]** 버튼으로 언제든 현재값을 강제로 다시 받을 수 있다. - `.erp-page` 의 `max-width` 를 이 화면에서만 풀어 편집 영역을 넓게 쓴다. 이때 `box-sizing: border-box` 를 함께 줘야 한다(안 주면 padding 이 폭에 더해져 문서에 가로 스크롤이 생긴다).