fix(cafe24): 앞 커밋에서 누락된 디자인파일 일반화 변경분 포함

config.py(Cafe24FtpConfig 일반화 + DESIGN_FILE_SPECS)·db.py(add/list_design_
revision)·router.py(design_files_router 연결)·_nav.html(탭 3개)·테스트·
.env.example·문서가 앞 커밋(8c2a9c4)에 함께 올라갔어야 하는데 스테이징 누락.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 14:50:58 +09:00
parent 8c2a9c47e1
commit e14d303b82
8 changed files with 140 additions and 64 deletions
+6
View File
@@ -29,11 +29,14 @@ from . import design_ftp, products
from .client import Cafe24Client
from .config import (
DEFAULT_SCOPES,
DESIGN_FILE_SPECS,
DESIGN_SCOPES,
ORDER_SCOPES,
PRODUCT_SCOPES,
Cafe24Config,
Cafe24FtpConfig,
design_file_label,
design_file_path,
load_config,
load_ftp_config,
)
@@ -56,6 +59,9 @@ __all__ = [
"Cafe24FtpConfig",
"load_ftp_config",
"design_ftp",
"DESIGN_FILE_SPECS",
"design_file_label",
"design_file_path",
"TokenService",
"TokenBundle",
"load_config",
+34 -10
View File
@@ -118,7 +118,8 @@ def load_config(*, scopes: tuple[str, ...] = DEFAULT_SCOPES) -> Cafe24Config:
# ════════════════════════════════════════════════════════════
# 디자인 보관함 FTP — 모바일 스와이프(product-swiper.js) 편집용
# 디자인 보관함 FTP — 상품 API 로 못 건드리는 스킨 파일 편집용
# (모바일 스와이프 product-swiper.js, PC/모바일 상품상세 템플릿 detail.html 등)
# ════════════════════════════════════════════════════════════
# Admin API(OAuth)로는 스킨 파일을 읽거나 쓸 수 없다(위 설명 참고). 카페24가
# 스킨 파일에 제공하는 유일한 프로그램적 접근은 "디자인 보관함" FTP 계정이다
@@ -133,12 +134,10 @@ class Cafe24FtpConfig:
port: int
user: str
password: str
# 편집 대상 파일의 절대 경로(디자인 보관함 기준). 스킨 번호가 바뀌면 함께 바뀐다.
swiper_path: str
@property
def configured(self) -> bool:
return bool(self.host and self.user and self.password and self.swiper_path)
return bool(self.host and self.user and self.password)
@property
def missing(self) -> list[str]:
@@ -146,7 +145,6 @@ class Cafe24FtpConfig:
("CAFE24_FTP_HOST", self.host),
("CAFE24_FTP_USER", self.user),
("CAFE24_FTP_PASSWORD", self.password),
("CAFE24_SWIPER_FTP_PATH", self.swiper_path),
)
return [name for name, value in pairs if not value]
@@ -166,9 +164,35 @@ def load_ftp_config(*, mall_id: str = "") -> Cafe24FtpConfig:
port=port,
user=_env("CAFE24_FTP_USER"),
password=_env("CAFE24_FTP_PASSWORD"),
# 실물 확인된 기본 경로(미라스키친 mobile11 스킨). 스킨을 바꾸면
# CAFE24_SWIPER_FTP_PATH 로 덮어쓴다.
swiper_path=_env(
"CAFE24_SWIPER_FTP_PATH", "/sde_design/mobile11/product-swiper/product-swiper.js"
),
)
# FTP 로 편집하는 디자인 파일들 — key: (표시 이름, 경로 환경변수 이름, 기본 경로).
# 기본 경로는 실물 확인된 값(미라스키친: 모바일 mobile11 / PC skin11 스킨)이다.
# 스킨을 바꾸면 각 CAFE24_*_FTP_PATH 로 덮어쓴다.
DESIGN_FILE_SPECS: dict[str, tuple[str, str, str]] = {
"swiper": (
"모바일 스와이프",
"CAFE24_SWIPER_FTP_PATH",
"/sde_design/mobile11/product-swiper/product-swiper.js",
),
"mobile_detail": (
"모바일 상품상세",
"CAFE24_MOBILE_DETAIL_FTP_PATH",
"/sde_design/mobile11/product/detail.html",
),
"pc_detail": (
"PC 상품상세",
"CAFE24_PC_DETAIL_FTP_PATH",
"/sde_design/skin11/product/detail.html",
),
}
def design_file_label(key: str) -> str:
return DESIGN_FILE_SPECS[key][0]
def design_file_path(key: str) -> str:
_, env_name, default = DESIGN_FILE_SPECS[key]
return _env(env_name, default)
+12 -10
View File
@@ -320,14 +320,16 @@ class Cafe24Store:
return self._serialize(row)
# ════════════════════════════════════════════════════════════
# 모바일 스와이프(product-swiper.js) 버전 (append-only)
# 상품이 아니라 디자인 보관함 FTP 파일이라 별도 테이블을 쓴다
# (cafe24_product_revisions.product_no 는 NOT NULL).
# 디자인 보관함 FTP 파일 버전 (append-only) — 모바일 스와이프
# (product-swiper.js), PC/모바일 상품상세 템플릿(detail.html) 등.
# 상품이 아니라 FTP 파일이라 별도 테이블을 쓴다
# (cafe24_product_revisions.product_no 는 NOT NULL). file_key 로 파일을
# 구분한다 — "swiper" / "mobile_detail" / "pc_detail" (config.py 의
# DESIGN_FILE_SPECS 키와 일치해야 한다).
# ════════════════════════════════════════════════════════════
SWIPER_FILE_KEY = "product-swiper"
def add_swiper_revision(
self, *, content: str, revision_type: str, memo: str = "", created_by: str = ""
def add_design_revision(
self, *, file_key: str, content: str, revision_type: str,
memo: str = "", created_by: str = "",
) -> int:
with self._pool.connection() as conn:
row = conn.execute(
@@ -338,7 +340,7 @@ class Cafe24Store:
RETURNING id
""",
(
self.SWIPER_FILE_KEY,
file_key,
content or "",
store.normalize_revision_type(revision_type),
(memo or "")[:500],
@@ -347,7 +349,7 @@ class Cafe24Store:
).fetchone()
return int(row["id"]) if row else 0
def list_swiper_revisions(self, *, limit: int = 20) -> list[dict[str, Any]]:
def list_design_revisions(self, file_key: str, *, limit: int = 20) -> list[dict[str, Any]]:
"""버전 목록. content 는 커질 수 있어 길이만 계산해서 준다."""
with self._pool.connection() as conn:
rows = conn.execute(
@@ -359,7 +361,7 @@ class Cafe24Store:
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
(self.SWIPER_FILE_KEY, max(1, min(int(limit), 200))),
(file_key, max(1, min(int(limit), 200))),
).fetchall()
return [self._serialize(r) for r in rows]
+5 -5
View File
@@ -12,9 +12,9 @@
routes_products 상품 목록/검색 · 상세설명 조회·편집·적용
routes_schedules 예약 등록·목록·취소 (실행은 worker.py)
routes_system 연결(OAuth)·상태·API 로그·작업 로그
routes_swiper 모바일 스와이프(product-swiper.js) 편집 — 카페24 디자인
보관함 FTP (Admin API 가 아니다. app/integrations/cafe24/
design_ftp.py 참고)
routes_design_files 모바일 스와이프·PC/모바일 상품상세 템플릿 편집 —
카페24 디자인 보관함 FTP (Admin API 가 아니다.
app/integrations/cafe24/design_ftp.py 참고)
"""
from __future__ import annotations
@@ -23,9 +23,9 @@ import logging
from fastapi import APIRouter
from .routes_design_files import design_files_router
from .routes_products import products_router
from .routes_schedules import schedules_router
from .routes_swiper import swiper_router
from .routes_system import system_router
logger = logging.getLogger("cafe24.router")
@@ -34,7 +34,7 @@ router = APIRouter(prefix="/cafe24", tags=["cafe24"])
router.include_router(products_router)
router.include_router(schedules_router)
router.include_router(swiper_router)
router.include_router(design_files_router)
router.include_router(system_router)
@@ -1,9 +1,13 @@
{# 카페24 모듈 공용 상단 탭. active_tab: products | swiper | schedules | system #}
{# 카페24 모듈 공용 상단 탭. active_tab: products | design:pc_detail | design:mobile_detail | design:swiper | schedules | system #}
<div class="erp-page-actions" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<a class="erp-btn {% if active_tab=='products' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
href="/cafe24/">상품관리</a>
<a class="erp-btn {% if active_tab=='swiper' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
href="/cafe24/swiper">모바일 스와이프</a>
<a class="erp-btn {% if active_tab=='design:pc_detail' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
href="/cafe24/design/pc_detail">PC 상품상세</a>
<a class="erp-btn {% if active_tab=='design:mobile_detail' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
href="/cafe24/design/mobile_detail">모바일 상품상세</a>
<a class="erp-btn {% if active_tab=='design:swiper' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
href="/cafe24/design/swiper">모바일 스와이프</a>
<a class="erp-btn {% if active_tab=='schedules' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
href="/cafe24/schedules">예약관리</a>
<a class="erp-btn {% if active_tab=='system' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}"
+35 -16
View File
@@ -665,7 +665,7 @@ def test_update_product_sends_flags_and_html():
# ════════════════════════════════════════════════════════════
# 모바일 스와이프(product-swiper.js) — 디자인 보관함 FTP
# 디자인 보관함 FTP — 모바일 스와이프 / PC·모바일 상품상세 템플릿
# ════════════════════════════════════════════════════════════
def test_ftp_config_host_falls_back_to_mall_id():
"""CAFE24_FTP_HOST 미설정 시 카페24 규칙({mall_id}.ftp.cafe24.com)을 쓴다."""
@@ -680,16 +680,37 @@ def test_ftp_config_host_falls_back_to_mall_id():
def test_ftp_config_missing_lists_absent_vars():
cfg = cfgmod.Cafe24FtpConfig(host="", port=21, user="", password="", swiper_path="")
cfg = cfgmod.Cafe24FtpConfig(host="", port=21, user="", password="")
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 cfg.missing == ["CAFE24_FTP_HOST", "CAFE24_FTP_USER", "CAFE24_FTP_PASSWORD"]
full = cfgmod.Cafe24FtpConfig(host="h", port=21, user="u", password="p")
assert full.configured is True
assert full.missing == []
def test_design_file_specs_cover_all_three_files():
"""상단 탭 3개(모바일 스와이프/모바일 상품상세/PC 상품상세)가 모두 등록돼 있는가."""
assert set(cfgmod.DESIGN_FILE_SPECS) == {"swiper", "mobile_detail", "pc_detail"}
assert cfgmod.design_file_label("swiper") == "모바일 스와이프"
assert cfgmod.design_file_label("mobile_detail") == "모바일 상품상세"
assert cfgmod.design_file_label("pc_detail") == "PC 상품상세"
# 기본 경로(env 미설정 시) — 실물 확인된 값
assert cfgmod.design_file_path("mobile_detail") == "/sde_design/mobile11/product/detail.html"
assert cfgmod.design_file_path("pc_detail") == "/sde_design/skin11/product/detail.html"
def test_design_file_path_env_override():
saved = os.environ.get("CAFE24_PC_DETAIL_FTP_PATH")
os.environ["CAFE24_PC_DETAIL_FTP_PATH"] = "/sde_design/skin99/product/detail.html"
try:
assert cfgmod.design_file_path("pc_detail") == "/sde_design/skin99/product/detail.html"
finally:
if saved is None:
os.environ.pop("CAFE24_PC_DETAIL_FTP_PATH", None)
else:
os.environ["CAFE24_PC_DETAIL_FTP_PATH"] = saved
class _FakeFTP:
"""ftplib.FTP 대역 — 실제 소켓 없이 RETR/STOR 명령만 흉내낸다."""
@@ -724,19 +745,17 @@ class _FakeFTP:
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")}
path = "/sde_design/mobile11/product-swiper/product-swiper.js"
_FakeFTP.files = {path: "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)
cfg = cfgmod.Cafe24FtpConfig(host="h", port=21, user="u", password="p")
content = design_ftp.read_text_file(cfg, 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"
design_ftp.write_text_file(cfg, path, "new content")
assert design_ftp.read_text_file(cfg, path) == "new content"
finally:
ftplib.FTP = saved
@@ -749,9 +768,9 @@ def test_design_ftp_missing_file_raises_cafe24_error():
saved = ftplib.FTP
ftplib.FTP = _FakeFTP
try:
cfg = cfgmod.Cafe24FtpConfig(host="h", port=21, user="u", password="p", swiper_path="/missing.js")
cfg = cfgmod.Cafe24FtpConfig(host="h", port=21, user="u", password="p")
try:
design_ftp.read_text_file(cfg, cfg.swiper_path)
design_ftp.read_text_file(cfg, "/missing.js")
raise AssertionError("Cafe24FtpError 가 났어야 한다")
except Cafe24FtpError:
pass