Files
dbx-main/app/integrations/cafe24/config.py
T
king 469de15998 feat(cafe24): 편집기에 상품 다이렉트 주소 + 클립보드 복사
상세 화면 상단에 고객이 보는 상세페이지 주소를 보여주고 「주소 복사」·「쇼핑몰에서
열기」를 붙였다.

    https://miras.co.kr/product/detail.html?product_no=119

도메인은 하드코딩하지 않고 CAFE24_SHOP_URL 로 받는다. 커스텀 도메인은 mall_id 로
알 수 없기 때문이다. 미설정 시 카페24 기본 도메인(https://<mall_id>.cafe24.com)으로
대체해 환경변수가 없어도 항상 유효한 주소가 나온다. 스킴 누락·끝 슬래시도 정규화한다.

주소 칸은 readonly <input> 이라 기존 복사 버튼(data-copy)이 값을 그대로 읽어간다 —
클립보드 로직을 새로 만들지 않았다. 클립보드 API 가 막힌 환경에서는 입력칸 선택으로
대체되고, 칸을 클릭하면 전체 선택된다.

CAFE24_SHOP_URL 은 .env.example 과 문서에 설명을 함께 넣었다(신규 환경변수 규칙).

검증: 유닛테스트 61개 통과(신규 2개 — 스킴/슬래시 유무 3가지 입력에서 같은 주소,
미설정 시 카페24 도메인 대체). 렌더 확인 — 주소 표시·복사 버튼·새 창 열기(noopener),
주소를 만들 수 없으면 줄 자체를 숨김.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:46:04 +09:00

118 lines
4.9 KiB
Python

"""카페24 연동 설정 — 환경변수만 읽는다(하드코딩 금지).
app/main.py 의 env() 헬퍼와 동일하게 os.getenv + strip 규칙을 쓴다.
integrations 계층은 app.main 을 import 하지 않는다(순환 import 방지).
"""
from __future__ import annotations
import os
from dataclasses import dataclass
# 상품관리에 필요한 최소 scope. 향후 주문관리는 ORDER_SCOPES 를 더한다.
PRODUCT_SCOPES: tuple[str, ...] = ("mall.read_product", "mall.write_product")
ORDER_SCOPES: tuple[str, ...] = ("mall.read_order", "mall.write_order")
# 디자인(테마) — **요청하지 않는다.** 아래 확인 결과 때문이다.
#
# 운영몰에서 직접 호출해 확인한 사실(2026-08-14):
# GET /admin/themes → 조회는 가능(디자인 권한 필요)하지만 테마 "목록"뿐이며
# 스킨 파일 내용은 응답에 없다.
# GET /admin/themes/pages → No API found. (버전 2026-03-01 에 존재하지 않음)
# 스킨 HTML 파일(product/detail.html 등)을 읽거나 쓰는 엔드포인트는 없다.
#
# 즉 스킨에 박힌 값(예: 상단 공통 홍보를 제외할 상품번호 목록)을 API 로 고치는
# 방법은 없다. 스킨은 카페24 관리자에서 직접 관리해야 한다.
#
# 페이지에 코드를 주입하는 유일한 수단은 스크립트 태그(/admin/scripttags)이며,
# 디자인이 아니라 **mall.write_store(상점)** 권한이 필요하고 인라인 코드가 아닌
# 외부 HTTPS URL 만 받는다. 쓰려면 우리 서버에 공개 JS 엔드포인트가 필요하다.
DESIGN_SCOPES: tuple[str, ...] = ("mall.read_design", "mall.write_design")
STORE_SCOPES: tuple[str, ...] = ("mall.read_store", "mall.write_store")
# 실제로 요청하는 scope 묶음. 개발자센터 앱에 등록된 권한과 어긋나면 인증이
# 거부되므로, 여기에 추가할 때는 앱 권한도 함께 확인해야 한다.
# 쓰지 않는 권한은 요청하지 않는다 — 토큰이 유출돼도 피해 범위를 좁히기 위함.
DEFAULT_SCOPES: tuple[str, ...] = PRODUCT_SCOPES
DEFAULT_API_VERSION = "2026-03-01"
def _env(name: str, default: str = "") -> str:
value = os.getenv(name, "").strip()
return value if value else default
@dataclass(frozen=True)
class Cafe24Config:
mall_id: str
client_id: str
client_secret: str
redirect_uri: str
api_version: str
token_secret: str
scopes: tuple[str, ...]
# 쇼핑몰 표시 주소(고객이 보는 도메인). 커스텀 도메인은 mall_id 로 알 수 없어
# 환경변수로 받는다. 미설정 시 카페24 기본 도메인으로 대체한다.
shop_url: str = ""
@property
def configured(self) -> bool:
"""OAuth 를 시작할 수 있는 최소 조건."""
return bool(self.mall_id and self.client_id and self.client_secret and self.redirect_uri)
@property
def missing(self) -> list[str]:
"""설정 안내 화면에 표시할 미설정 환경변수 이름들."""
pairs = (
("CAFE24_MALL_ID", self.mall_id),
("CAFE24_CLIENT_ID", self.client_id),
("CAFE24_CLIENT_SECRET", self.client_secret),
("CAFE24_REDIRECT_URI", self.redirect_uri),
("CAFE24_TOKEN_SECRET", self.token_secret),
)
return [name for name, value in pairs if not value]
@property
def api_base(self) -> str:
return f"https://{self.mall_id}.cafe24api.com/api/v2"
@property
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:
"""환경변수에서 설정을 읽는다. 값이 없어도 예외를 던지지 않는다.
미설정 판단은 호출부가 `configured` / `missing` 으로 한다
(앱 기동을 막지 않기 위해 — 다른 모듈과 동일한 정책).
"""
return Cafe24Config(
mall_id=_env("CAFE24_MALL_ID"),
client_id=_env("CAFE24_CLIENT_ID"),
client_secret=_env("CAFE24_CLIENT_SECRET"),
redirect_uri=_env("CAFE24_REDIRECT_URI"),
api_version=_env("CAFE24_API_VERSION", DEFAULT_API_VERSION),
token_secret=_env("CAFE24_TOKEN_SECRET"),
scopes=scopes,
shop_url=_env("CAFE24_SHOP_URL"),
)