feat(cafe24): PC/모바일 상품상세 템플릿 편집 추가, 디자인파일 편집기 통합
- 상단 탭에 "PC 상품상세"(/sde_design/skin11/product/detail.html), "모바일
상품상세"(/sde_design/mobile11/product/detail.html) 버튼 추가. 모바일
스와이프와 마찬가지로 상품 API가 아닌 디자인 보관함 FTP 파일이며, 상세페이지
편집기와 완전히 같은 문법강조·색상·단축키를 그대로 재사용.
- 세 파일(swiper/mobile_detail/pc_detail)이 화면·로직 100% 동일해 라우터를
routes_swiper.py → routes_design_files.py 로 일반화(/cafe24/design/{file_key}
하나로 공유). 템플릿도 swiper.html → design_editor.html 로 통합.
db.py 의 add_swiper_revision/list_swiper_revisions 도 file_key 를 받는
add_design_revision/list_design_revisions 로 일반화(테이블은 처음부터
file_key 컬럼으로 여러 파일을 담게 설계돼 있어 마이그레이션 불필요).
- PC/모바일 상품상세는 상품 1건이 아니라 전체 상품 페이지가 공유하는
템플릿이라 적용 확인 문구에 별도 경고 추가.
- 신규 env: CAFE24_MOBILE_DETAIL_FTP_PATH, CAFE24_PC_DETAIL_FTP_PATH
(FTP 호스트/계정은 기존 CAFE24_FTP_* 공유). .env.example·문서 갱신.
- 유닛테스트 갱신/추가: DESIGN_FILE_SPECS 3종 등록 확인, 경로 env 오버라이드.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
"""디자인 보관함 FTP 파일 편집 화면 — 상품 API 로 못 건드리는 스킨 파일 전용.
|
||||||
|
|
||||||
|
대상 파일(`app/integrations/cafe24/config.py` 의 `DESIGN_FILE_SPECS`):
|
||||||
|
swiper 모바일 스와이프 product-swiper.js
|
||||||
|
mobile_detail 모바일 상품상세 스킨 템플릿 detail.html
|
||||||
|
pc_detail PC 상품상세 스킨 템플릿 detail.html
|
||||||
|
|
||||||
|
상세페이지 편집기(routes_products.py)와 **같은** 문법강조 편집기·색상·단축키를
|
||||||
|
그대로 쓴다(요청사항: 모든 편집 기능이 상세페이지 소스 수정 기능과 같아야 한다).
|
||||||
|
다만 대상이 상품이 아니라 파일 1개(FTP)라서 목록·진열/판매·예약 같은 상품 전용
|
||||||
|
기능은 없다 — 편집·적용(백업 포함)·버전 이력만 있다. 세 파일 모두 화면·로직이
|
||||||
|
완전히 같아서 `file_key` 하나로 라우트를 공유한다(products.html 은 상품마다
|
||||||
|
다른 데이터를 다루지만, 여기는 파일마다 경로만 다르고 나머지는 동일하다).
|
||||||
|
|
||||||
|
쓰기 순서는 상세페이지 적용과 동일한 원칙을 따른다:
|
||||||
|
FTP 에서 현재 내용을 다시 읽는다(로컬 값을 현재값으로 가정하지 않는다)
|
||||||
|
→ BACKUP 버전 저장 → 지문 대조(충돌 시 거부) → FTP 에 쓴다
|
||||||
|
→ MANUAL 버전 + 감사로그
|
||||||
|
|
||||||
|
핸들러는 `async def` 가 아니라 `def`(동기)다 — FTP 호출이 블로킹이므로
|
||||||
|
FastAPI 스레드풀에서 돌게 둔다(다른 cafe24 라우트와 동일한 규칙).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Form, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
|
from app.integrations.cafe24 import (
|
||||||
|
DESIGN_FILE_SPECS,
|
||||||
|
Cafe24FtpError,
|
||||||
|
design_file_label,
|
||||||
|
design_file_path,
|
||||||
|
design_ftp,
|
||||||
|
load_config,
|
||||||
|
load_ftp_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import store
|
||||||
|
from .common import base_ctx, guard
|
||||||
|
from .routes_products import _no_store
|
||||||
|
|
||||||
|
logger = logging.getLogger("cafe24.design_files")
|
||||||
|
|
||||||
|
design_files_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _ftp_config():
|
||||||
|
# FTP 호스트 기본값(`{mall_id}.ftp.cafe24.com`)을 만들기 위해 OAuth 쪽
|
||||||
|
# mall_id 를 재사용한다 — FTP 자체는 OAuth 와 별개 인증이다.
|
||||||
|
return load_ftp_config(mall_id=load_config().mall_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_known_key(file_key: str) -> None:
|
||||||
|
if file_key not in DESIGN_FILE_SPECS:
|
||||||
|
raise HTTPException(status_code=404, detail="알 수 없는 디자인 파일입니다.")
|
||||||
|
|
||||||
|
|
||||||
|
# 상품상세 템플릿(mobile_detail/pc_detail)은 상품 1건이 아니라 전체 상품
|
||||||
|
# 페이지가 공유하는 레이아웃이다 — 잘못 고치면 모든 상품에 영향을 준다.
|
||||||
|
# 스와이프(개별 스크립트)와 위험도가 달라 확인 문구를 따로 둔다.
|
||||||
|
_TEMPLATE_KEYS = {"mobile_detail", "pc_detail"}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_confirm_text(file_key: str) -> str:
|
||||||
|
base = "카페24 디자인 보관함(FTP)에 바로 반영됩니다. 적용할까요?"
|
||||||
|
if file_key in _TEMPLATE_KEYS:
|
||||||
|
base = "⚠ 전체 상품 페이지가 공유하는 템플릿입니다. " + base
|
||||||
|
return base + "\n\n직전 내용은 자동으로 백업되어 되돌릴 수 있습니다."
|
||||||
|
|
||||||
|
|
||||||
|
@design_files_router.get("/design/{file_key}", response_class=HTMLResponse)
|
||||||
|
def design_file_page(request: Request, file_key: str) -> HTMLResponse:
|
||||||
|
from app.main import render_template # noqa: WPS433
|
||||||
|
|
||||||
|
_require_known_key(file_key)
|
||||||
|
checked = guard(request)
|
||||||
|
if not isinstance(checked, tuple):
|
||||||
|
return checked
|
||||||
|
st, user = checked
|
||||||
|
|
||||||
|
path = design_file_path(file_key)
|
||||||
|
ftp_config = _ftp_config()
|
||||||
|
content = ""
|
||||||
|
error = ""
|
||||||
|
if not ftp_config.configured:
|
||||||
|
error = "카페24 디자인 FTP 설정이 필요합니다: " + ", ".join(ftp_config.missing)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
content = design_ftp.read_text_file(ftp_config, path)
|
||||||
|
except Cafe24FtpError as exc:
|
||||||
|
error = str(exc)
|
||||||
|
logger.warning("디자인 파일 조회 실패(%s): %s", file_key, exc)
|
||||||
|
|
||||||
|
ctx = base_ctx(request, user, active_tab=f"design:{file_key}")
|
||||||
|
ctx.update(
|
||||||
|
{
|
||||||
|
"page_title": design_file_label(file_key),
|
||||||
|
"page_subtitle": f"{path} (카페24 디자인 보관함)",
|
||||||
|
"file_key": file_key,
|
||||||
|
"file_path": path,
|
||||||
|
"content": content,
|
||||||
|
"fingerprint": store.fingerprint(content) if not error else "",
|
||||||
|
"error": error,
|
||||||
|
"apply_confirm": _apply_confirm_text(file_key),
|
||||||
|
"revisions": st.list_design_revisions(file_key, limit=20),
|
||||||
|
"flash": request.query_params.get("msg", ""),
|
||||||
|
"flash_error": request.query_params.get("err", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return _no_store(render_template(request, "cafe24/design_editor.html", ctx))
|
||||||
|
|
||||||
|
|
||||||
|
@design_files_router.post("/design/{file_key}/apply")
|
||||||
|
def design_file_apply(
|
||||||
|
request: Request,
|
||||||
|
file_key: str,
|
||||||
|
content: str = Form(...),
|
||||||
|
base_fingerprint: str = Form(""),
|
||||||
|
memo: str = Form(""),
|
||||||
|
) -> RedirectResponse:
|
||||||
|
_require_known_key(file_key)
|
||||||
|
checked = guard(request)
|
||||||
|
if not isinstance(checked, tuple):
|
||||||
|
return checked
|
||||||
|
st, user = checked
|
||||||
|
actor = str(user.get("email") or "")
|
||||||
|
back = f"/cafe24/design/{file_key}"
|
||||||
|
|
||||||
|
path = design_file_path(file_key)
|
||||||
|
ftp_config = _ftp_config()
|
||||||
|
if not ftp_config.configured:
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"{back}?err=" + "FTP 설정이 필요합니다: " + ", ".join(ftp_config.missing),
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 브라우저 textarea 는 줄바꿈을 CRLF 로 보낸다 — 그대로 저장하면 실제 수정이
|
||||||
|
# 없어도 매번 파일 전체의 줄바꿈이 바뀌어(지문 비교·"변경 없음" 판정이 어긋난다).
|
||||||
|
submitted = content.replace("\r\n", "\n")
|
||||||
|
if not submitted.strip():
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"{back}?err=" + "내용이 비어 있습니다. 파일을 비우려면 FTP 로 직접 하세요.",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = design_ftp.read_text_file(ftp_config, path)
|
||||||
|
except Cafe24FtpError as exc:
|
||||||
|
st.log_audit(
|
||||||
|
actor=actor, action=f"apply_design:{file_key}", result="FAIL",
|
||||||
|
detail=f"현재값 조회 실패: {exc}",
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=f"{back}?err=현재 파일을 읽지 못해 중단했습니다: {exc}", status_code=303)
|
||||||
|
|
||||||
|
backup_id = st.add_design_revision(
|
||||||
|
file_key=file_key, content=current, revision_type=store.REVISION_BACKUP,
|
||||||
|
memo="적용 직전 자동 백업", created_by=actor,
|
||||||
|
)
|
||||||
|
|
||||||
|
if base_fingerprint and base_fingerprint != store.fingerprint(current):
|
||||||
|
st.log_audit(
|
||||||
|
actor=actor, action=f"apply_design:{file_key}", revision_id=backup_id,
|
||||||
|
result="FAIL", detail="충돌 — 편집 중 파일이 변경됨",
|
||||||
|
)
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"{back}?err=편집하는 동안 파일이 변경되었습니다. 새로고침해 현재 내용을 확인한 뒤 다시 적용하세요.",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
if submitted == current:
|
||||||
|
return RedirectResponse(url=f"{back}?msg=변경된 내용이 없어 적용하지 않았습니다.", status_code=303)
|
||||||
|
|
||||||
|
try:
|
||||||
|
design_ftp.write_text_file(ftp_config, path, submitted)
|
||||||
|
except Cafe24FtpError as exc:
|
||||||
|
st.log_audit(
|
||||||
|
actor=actor, action=f"apply_design:{file_key}", revision_id=backup_id,
|
||||||
|
result="FAIL", detail=str(exc),
|
||||||
|
)
|
||||||
|
logger.warning("디자인 파일 적용 실패(%s): %s", file_key, exc)
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"{back}?err=적용에 실패했습니다: {exc} (직전 내용은 버전 {backup_id} 로 보관됨)",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
revision_id = st.add_design_revision(
|
||||||
|
file_key=file_key, content=submitted, revision_type=store.REVISION_MANUAL,
|
||||||
|
memo=memo, created_by=actor,
|
||||||
|
)
|
||||||
|
st.log_audit(
|
||||||
|
actor=actor, action=f"apply_design:{file_key}", revision_id=revision_id, result="SUCCESS",
|
||||||
|
detail=f"{len(submitted)}자 적용 (백업 {backup_id})",
|
||||||
|
)
|
||||||
|
logger.info("디자인 파일 적용(%s) (%s)", file_key, actor)
|
||||||
|
return RedirectResponse(
|
||||||
|
url=f"{back}?msg=적용했습니다. 직전 내용은 버전 {backup_id} 로 보관됩니다.",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
"""모바일 스와이프(product-swiper.js) 편집 화면 — 카페24 "디자인 보관함" FTP.
|
|
||||||
|
|
||||||
상세페이지 편집기(routes_products.py)와 **같은** 문법강조 편집기·색상·단축키를
|
|
||||||
그대로 쓴다(요청사항: 모든 편집 기능이 상세페이지 소스 수정 기능과 같아야 한다).
|
|
||||||
다만 대상이 상품이 아니라 파일 1개(FTP)라서 목록·진열/판매·예약 같은 상품 전용
|
|
||||||
기능은 없다 — 편집·적용(백업 포함)·버전 이력만 있다.
|
|
||||||
|
|
||||||
쓰기 순서는 상세페이지 적용과 동일한 원칙을 따른다:
|
|
||||||
FTP 에서 현재 내용을 다시 읽는다(로컬 값을 현재값으로 가정하지 않는다)
|
|
||||||
→ BACKUP 버전 저장 → 지문 대조(충돌 시 거부) → FTP 에 쓴다
|
|
||||||
→ MANUAL 버전 + 감사로그
|
|
||||||
|
|
||||||
핸들러는 `async def` 가 아니라 `def`(동기)다 — FTP 호출이 블로킹이므로
|
|
||||||
FastAPI 스레드풀에서 돌게 둔다(다른 cafe24 라우트와 동일한 규칙).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, Request
|
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
||||||
|
|
||||||
from app.integrations.cafe24 import Cafe24FtpError, design_ftp, load_config, load_ftp_config
|
|
||||||
|
|
||||||
from . import store
|
|
||||||
from .common import base_ctx, guard
|
|
||||||
from .routes_products import _no_store
|
|
||||||
|
|
||||||
logger = logging.getLogger("cafe24.swiper")
|
|
||||||
|
|
||||||
swiper_router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
def _ftp_config():
|
|
||||||
# FTP 호스트 기본값(`{mall_id}.ftp.cafe24.com`)을 만들기 위해 OAuth 쪽
|
|
||||||
# mall_id 를 재사용한다 — FTP 자체는 OAuth 와 별개 인증이다.
|
|
||||||
return load_ftp_config(mall_id=load_config().mall_id)
|
|
||||||
|
|
||||||
|
|
||||||
@swiper_router.get("/swiper", response_class=HTMLResponse)
|
|
||||||
def swiper_page(request: Request) -> HTMLResponse:
|
|
||||||
from app.main import render_template # noqa: WPS433
|
|
||||||
|
|
||||||
checked = guard(request)
|
|
||||||
if not isinstance(checked, tuple):
|
|
||||||
return checked
|
|
||||||
st, user = checked
|
|
||||||
|
|
||||||
ftp_config = _ftp_config()
|
|
||||||
content = ""
|
|
||||||
error = ""
|
|
||||||
if not ftp_config.configured:
|
|
||||||
error = "카페24 디자인 FTP 설정이 필요합니다: " + ", ".join(ftp_config.missing)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
content = design_ftp.read_text_file(ftp_config, ftp_config.swiper_path)
|
|
||||||
except Cafe24FtpError as exc:
|
|
||||||
error = str(exc)
|
|
||||||
logger.warning("모바일 스와이프 파일 조회 실패: %s", exc)
|
|
||||||
|
|
||||||
ctx = base_ctx(request, user, active_tab="swiper")
|
|
||||||
ctx.update(
|
|
||||||
{
|
|
||||||
"page_title": "모바일 스와이프",
|
|
||||||
"page_subtitle": "product-swiper.js (카페24 디자인 보관함)",
|
|
||||||
"swiper_path": ftp_config.swiper_path,
|
|
||||||
"content": content,
|
|
||||||
"fingerprint": store.fingerprint(content) if not error else "",
|
|
||||||
"error": error,
|
|
||||||
"revisions": st.list_swiper_revisions(limit=20),
|
|
||||||
"flash": request.query_params.get("msg", ""),
|
|
||||||
"flash_error": request.query_params.get("err", ""),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return _no_store(render_template(request, "cafe24/swiper.html", ctx))
|
|
||||||
|
|
||||||
|
|
||||||
@swiper_router.post("/swiper/apply")
|
|
||||||
def swiper_apply(
|
|
||||||
request: Request,
|
|
||||||
content: str = Form(...),
|
|
||||||
base_fingerprint: str = Form(""),
|
|
||||||
memo: str = Form(""),
|
|
||||||
) -> RedirectResponse:
|
|
||||||
checked = guard(request)
|
|
||||||
if not isinstance(checked, tuple):
|
|
||||||
return checked
|
|
||||||
st, user = checked
|
|
||||||
actor = str(user.get("email") or "")
|
|
||||||
|
|
||||||
ftp_config = _ftp_config()
|
|
||||||
if not ftp_config.configured:
|
|
||||||
return RedirectResponse(
|
|
||||||
url="/cafe24/swiper?err=" + "FTP 설정이 필요합니다: " + ", ".join(ftp_config.missing),
|
|
||||||
status_code=303,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 브라우저 textarea 는 줄바꿈을 CRLF 로 보낸다 — 그대로 저장하면 실제 수정이
|
|
||||||
# 없어도 매번 파일 전체의 줄바꿈이 바뀌어(지문 비교·"변경 없음" 판정이 어긋난다).
|
|
||||||
submitted = content.replace("\r\n", "\n")
|
|
||||||
if not submitted.strip():
|
|
||||||
return RedirectResponse(
|
|
||||||
url="/cafe24/swiper?err=" + "내용이 비어 있습니다. 파일을 비우려면 FTP 로 직접 하세요.",
|
|
||||||
status_code=303,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
current = design_ftp.read_text_file(ftp_config, ftp_config.swiper_path)
|
|
||||||
except Cafe24FtpError as exc:
|
|
||||||
st.log_audit(actor=actor, action="apply_swiper", result="FAIL", detail=f"현재값 조회 실패: {exc}")
|
|
||||||
return RedirectResponse(url=f"/cafe24/swiper?err=현재 파일을 읽지 못해 중단했습니다: {exc}", status_code=303)
|
|
||||||
|
|
||||||
backup_id = st.add_swiper_revision(
|
|
||||||
content=current, revision_type=store.REVISION_BACKUP,
|
|
||||||
memo="적용 직전 자동 백업", created_by=actor,
|
|
||||||
)
|
|
||||||
|
|
||||||
if base_fingerprint and base_fingerprint != store.fingerprint(current):
|
|
||||||
st.log_audit(
|
|
||||||
actor=actor, action="apply_swiper", revision_id=backup_id,
|
|
||||||
result="FAIL", detail="충돌 — 편집 중 파일이 변경됨",
|
|
||||||
)
|
|
||||||
return RedirectResponse(
|
|
||||||
url="/cafe24/swiper?err=편집하는 동안 파일이 변경되었습니다. 새로고침해 현재 내용을 확인한 뒤 다시 적용하세요.",
|
|
||||||
status_code=303,
|
|
||||||
)
|
|
||||||
|
|
||||||
if submitted == current:
|
|
||||||
return RedirectResponse(url="/cafe24/swiper?msg=변경된 내용이 없어 적용하지 않았습니다.", status_code=303)
|
|
||||||
|
|
||||||
try:
|
|
||||||
design_ftp.write_text_file(ftp_config, ftp_config.swiper_path, submitted)
|
|
||||||
except Cafe24FtpError as exc:
|
|
||||||
st.log_audit(
|
|
||||||
actor=actor, action="apply_swiper", revision_id=backup_id,
|
|
||||||
result="FAIL", detail=str(exc),
|
|
||||||
)
|
|
||||||
logger.warning("모바일 스와이프 파일 적용 실패: %s", exc)
|
|
||||||
return RedirectResponse(
|
|
||||||
url=f"/cafe24/swiper?err=적용에 실패했습니다: {exc} (직전 내용은 버전 {backup_id} 로 보관됨)",
|
|
||||||
status_code=303,
|
|
||||||
)
|
|
||||||
|
|
||||||
revision_id = st.add_swiper_revision(
|
|
||||||
content=submitted, revision_type=store.REVISION_MANUAL, memo=memo, created_by=actor,
|
|
||||||
)
|
|
||||||
st.log_audit(
|
|
||||||
actor=actor, action="apply_swiper", revision_id=revision_id, result="SUCCESS",
|
|
||||||
detail=f"{len(submitted)}자 적용 (백업 {backup_id})",
|
|
||||||
)
|
|
||||||
logger.info("모바일 스와이프 파일 적용 (%s)", actor)
|
|
||||||
return RedirectResponse(
|
|
||||||
url=f"/cafe24/swiper?msg=적용했습니다. 직전 내용은 버전 {backup_id} 로 보관됩니다.",
|
|
||||||
status_code=303,
|
|
||||||
)
|
|
||||||
+19
-18
@@ -19,14 +19,15 @@
|
|||||||
|
|
||||||
{# 상세페이지 편집기(_editor.html)와 완전히 같은 문법강조·단축키·버튼을 쓴다.
|
{# 상세페이지 편집기(_editor.html)와 완전히 같은 문법강조·단축키·버튼을 쓴다.
|
||||||
목록/진열·판매/예약처럼 "상품" 전용인 것만 뺐다 — 이건 상품이 아니라
|
목록/진열·판매/예약처럼 "상품" 전용인 것만 뺐다 — 이건 상품이 아니라
|
||||||
디자인 보관함(FTP)의 파일 1개다. #}
|
디자인 보관함(FTP)의 파일 1개다(모바일 스와이프 / PC·모바일 상품상세
|
||||||
<section class="erp-card cf24-pane-editor" id="cf24-swiper-pane">
|
템플릿이 file_key 만 다르고 화면·로직은 완전히 같다). #}
|
||||||
<p class="cf24-editor-sub"><code>{{ swiper_path }}</code></p>
|
<section class="erp-card cf24-pane-editor" id="cf24-design-pane">
|
||||||
|
<p class="cf24-editor-sub"><code>{{ file_path }}</code></p>
|
||||||
|
|
||||||
{% if not error %}
|
{% if not error %}
|
||||||
<form class="cf24-editor-form" id="cf24-swiper-form" method="post"
|
<form class="cf24-editor-form" id="cf24-design-form" method="post"
|
||||||
action="/cafe24/swiper/apply"
|
action="/cafe24/design/{{ file_key }}/apply"
|
||||||
data-confirm="카페24 디자인 보관함(FTP)에 바로 반영됩니다. 적용할까요? 직전 내용은 자동으로 백업되어 되돌릴 수 있습니다.">
|
data-confirm="{{ apply_confirm }}">
|
||||||
<input type="hidden" name="base_fingerprint" value="{{ fingerprint }}" />
|
<input type="hidden" name="base_fingerprint" value="{{ fingerprint }}" />
|
||||||
|
|
||||||
<div class="cf24-editor-bar">
|
<div class="cf24-editor-bar">
|
||||||
@@ -34,20 +35,20 @@
|
|||||||
<span class="cf24-editor-bar-right">
|
<span class="cf24-editor-bar-right">
|
||||||
<input class="cf24-memo" type="text" name="memo" maxlength="200"
|
<input class="cf24-memo" type="text" name="memo" maxlength="200"
|
||||||
placeholder="변경 메모 (버전 이력에 남습니다)" />
|
placeholder="변경 메모 (버전 이력에 남습니다)" />
|
||||||
<button class="erp-btn erp-btn-outline" type="button" data-copy="cf24-swiper-code">복사</button>
|
<button class="erp-btn erp-btn-outline" type="button" data-copy="cf24-design-code">복사</button>
|
||||||
<button class="erp-btn erp-btn-outline" type="button" id="cf24-swiper-reload"
|
<button class="erp-btn erp-btn-outline" type="button" id="cf24-design-reload"
|
||||||
title="카페24 디자인 보관함(FTP)에서 현재 파일을 다시 읽어옵니다.">다시 읽기</button>
|
title="카페24 디자인 보관함(FTP)에서 현재 파일을 다시 읽어옵니다.">다시 읽기</button>
|
||||||
<button class="erp-btn erp-btn-primary" type="submit">카페24에 적용</button>
|
<button class="erp-btn erp-btn-primary" type="submit">카페24에 적용</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# 색칠된 <pre> 위에 투명한 <textarea> 를 겹쳐 문법 강조를 만든다 —
|
{# 색칠된 <pre> 위에 투명한 <textarea> 를 겹쳐 문법 강조를 만든다 —
|
||||||
상세페이지 편집기와 동일한 방식(store.md 2-2 절 참고). #}
|
상세페이지 편집기와 동일한 방식(docs/CAFE24_MODULE.md 2-2 절 참고). #}
|
||||||
<div class="cf24-code" id="cf24-swiper-code-box">
|
<div class="cf24-code" id="cf24-design-code-box">
|
||||||
<div class="cf24-gutter" aria-hidden="true"><div class="cf24-gutter-inner" id="cf24-swiper-gutter"></div></div>
|
<div class="cf24-gutter" aria-hidden="true"><div class="cf24-gutter-inner" id="cf24-design-gutter"></div></div>
|
||||||
<div class="cf24-code-body">
|
<div class="cf24-code-body">
|
||||||
<pre class="cf24-code-hl" id="cf24-swiper-hl" aria-hidden="true"></pre>
|
<pre class="cf24-code-hl" id="cf24-design-hl" aria-hidden="true"></pre>
|
||||||
<textarea id="cf24-swiper-code" class="cf24-code-input" name="content" wrap="off"
|
<textarea id="cf24-design-code" class="cf24-code-input" name="content" wrap="off"
|
||||||
spellcheck="false" autocapitalize="off" autocorrect="off">{{ content }}</textarea>
|
spellcheck="false" autocapitalize="off" autocorrect="off">{{ content }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,9 +127,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupCodeEditor() {
|
function setupCodeEditor() {
|
||||||
var ta = document.getElementById("cf24-swiper-code");
|
var ta = document.getElementById("cf24-design-code");
|
||||||
var hl = document.getElementById("cf24-swiper-hl");
|
var hl = document.getElementById("cf24-design-hl");
|
||||||
var gutter = document.getElementById("cf24-swiper-gutter");
|
var gutter = document.getElementById("cf24-design-gutter");
|
||||||
if (!ta || !hl) return;
|
if (!ta || !hl) return;
|
||||||
|
|
||||||
var timer = null;
|
var timer = null;
|
||||||
@@ -322,14 +323,14 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var reloadBtn = document.getElementById("cf24-swiper-reload");
|
var reloadBtn = document.getElementById("cf24-design-reload");
|
||||||
if (reloadBtn) {
|
if (reloadBtn) {
|
||||||
reloadBtn.addEventListener("click", function () {
|
reloadBtn.addEventListener("click", function () {
|
||||||
confirmLeave().then(function (ok) { if (ok) location.reload(); });
|
confirmLeave().then(function (ok) { if (ok) location.reload(); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var form = document.getElementById("cf24-swiper-form");
|
var form = document.getElementById("cf24-design-form");
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Reference in New Issue
Block a user