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
+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