fix(cafe24): 모바일 상세설명 '직접등록' 오적용·적용직후 읽기지연 수정
- PUT 시 mobile_description 필드를 더 이상 보내지 않는다. 그 필드를 보내는 순간 카페24가 모바일 상세설명 설정을 "직접 등록"으로 바꿔버림을 실물로 확인(separated_mobile_description 이 'T'가 됨). 대신 separated_mobile_description="F" 만 지정해 "PC 상세설명과 동일"을 강제하고, 카페24가 모바일 값을 PC 와 자동으로 맞추게 한다. 화면 적용(apply)과 예약 적용(worker) 양쪽 다 수정. - products.wait_for_description 추가 — 적용 직후 카페24 관리자 API 의 짧은 읽기 지연(쓰기 직후 몇 초간 이전 값을 돌려줌 — 쇼핑몰 화면에는 바로 반영됨)을 0.8초 간격 최대 3회 재확인으로 흡수. "쇼핑몰엔 반영됐는데 카페24 상품관리 화면만 적용 안 된 것처럼 보이는" 증상 완화. - CAFE24_SHOP_URL 예시를 www.miras.co.kr 로 갱신(.env.example) — 실제 값은 운영 서버 .env 에서 직접 설정해야 함(코드는 그대로 이 값을 읽어 다이렉트 주소를 만듦). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -103,7 +103,7 @@ CUSTOMER_ORDER_LIST_URL=/orderlist/
|
|||||||
# (예: https://miras.co.kr/product/detail.html?product_no=119).
|
# (예: https://miras.co.kr/product/detail.html?product_no=119).
|
||||||
# 커스텀 도메인은 mall_id 로 알 수 없어 직접 지정해야 한다.
|
# 커스텀 도메인은 mall_id 로 알 수 없어 직접 지정해야 한다.
|
||||||
# 미설정 시 카페24 기본 도메인(https://<mall_id>.cafe24.com)으로 대체된다.
|
# 미설정 시 카페24 기본 도메인(https://<mall_id>.cafe24.com)으로 대체된다.
|
||||||
# CAFE24_SHOP_URL=https://miras.co.kr
|
# CAFE24_SHOP_URL=https://www.miras.co.kr
|
||||||
#
|
#
|
||||||
# access/refresh token 을 DB 에 Fernet 암호화해서 저장할 때 쓰는 키.
|
# access/refresh token 을 DB 에 Fernet 암호화해서 저장할 때 쓰는 키.
|
||||||
# openssl rand -hex 32 로 생성. ⚠️ 값을 바꾸면 기존 토큰을 복호화할 수 없어
|
# openssl rand -hex 32 로 생성. ⚠️ 값을 바꾸면 기존 토큰을 복호화할 수 없어
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -149,6 +150,34 @@ def fetch_descriptions(client: Cafe24Client, product_no: int) -> Descriptions:
|
|||||||
return descriptions_from_product(get_product(client, product_no))
|
return descriptions_from_product(get_product(client, product_no))
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_description(
|
||||||
|
client: Cafe24Client,
|
||||||
|
product_no: int,
|
||||||
|
expected: str,
|
||||||
|
*,
|
||||||
|
attempts: int = 3,
|
||||||
|
delay: float = 0.8,
|
||||||
|
) -> bool:
|
||||||
|
"""PUT 직후 카페24 관리자 API 가 새 값을 돌려줄 때까지 짧게 재확인한다.
|
||||||
|
|
||||||
|
실물에서 관찰된 지연: PUT 이 성공하고 쇼핑몰 화면(고객이 보는 상세페이지)에는
|
||||||
|
바로 반영되는데도, 관리자 API(`GET /admin/products/{no}`)는 몇 초간 직전 값을
|
||||||
|
돌려줄 때가 있다. 그 상태에서 다른 상품을 봤다가 다시 돌아오면 우리 편집기가
|
||||||
|
"적용 안 된 것"처럼 보인다 — 우리 쪽 캐시 문제가 아니라 카페24 쪽 읽기 지연이다.
|
||||||
|
적용 직후 여기서 짧게 흡수해, 화면에 돌아왔을 때는 이미 새 값이 보이게 한다.
|
||||||
|
실패해도 PUT 자체는 이미 성공했으므로 예외를 던지지 않는다.
|
||||||
|
"""
|
||||||
|
for _ in range(max(1, attempts)):
|
||||||
|
time.sleep(delay)
|
||||||
|
try:
|
||||||
|
current = fetch_descriptions(client, product_no)
|
||||||
|
except Exception: # noqa: BLE001 — 확인 실패는 무시(적용 자체는 이미 성공)
|
||||||
|
return False
|
||||||
|
if current.description == expected:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _flag_value(flag: bool) -> str:
|
def _flag_value(flag: bool) -> str:
|
||||||
"""카페24는 boolean 을 'T'/'F' 문자열로 받는다."""
|
"""카페24는 boolean 을 'T'/'F' 문자열로 받는다."""
|
||||||
return "T" if flag else "F"
|
return "T" if flag else "F"
|
||||||
@@ -158,6 +187,7 @@ def build_update_payload(
|
|||||||
*,
|
*,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
mobile_description: str | None = None,
|
mobile_description: str | None = None,
|
||||||
|
separated_mobile_description: str | None = None,
|
||||||
product_name: str | None = None,
|
product_name: str | None = None,
|
||||||
display: bool | None = None,
|
display: bool | None = None,
|
||||||
selling: bool | None = None,
|
selling: bool | None = None,
|
||||||
@@ -168,6 +198,11 @@ def build_update_payload(
|
|||||||
`None` 인 항목은 payload 에 넣지 않는다 = 그 필드를 건드리지 않는다.
|
`None` 인 항목은 payload 에 넣지 않는다 = 그 필드를 건드리지 않는다.
|
||||||
예약에서 "진열만 켜기"처럼 상세설명 없이 상태만 바꾸는 경우가 있으므로
|
예약에서 "진열만 켜기"처럼 상세설명 없이 상태만 바꾸는 경우가 있으므로
|
||||||
description 도 생략할 수 있다.
|
description 도 생략할 수 있다.
|
||||||
|
|
||||||
|
⚠️ `mobile_description` 을 명시적으로 보내면 카페24가 `separated_mobile_description`
|
||||||
|
을 'T'(관리자 화면 "직접 등록")로 바꿔버린다(실물 확인). "PC 상세설명과 동일"을
|
||||||
|
유지하려면 `mobile_description` 은 보내지 말고 `separated_mobile_description="F"`
|
||||||
|
만 지정할 것 — `update_descriptions` 가 이 방식을 쓴다.
|
||||||
"""
|
"""
|
||||||
request: dict[str, Any] = {}
|
request: dict[str, Any] = {}
|
||||||
if description is not None:
|
if description is not None:
|
||||||
@@ -176,6 +211,8 @@ def build_update_payload(
|
|||||||
request["product_name"] = product_name
|
request["product_name"] = product_name
|
||||||
if mobile_description is not None:
|
if mobile_description is not None:
|
||||||
request["mobile_description"] = mobile_description
|
request["mobile_description"] = mobile_description
|
||||||
|
if separated_mobile_description is not None:
|
||||||
|
request["separated_mobile_description"] = separated_mobile_description
|
||||||
if display is not None:
|
if display is not None:
|
||||||
request["display"] = _flag_value(display)
|
request["display"] = _flag_value(display)
|
||||||
if selling is not None:
|
if selling is not None:
|
||||||
@@ -192,6 +229,7 @@ def update_product(
|
|||||||
*,
|
*,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
mobile_description: str | None = None,
|
mobile_description: str | None = None,
|
||||||
|
separated_mobile_description: str | None = None,
|
||||||
product_name: str | None = None,
|
product_name: str | None = None,
|
||||||
display: bool | None = None,
|
display: bool | None = None,
|
||||||
selling: bool | None = None,
|
selling: bool | None = None,
|
||||||
@@ -207,6 +245,7 @@ def update_product(
|
|||||||
payload = build_update_payload(
|
payload = build_update_payload(
|
||||||
description=description,
|
description=description,
|
||||||
mobile_description=mobile_description,
|
mobile_description=mobile_description,
|
||||||
|
separated_mobile_description=separated_mobile_description,
|
||||||
product_name=product_name,
|
product_name=product_name,
|
||||||
display=display,
|
display=display,
|
||||||
selling=selling,
|
selling=selling,
|
||||||
@@ -225,15 +264,21 @@ def update_descriptions(
|
|||||||
product_no: int,
|
product_no: int,
|
||||||
*,
|
*,
|
||||||
description: str,
|
description: str,
|
||||||
mobile_description: str | None = None,
|
|
||||||
shop_no: int | None = None,
|
shop_no: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""상세설명만 교체하는 지름길. 실제 전송은 `update_product` 가 한다."""
|
"""상세설명 교체 + "PC 상세설명과 동일" 강제.
|
||||||
|
|
||||||
|
`mobile_description` 필드는 보내지 않는다 — 보내는 순간 카페24 관리자
|
||||||
|
화면의 모바일 상세설명 설정이 "직접 등록"으로 바뀌어버리기 때문이다(실물
|
||||||
|
확인). 대신 `separated_mobile_description="F"` 만 지정하면 카페24가 모바일
|
||||||
|
값을 PC 와 자동으로 맞춰주면서 설정도 "PC 상세설명과 동일하게 사용"으로
|
||||||
|
유지된다.
|
||||||
|
"""
|
||||||
return update_product(
|
return update_product(
|
||||||
client,
|
client,
|
||||||
product_no,
|
product_no,
|
||||||
description=description,
|
description=description,
|
||||||
mobile_description=mobile_description,
|
separated_mobile_description="F",
|
||||||
shop_no=shop_no,
|
shop_no=shop_no,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,11 @@
|
|||||||
MANUAL 버전 + 감사로그
|
MANUAL 버전 + 감사로그
|
||||||
로컬 DB 의 마지막 버전을 "지금 카페24에 올라간 값"으로 가정하지 않는다.
|
로컬 DB 의 마지막 버전을 "지금 카페24에 올라간 값"으로 가정하지 않는다.
|
||||||
|
|
||||||
PC/모바일은 구분하지 않는다 — 적용 시 `description` 과 `mobile_description` 에
|
PC/모바일은 구분하지 않는다 — 적용 시 `description` 만 쓰고
|
||||||
같은 HTML 을 쓴다(운영 방침). 분리 사용 상품이어도 한쪽만 바뀌는 일이 없다.
|
`separated_mobile_description="F"` 를 강제해 카페24가 모바일 값을 PC 와 자동으로
|
||||||
|
맞추게 한다(운영 방침). `mobile_description` 필드를 직접 보내면 카페24 관리자
|
||||||
|
화면의 모바일 상세설명 설정이 "직접 등록"으로 바뀌어버리므로(실물 확인) 보내지
|
||||||
|
않는다.
|
||||||
|
|
||||||
이미지 경로의 한글은 카페24에 퍼센트 인코딩으로 저장돼 있다. 편집기에는
|
이미지 경로의 한글은 카페24에 퍼센트 인코딩으로 저장돼 있다. 편집기에는
|
||||||
`store.decode_html_urls` 로 풀어서 보여주고, 저장할 때 `encode_html_urls` 로
|
`store.decode_html_urls` 로 풀어서 보여주고, 저장할 때 `encode_html_urls` 로
|
||||||
@@ -448,16 +451,13 @@ def product_apply(
|
|||||||
status_code=303,
|
status_code=303,
|
||||||
)
|
)
|
||||||
|
|
||||||
# PC/모바일을 구분하지 않는다 — 항상 같은 내용으로 함께 쓴다(운영 방침).
|
if submitted == current.description:
|
||||||
# 분리 사용 상품이어도 모바일에 같은 HTML 을 넣으므로 한쪽만 바뀌는 일이 없다.
|
|
||||||
mobile_html = submitted
|
|
||||||
|
|
||||||
if submitted == current.description and mobile_html == current.mobile_description:
|
|
||||||
return RedirectResponse(url=f"{back}&msg=변경된 내용이 없어 적용하지 않았습니다.", status_code=303)
|
return RedirectResponse(url=f"{back}&msg=변경된 내용이 없어 적용하지 않았습니다.", status_code=303)
|
||||||
try:
|
try:
|
||||||
products.update_descriptions(
|
# mobile_description 은 보내지 않는다 — update_descriptions 가
|
||||||
api.client, product_no, description=submitted, mobile_description=mobile_html
|
# separated_mobile_description="F" 로 "PC 상세설명과 동일"을 강제하고
|
||||||
)
|
# 카페24가 모바일 값을 자동으로 맞춰준다(모바일도 항상 PC와 같다).
|
||||||
|
products.update_descriptions(api.client, product_no, description=submitted)
|
||||||
except Cafe24Error as exc:
|
except Cafe24Error as exc:
|
||||||
st.log_audit(
|
st.log_audit(
|
||||||
actor=actor, action="apply_description", product_no=product_no,
|
actor=actor, action="apply_description", product_no=product_no,
|
||||||
@@ -469,6 +469,11 @@ def product_apply(
|
|||||||
status_code=303,
|
status_code=303,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 카페24 관리자 API 는 쓰기 직후 몇 초간 이전 값을 돌려줄 때가 있다(쇼핑몰
|
||||||
|
# 화면에는 바로 반영됨). 여기서 짧게 확인해, 화면으로 돌아갔을 때 우리
|
||||||
|
# 편집기에도 이미 새 값이 보이게 한다(실패해도 적용 자체는 이미 끝났다).
|
||||||
|
products.wait_for_description(api.client, product_no, submitted)
|
||||||
|
|
||||||
revision_id = st.add_revision(
|
revision_id = st.add_revision(
|
||||||
product_no=product_no,
|
product_no=product_no,
|
||||||
html_content=submitted,
|
html_content=submitted,
|
||||||
@@ -479,7 +484,7 @@ def product_apply(
|
|||||||
st.log_audit(
|
st.log_audit(
|
||||||
actor=actor, action="apply_description", product_no=product_no,
|
actor=actor, action="apply_description", product_no=product_no,
|
||||||
revision_id=revision_id, result="SUCCESS",
|
revision_id=revision_id, result="SUCCESS",
|
||||||
detail=f"{len(submitted)}자 적용 (백업 {backup_id}, PC·모바일 동시 반영)",
|
detail=f"{len(submitted)}자 적용 (백업 {backup_id}, 모바일 PC와 동일 유지)",
|
||||||
)
|
)
|
||||||
logger.info("카페24 상품 %s 상세설명 적용 (%s)", product_no, actor)
|
logger.info("카페24 상품 %s 상세설명 적용 (%s)", product_no, actor)
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ from app.timezone import now_kst
|
|||||||
|
|
||||||
SECRET = "unit-test-secret"
|
SECRET = "unit-test-secret"
|
||||||
|
|
||||||
|
# wait_for_description 은 실물 카페24 API 의 쓰기 직후 읽기 지연을 흡수하려고
|
||||||
|
# 실제로 sleep 한다. 가짜 클라이언트는 그 지연을 재현하지 않으므로(항상 같은
|
||||||
|
# 값을 돌려줌) 재시도 예산을 다 채우게 되는데, 테스트에서까지 그 시간을 그대로
|
||||||
|
# 기다릴 필요는 없다.
|
||||||
|
products.time.sleep = lambda *_a, **_k: None
|
||||||
|
|
||||||
_TEST_ENV = {
|
_TEST_ENV = {
|
||||||
"CAFE24_MALL_ID": "testmall",
|
"CAFE24_MALL_ID": "testmall",
|
||||||
"CAFE24_CLIENT_ID": "cid",
|
"CAFE24_CLIENT_ID": "cid",
|
||||||
@@ -338,8 +344,10 @@ def test_update_descriptions_payload():
|
|||||||
products.update_descriptions(client, 131, description="<p>NEW</p>")
|
products.update_descriptions(client, 131, description="<p>NEW</p>")
|
||||||
call = client.calls[0]
|
call = client.calls[0]
|
||||||
assert call["method"] == "PUT" and call["path"] == "/admin/products/131"
|
assert call["method"] == "PUT" and call["path"] == "/admin/products/131"
|
||||||
# 준 필드만 바뀌어야 한다 — 모바일을 지정하지 않으면 보내지 않는다.
|
# mobile_description 은 절대 보내지 않는다 — 보내면 카페24가 모바일 상세설명
|
||||||
assert call["json"] == {"request": {"description": "<p>NEW</p>"}}
|
# 설정을 "직접 등록"으로 바꿔버린다(실물 확인). separated_mobile_description="F"
|
||||||
|
# 만 지정해 "PC 상세설명과 동일"을 강제한다.
|
||||||
|
assert call["json"] == {"request": {"description": "<p>NEW</p>", "separated_mobile_description": "F"}}
|
||||||
|
|
||||||
|
|
||||||
def test_update_payload_optional_fields():
|
def test_update_payload_optional_fields():
|
||||||
@@ -727,7 +735,10 @@ def test_worker_applies_html_and_flags():
|
|||||||
# HTML 과 진열 상태를 한 번의 PUT 으로 보냈는가
|
# HTML 과 진열 상태를 한 번의 PUT 으로 보냈는가
|
||||||
put = [c for c in client.calls if c["method"] == "PUT"][0]
|
put = [c for c in client.calls if c["method"] == "PUT"][0]
|
||||||
assert put["json"]["request"]["description"] == "<p>예약 내용</p>"
|
assert put["json"]["request"]["description"] == "<p>예약 내용</p>"
|
||||||
assert put["json"]["request"]["mobile_description"] == "<p>예약 내용</p>"
|
# mobile_description 은 보내지 않는다 — separated_mobile_description="F" 로
|
||||||
|
# "PC 상세설명과 동일"을 강제한다(직접 보내면 "직접 등록"으로 바뀌어버린다).
|
||||||
|
assert "mobile_description" not in put["json"]["request"]
|
||||||
|
assert put["json"]["request"]["separated_mobile_description"] == "F"
|
||||||
assert put["json"]["request"]["display"] == "T"
|
assert put["json"]["request"]["display"] == "T"
|
||||||
assert "selling" not in put["json"]["request"] # 변경 없음이면 보내지 않는다
|
assert "selling" not in put["json"]["request"] # 변경 없음이면 보내지 않는다
|
||||||
assert st.finished == [
|
assert st.finished == [
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ def _apply(store_db: Any, api: Any, row: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
# 상세설명을 바꿀 때는 쓰기 직전 현재값을 읽어 백업한다(로컬 값을 믿지 않는다).
|
# 상세설명을 바꿀 때는 쓰기 직전 현재값을 읽어 백업한다(로컬 값을 믿지 않는다).
|
||||||
backup_id = 0
|
backup_id = 0
|
||||||
mobile_html: str | None = None
|
|
||||||
if html is not None:
|
if html is not None:
|
||||||
current = products.fetch_descriptions(api.client, product_no)
|
current = products.fetch_descriptions(api.client, product_no)
|
||||||
backup_id = store_db.add_revision(
|
backup_id = store_db.add_revision(
|
||||||
@@ -77,17 +76,22 @@ def _apply(store_db: Any, api: Any, row: dict[str, Any]) -> str:
|
|||||||
memo=f"예약 #{schedule_id} 적용 직전 자동 백업 (모바일)",
|
memo=f"예약 #{schedule_id} 적용 직전 자동 백업 (모바일)",
|
||||||
created_by=ACTOR,
|
created_by=ACTOR,
|
||||||
)
|
)
|
||||||
# PC/모바일은 구분하지 않는다 — 화면 편집과 같은 방침.
|
|
||||||
mobile_html = html
|
|
||||||
|
|
||||||
|
# PC/모바일은 구분하지 않는다 — mobile_description 은 보내지 않고
|
||||||
|
# separated_mobile_description="F" 로 "PC 상세설명과 동일"을 강제한다.
|
||||||
|
# (mobile_description 을 직접 보내면 카페24가 그 설정을 "직접 등록"으로 바꿔버린다.)
|
||||||
products.update_product(
|
products.update_product(
|
||||||
api.client,
|
api.client,
|
||||||
product_no,
|
product_no,
|
||||||
description=html,
|
description=html,
|
||||||
mobile_description=mobile_html,
|
separated_mobile_description="F" if html is not None else None,
|
||||||
display=set_display,
|
display=set_display,
|
||||||
selling=set_selling,
|
selling=set_selling,
|
||||||
)
|
)
|
||||||
|
if html is not None:
|
||||||
|
# 화면 편집(apply)과 동일 — 카페24 관리자 API 의 쓰기 직후 읽기 지연을
|
||||||
|
# 여기서 짧게 흡수한다(쇼핑몰에는 바로 반영되지만 관리자 조회만 뒤쳐질 때가 있다).
|
||||||
|
products.wait_for_description(api.client, product_no, html)
|
||||||
|
|
||||||
summary = store.describe_schedule_action(
|
summary = store.describe_schedule_action(
|
||||||
has_html=html is not None, set_display=set_display, set_selling=set_selling
|
has_html=html is not None, set_display=set_display, set_selling=set_selling
|
||||||
|
|||||||
+13
-3
@@ -300,12 +300,22 @@ cafe24_oauth_tokens 저장
|
|||||||
추가 규칙:
|
추가 규칙:
|
||||||
|
|
||||||
- 빈 내용은 거부한다(상세페이지 전체를 날리는 실수 방지).
|
- 빈 내용은 거부한다(상세페이지 전체를 날리는 실수 방지).
|
||||||
- **PC/모바일을 구분하지 않는다.** `description` 과 `mobile_description` 에 항상 같은
|
- **PC/모바일을 구분하지 않는다.** 단, PUT 에 `mobile_description` 필드를 직접
|
||||||
HTML 을 쓴다(운영 방침). `separated_mobile_description` 값과 무관하며, 편집 화면에도
|
보내지 않는다 — 실물 확인 결과 그 필드를 보내는 순간 카페24가
|
||||||
모바일 소스를 따로 보여주지 않는다 — 한쪽만 바뀌어 어긋나는 사고가 없어진다.
|
`separated_mobile_description` 을 `'T'`(관리자 화면 "직접 등록")로 바꿔버린다.
|
||||||
|
대신 `separated_mobile_description: "F"` 만 지정하면 카페24가 모바일 값을 PC 와
|
||||||
|
자동으로 맞춰주면서 설정도 **"PC 상세설명과 동일하게 사용"으로 유지**된다
|
||||||
|
(`products.update_descriptions`). 편집 화면에도 모바일 소스를 따로 보여주지
|
||||||
|
않는다 — 한쪽만 바뀌어 어긋나는 사고가 없어진다.
|
||||||
분리 사용 상품의 모바일 내용이 PC 와 달랐다면 덮어쓰기 전에 **그 내용도 BACKUP
|
분리 사용 상품의 모바일 내용이 PC 와 달랐다면 덮어쓰기 전에 **그 내용도 BACKUP
|
||||||
revision 으로 남긴다**(백업이 없으면 되찾을 방법이 없다).
|
revision 으로 남긴다**(백업이 없으면 되찾을 방법이 없다).
|
||||||
- 실패해도 BACKUP 은 이미 남아 있으므로 오류 메시지에 버전 번호를 알려준다.
|
- 실패해도 BACKUP 은 이미 남아 있으므로 오류 메시지에 버전 번호를 알려준다.
|
||||||
|
- **적용 직후 짧게 재확인한다(`products.wait_for_description`).** 카페24 관리자
|
||||||
|
API(`GET /admin/products/{no}`)는 PUT 직후 몇 초간 이전 값을 돌려줄 때가
|
||||||
|
있다(쇼핑몰 화면에는 바로 반영됨 — 실물 관찰). 그 상태에서 다른 상품을 봤다가
|
||||||
|
돌아오면 우리 편집기만 "적용 안 된 것"처럼 보인다. 적용/예약 실행 직후
|
||||||
|
0.8초 간격으로 최대 3회 재조회해 새 값이 확인될 때까지 기다린 뒤 화면으로
|
||||||
|
돌아간다(실패해도 PUT 자체는 이미 성공했으므로 예외를 던지지 않는다).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user