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:
2026-09-15 13:49:05 +09:00
parent d6424c2272
commit 20f3f0f7df
6 changed files with 100 additions and 25 deletions
+48 -3
View File
@@ -17,6 +17,7 @@
from __future__ import annotations
import time
from dataclasses import dataclass
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))
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:
"""카페24는 boolean 을 'T'/'F' 문자열로 받는다."""
return "T" if flag else "F"
@@ -158,6 +187,7 @@ def build_update_payload(
*,
description: str | None = None,
mobile_description: str | None = None,
separated_mobile_description: str | None = None,
product_name: str | None = None,
display: bool | None = None,
selling: bool | None = None,
@@ -168,6 +198,11 @@ def build_update_payload(
`None` 인 항목은 payload 에 넣지 않는다 = 그 필드를 건드리지 않는다.
예약에서 "진열만 켜기"처럼 상세설명 없이 상태만 바꾸는 경우가 있으므로
description 도 생략할 수 있다.
⚠️ `mobile_description` 을 명시적으로 보내면 카페24가 `separated_mobile_description`
'T'(관리자 화면 "직접 등록")로 바꿔버린다(실물 확인). "PC 상세설명과 동일"
유지하려면 `mobile_description` 은 보내지 말고 `separated_mobile_description="F"`
만 지정할 것 — `update_descriptions` 가 이 방식을 쓴다.
"""
request: dict[str, Any] = {}
if description is not None:
@@ -176,6 +211,8 @@ def build_update_payload(
request["product_name"] = product_name
if mobile_description is not None:
request["mobile_description"] = mobile_description
if separated_mobile_description is not None:
request["separated_mobile_description"] = separated_mobile_description
if display is not None:
request["display"] = _flag_value(display)
if selling is not None:
@@ -192,6 +229,7 @@ def update_product(
*,
description: str | None = None,
mobile_description: str | None = None,
separated_mobile_description: str | None = None,
product_name: str | None = None,
display: bool | None = None,
selling: bool | None = None,
@@ -207,6 +245,7 @@ def update_product(
payload = build_update_payload(
description=description,
mobile_description=mobile_description,
separated_mobile_description=separated_mobile_description,
product_name=product_name,
display=display,
selling=selling,
@@ -225,15 +264,21 @@ def update_descriptions(
product_no: int,
*,
description: str,
mobile_description: str | None = None,
shop_no: int | None = None,
) -> dict[str, Any]:
"""상세설명 교체하는 지름길. 실제 전송은 `update_product` 가 한다."""
"""상세설명 교체 + "PC 상세설명과 동일" 강제.
`mobile_description` 필드는 보내지 않는다 — 보내는 순간 카페24 관리자
화면의 모바일 상세설명 설정이 "직접 등록"으로 바뀌어버리기 때문이다(실물
확인). 대신 `separated_mobile_description="F"` 만 지정하면 카페24가 모바일
값을 PC 와 자동으로 맞춰주면서 설정도 "PC 상세설명과 동일하게 사용"으로
유지된다.
"""
return update_product(
client,
product_no,
description=description,
mobile_description=mobile_description,
separated_mobile_description="F",
shop_no=shop_no,
)