93037411fb
- product-swiper.js는 상품이 아니라 카페24 "디자인 보관함" 스킨 파일이라
Admin API(OAuth)로는 접근 불가 — 실물 확인(스크린샷) 결과 디자인 보관함
FTP 계정(호스트/포트/ID/PW, OAuth와 별개)으로만 읽기/쓰기 가능.
app/integrations/cafe24/design_ftp.py 를 표준 ftplib 로 새로 추가.
- 상단 탭에 "모바일 스와이프" 버튼 추가. 편집 화면(swiper.html)은 상세페이지
편집기(_editor.html)와 완전히 같은 문법강조·색상·단축키 JS를 그대로 옮겨
씀(요청사항) — 상품 전용 UI(목록·진열/판매·예약)는 제외.
- 적용 순서도 상세페이지와 동일한 원칙: FTP에서 현재값 재조회 → BACKUP →
지문 대조(충돌 거부) → FTP 쓰기 → MANUAL 버전 + 감사로그(apply_swiper).
textarea의 CRLF는 적용 전 LF로 정규화(안 하면 매번 "변경됨"으로 오판).
- 버전 이력은 cafe24_product_revisions(product_no NOT NULL)에 넣을 수 없어
새 테이블 cafe24_swiper_revisions 추가(scripts/sql/cafe24_db_003_*.sql,
서버에서 별도 실행 필요).
- 신규 env: CAFE24_FTP_HOST(미설정 시 {mall_id}.ftp.cafe24.com)/PORT/USER/
PASSWORD, CAFE24_SWIPER_FTP_PATH. .env.example·문서 갱신.
- 유닛테스트 4건 추가(FTP config 기본값, 가짜 FTP로 읽기/쓰기 왕복·오류 처리).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
50 lines
1.6 KiB
Python
50 lines
1.6 KiB
Python
"""카페24 연동 공통 예외.
|
|
|
|
라우터/서비스는 httpx 예외를 직접 다루지 않고 여기 정의된 타입만 잡는다.
|
|
모든 메시지는 사용자에게 그대로 노출될 수 있으므로 토큰/시크릿을 담지 않는다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class Cafe24Error(Exception):
|
|
"""카페24 연동 최상위 예외."""
|
|
|
|
|
|
class Cafe24ConfigError(Cafe24Error):
|
|
"""CAFE24_* 환경변수 미설정 등 설정 문제."""
|
|
|
|
|
|
class Cafe24AuthError(Cafe24Error):
|
|
"""인증 실패 — 토큰 없음/만료/refresh 불가. 재인증이 필요하다."""
|
|
|
|
def __init__(self, message: str, *, needs_reauth: bool = False):
|
|
super().__init__(message)
|
|
self.needs_reauth = needs_reauth
|
|
|
|
|
|
class Cafe24RateLimitError(Cafe24Error):
|
|
"""429 Too Many Requests. retry_after 초 뒤 재시도 가능."""
|
|
|
|
def __init__(self, message: str, *, retry_after: float = 1.0):
|
|
super().__init__(message)
|
|
self.retry_after = retry_after
|
|
|
|
|
|
class Cafe24FtpError(Cafe24Error):
|
|
"""디자인 보관함 FTP 연결/전송 실패 (product-swiper.js 편집 등)."""
|
|
|
|
|
|
class Cafe24ApiError(Cafe24Error):
|
|
"""그 외 API 오류(4xx/5xx). status 로 재시도 가능 여부를 판단한다."""
|
|
|
|
def __init__(self, message: str, *, status: int = 0, endpoint: str = ""):
|
|
super().__init__(message)
|
|
self.status = status
|
|
self.endpoint = endpoint
|
|
|
|
@property
|
|
def retryable(self) -> bool:
|
|
"""5xx 와 타임아웃(status=0)만 재시도 대상. 4xx 는 고쳐야 할 요청."""
|
|
return self.status == 0 or self.status >= 500
|