feat(cafe24): 모바일 스와이프(product-swiper.js) 편집 화면 추가 (FTP)
- 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>
This commit is contained in:
@@ -7,6 +7,7 @@ DB/네트워크 없이 검증한다(가짜 저장소 + refresh 함수 주입).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ftplib
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
@@ -663,6 +664,101 @@ def test_update_product_sends_flags_and_html():
|
||||
assert call["json"] == {"request": {"description": "<p>새</p>", "display": "F"}}
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 모바일 스와이프(product-swiper.js) — 디자인 보관함 FTP
|
||||
# ════════════════════════════════════════════════════════════
|
||||
def test_ftp_config_host_falls_back_to_mall_id():
|
||||
"""CAFE24_FTP_HOST 미설정 시 카페24 규칙({mall_id}.ftp.cafe24.com)을 쓴다."""
|
||||
saved = os.environ.pop("CAFE24_FTP_HOST", None)
|
||||
try:
|
||||
cfg = cfgmod.load_ftp_config(mall_id="testmall")
|
||||
assert cfg.host == "testmall.ftp.cafe24.com"
|
||||
assert cfg.port == cfgmod.DEFAULT_FTP_PORT
|
||||
finally:
|
||||
if saved is not None:
|
||||
os.environ["CAFE24_FTP_HOST"] = saved
|
||||
|
||||
|
||||
def test_ftp_config_missing_lists_absent_vars():
|
||||
cfg = cfgmod.Cafe24FtpConfig(host="", port=21, user="", password="", swiper_path="")
|
||||
assert cfg.configured is False
|
||||
assert cfg.missing == [
|
||||
"CAFE24_FTP_HOST", "CAFE24_FTP_USER", "CAFE24_FTP_PASSWORD", "CAFE24_SWIPER_FTP_PATH",
|
||||
]
|
||||
full = cfgmod.Cafe24FtpConfig(host="h", port=21, user="u", password="p", swiper_path="/x.js")
|
||||
assert full.configured is True
|
||||
assert full.missing == []
|
||||
|
||||
|
||||
class _FakeFTP:
|
||||
"""ftplib.FTP 대역 — 실제 소켓 없이 RETR/STOR 명령만 흉내낸다."""
|
||||
|
||||
files: dict[str, bytes] = {}
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def connect(self, host, port, timeout=None):
|
||||
self.calls.append(("connect", host, port))
|
||||
|
||||
def login(self, user, password):
|
||||
self.calls.append(("login", user, password))
|
||||
|
||||
def set_pasv(self, value):
|
||||
pass
|
||||
|
||||
def retrbinary(self, cmd, callback):
|
||||
path = cmd.split(" ", 1)[1]
|
||||
if path not in self.files:
|
||||
raise ftplib.error_perm("550 No such file")
|
||||
callback(self.files[path])
|
||||
|
||||
def storbinary(self, cmd, fp):
|
||||
path = cmd.split(" ", 1)[1]
|
||||
self.files[path] = fp.read()
|
||||
|
||||
def quit(self):
|
||||
self.calls.append(("quit",))
|
||||
|
||||
|
||||
def test_design_ftp_read_and_write_roundtrip():
|
||||
from app.integrations.cafe24 import design_ftp
|
||||
|
||||
_FakeFTP.files = {"/sde_design/mobile11/product-swiper/product-swiper.js": "old".encode("utf-8")}
|
||||
saved = ftplib.FTP
|
||||
ftplib.FTP = _FakeFTP
|
||||
try:
|
||||
cfg = cfgmod.Cafe24FtpConfig(
|
||||
host="h", port=21, user="u", password="p",
|
||||
swiper_path="/sde_design/mobile11/product-swiper/product-swiper.js",
|
||||
)
|
||||
content = design_ftp.read_text_file(cfg, cfg.swiper_path)
|
||||
assert content == "old"
|
||||
|
||||
design_ftp.write_text_file(cfg, cfg.swiper_path, "new content")
|
||||
assert design_ftp.read_text_file(cfg, cfg.swiper_path) == "new content"
|
||||
finally:
|
||||
ftplib.FTP = saved
|
||||
|
||||
|
||||
def test_design_ftp_missing_file_raises_cafe24_error():
|
||||
from app.integrations.cafe24 import design_ftp
|
||||
from app.integrations.cafe24.errors import Cafe24FtpError
|
||||
|
||||
_FakeFTP.files = {}
|
||||
saved = ftplib.FTP
|
||||
ftplib.FTP = _FakeFTP
|
||||
try:
|
||||
cfg = cfgmod.Cafe24FtpConfig(host="h", port=21, user="u", password="p", swiper_path="/missing.js")
|
||||
try:
|
||||
design_ftp.read_text_file(cfg, cfg.swiper_path)
|
||||
raise AssertionError("Cafe24FtpError 가 났어야 한다")
|
||||
except Cafe24FtpError:
|
||||
pass
|
||||
finally:
|
||||
ftplib.FTP = saved
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 예약 worker — 성공/재시도/최종실패
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user