diff --git a/.env.example b/.env.example index 1174609..1b289ae 100644 --- a/.env.example +++ b/.env.example @@ -81,6 +81,12 @@ CUSTOMER_ORDER_LIST_URL=/orderlist/ # CAFE24_REDIRECT_URI=https://dbx.no1king.freeddns.org/cafe24/oauth/callback # CAFE24_API_VERSION=2026-03-01 # +# 고객이 보는 쇼핑몰 주소. 상품관리 화면에서 상세페이지 다이렉트 주소를 만들 때 쓴다 +# (예: https://miras.co.kr/product/detail.html?product_no=119). +# 커스텀 도메인은 mall_id 로 알 수 없어 직접 지정해야 한다. +# 미설정 시 카페24 기본 도메인(https://.cafe24.com)으로 대체된다. +# CAFE24_SHOP_URL=https://miras.co.kr +# # access/refresh token 을 DB 에 Fernet 암호화해서 저장할 때 쓰는 키. # openssl rand -hex 32 로 생성. ⚠️ 값을 바꾸면 기존 토큰을 복호화할 수 없어 # 카페24 재연결(재인증)이 필요하다. diff --git a/app/integrations/cafe24/config.py b/app/integrations/cafe24/config.py index f88472f..55878de 100644 --- a/app/integrations/cafe24/config.py +++ b/app/integrations/cafe24/config.py @@ -52,6 +52,9 @@ class Cafe24Config: api_version: str token_secret: str scopes: tuple[str, ...] + # 쇼핑몰 표시 주소(고객이 보는 도메인). 커스텀 도메인은 mall_id 로 알 수 없어 + # 환경변수로 받는다. 미설정 시 카페24 기본 도메인으로 대체한다. + shop_url: str = "" @property def configured(self) -> bool: @@ -78,6 +81,23 @@ class Cafe24Config: def scope_param(self) -> str: return ",".join(self.scopes) + @property + def shop_base(self) -> str: + """고객이 보는 쇼핑몰 주소(끝 슬래시 없음). + + `CAFE24_SHOP_URL` 이 없으면 카페24 기본 도메인을 쓴다 — 커스텀 도메인을 + 모르더라도 항상 유효한 주소가 나온다. + """ + url = (self.shop_url or "").strip().rstrip("/") + if url: + return url if "://" in url else f"https://{url}" + return f"https://{self.mall_id}.cafe24.com" if self.mall_id else "" + + def product_url(self, product_no: int | str) -> str: + """상품 상세페이지 다이렉트 주소.""" + base = self.shop_base + return f"{base}/product/detail.html?product_no={product_no}" if base else "" + def load_config(*, scopes: tuple[str, ...] = DEFAULT_SCOPES) -> Cafe24Config: """환경변수에서 설정을 읽는다. 값이 없어도 예외를 던지지 않는다. @@ -93,4 +113,5 @@ def load_config(*, scopes: tuple[str, ...] = DEFAULT_SCOPES) -> Cafe24Config: api_version=_env("CAFE24_API_VERSION", DEFAULT_API_VERSION), token_secret=_env("CAFE24_TOKEN_SECRET"), scopes=scopes, + shop_url=_env("CAFE24_SHOP_URL"), ) diff --git a/app/modules/cafe24/routes_products.py b/app/modules/cafe24/routes_products.py index 04425aa..416e8d0 100644 --- a/app/modules/cafe24/routes_products.py +++ b/app/modules/cafe24/routes_products.py @@ -131,6 +131,8 @@ def _editor_ctx(st: Any, product_no: int) -> dict[str, Any]: info = products.normalize_product(product) if product else {} return { "product_no": product_no, + # 고객이 보는 상세페이지 주소 (CAFE24_SHOP_URL, 없으면 카페24 기본 도메인) + "product_url": api.config.product_url(product_no), "info": { **info, "price": str(product.get("price") or ""), diff --git a/app/modules/cafe24/templates/cafe24/_editor.html b/app/modules/cafe24/templates/cafe24/_editor.html index da64f41..53bc47b 100644 --- a/app/modules/cafe24/templates/cafe24/_editor.html +++ b/app/modules/cafe24/templates/cafe24/_editor.html @@ -29,6 +29,18 @@ +{# 고객이 보는 상세페이지 주소. readonly input 이라 기존 복사 버튼(data-copy)이 + 그대로 동작한다(값을 box.value 로 읽는다). #} +{% if product_url %} +
+ + + 쇼핑몰에서 열기 +
+{% endif %} + {% if desc %}
+ {% endblock %} {% block content %} diff --git a/app/modules/cafe24/templates/cafe24/schedules.html b/app/modules/cafe24/templates/cafe24/schedules.html index 6d0e9dc..f1b4809 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 16782a8..2f99d16 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/app/modules/cafe24/tests/test_cafe24.py b/app/modules/cafe24/tests/test_cafe24.py index 239af1e..8a383bc 100644 --- a/app/modules/cafe24/tests/test_cafe24.py +++ b/app/modules/cafe24/tests/test_cafe24.py @@ -134,6 +134,36 @@ def test_config_basics(): assert "mall.write_product" in config.scope_param +def test_product_url_uses_shop_url_env(): + """다이렉트 주소는 CAFE24_SHOP_URL 기준. 스킴·끝 슬래시가 없어도 맞춘다.""" + saved = os.environ.get("CAFE24_SHOP_URL") + try: + for value in ("https://miras.co.kr", "miras.co.kr", "https://miras.co.kr/"): + os.environ["CAFE24_SHOP_URL"] = value + config = _config() + assert ( + config.product_url(119) + == "https://miras.co.kr/product/detail.html?product_no=119" + ), value + finally: + if saved is None: + os.environ.pop("CAFE24_SHOP_URL", None) + else: + os.environ["CAFE24_SHOP_URL"] = saved + + +def test_product_url_falls_back_to_cafe24_domain(): + """CAFE24_SHOP_URL 이 없어도 항상 유효한 주소가 나와야 한다.""" + saved = os.environ.pop("CAFE24_SHOP_URL", None) + try: + assert _config().product_url(119) == ( + "https://testmall.cafe24.com/product/detail.html?product_no=119" + ) + finally: + if saved is not None: + os.environ["CAFE24_SHOP_URL"] = saved + + def test_authorize_url_has_state_and_no_secret(): url = oauth.build_authorize_url(_config(), state="STATE123") assert url.startswith("https://testmall.cafe24api.com/api/v2/oauth/authorize?") diff --git a/app/static/cafe24.css b/app/static/cafe24.css index d96de72..4446d1d 100644 --- a/app/static/cafe24.css +++ b/app/static/cafe24.css @@ -624,3 +624,26 @@ color: var(--color-rich-black, #0a0a0a); background: var(--color-canvas-white, #fff); } + +/* ── 상품 다이렉트 주소 ── */ +.cf24-url-row { + display: flex; + gap: var(--sp-8, 8px); + align-items: center; + flex: 0 0 auto; + flex-wrap: wrap; +} + +.cf24-url { + flex: 1 1 260px; + min-width: 0; + height: 30px; + box-sizing: border-box; + padding: 0 var(--sp-10, 10px); + border: 1px solid var(--color-subtle-ash, #e5e5e5); + border-radius: var(--r-lg, 10px); + background: var(--color-ghost-gray, #f6f8fa); + color: var(--color-midtone-gray, #737373); + font-family: var(--font-geist-mono, ui-monospace, monospace); + font-size: 12px; +} diff --git a/docs/CAFE24_MODULE.md b/docs/CAFE24_MODULE.md index 59f4f6a..fc5c426 100644 --- a/docs/CAFE24_MODULE.md +++ b/docs/CAFE24_MODULE.md @@ -97,6 +97,10 @@ app/modules/cafe24/ ← 상품관리 모듈 덕분에 목록 전체가 대상이 된다. - **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지 않으므로 카페24 호출이 1회로 끝난다. JS 실패 시 각 행의 링크로 정상 동작한다. +- 편집기 상단에 **상품 다이렉트 주소**(고객이 보는 상세페이지 URL)와 「주소 복사」· + 「쇼핑몰에서 열기」를 둔다. 주소는 `CAFE24_SHOP_URL` 기준으로 만들고, 미설정 시 + 카페24 기본 도메인(`https://.cafe24.com`)으로 대체한다 — 커스텀 도메인은 + `mall_id` 로 알 수 없으므로 환경변수가 필요하다. - 편집 중 다른 상품을 클릭하거나 페이지를 벗어나면 **저장 안 됨 경고**가 뜬다. - **캐시 금지.** 화면·조각 응답에 `Cache-Control: no-store` 를 붙이고 조각 fetch 에도 `cache: "no-store"` 를 건다. 캐시된 조각이 다시 그려지면 카페24 관리자에서 값을 바꾼 @@ -318,6 +322,7 @@ CAFE24_CLIENT_SECRET=... CAFE24_REDIRECT_URI=https://dbx.no1king.freeddns.org/cafe24/oauth/callback CAFE24_API_VERSION=2026-03-01 CAFE24_TOKEN_SECRET= +CAFE24_SHOP_URL=https://miras.co.kr # 선택 — 다이렉트 주소용 커스텀 도메인 ``` ### 5-4. 재기동 + 권한 부여