feat(cafe24): 진열중·판매중 기본 체크 + 분리 상품 모바일 동시 반영 선택
1) 필터 기본값 진열중·판매중을 기본 체크로 바꿨다. 체크박스는 해제 상태면 아무 값도 보내지 않아 기본값이 체크면 "사용자가 일부러 해제함"을 구분할 수 없다. 그래서 폼에 표식(f=1)을 넣어, 표식이 없으면 첫 방문(기본값), 있으면 실제 체크 상태를 따르게 했다. 표식은 목록 링크·적용 후 리다이렉트에도 이어 붙어 해제 상태가 유지된다. 2) 분리 상품의 모바일 반영 "소스를 수정하면 PC와 모바일이 같이 수정되는 것 아닌가" 라는 지적대로, PC/모바일 분리 사용 상품은 지금까지 PC 만 바뀌고 있었다(미분리 상품은 원래 함께 반영). 편집기에 「모바일도 함께」 체크박스를 추가했다. 현재 두 내용이 같으면 기본 체크라 그대로 적용하면 함께 반영되고, 내용이 다르면 기본 해제하고 경고를 띄운다 — 일부러 다르게 만든 모바일 페이지를 조용히 덮어쓰는 것이 더 큰 사고이기 때문이다. 미분리 상품은 종전처럼 항상 함께 반영하며 체크박스를 보여주지 않는다. 검증: 유닛테스트 51개 통과. 필터 판정을 5가지 경우로 확인(첫 방문·둘 다 체크·하나만· 둘 다 해제·검색 링크) — 둘 다 해제가 f=1 표식으로 유지됨. 편집기 렌더를 3가지 상태로 확인(분리+동일=기본체크, 분리+상이=기본해제+경고, 미분리=체크박스 없음). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,12 +81,28 @@ def _row_for_list(raw: dict[str, Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 필터 폼이 제출됐음을 알리는 표식.
|
||||||
|
# 체크박스는 해제 상태면 아무 값도 보내지 않으므로, 이것 없이는 "첫 방문"과
|
||||||
|
# "사용자가 일부러 해제함"을 구분할 수 없다(기본값이 체크라서 해제가 무시된다).
|
||||||
|
_FILTER_MARK = "f"
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_flags(request: Request) -> tuple[bool, bool]:
|
||||||
|
"""(진열중만, 판매중만). 첫 방문이면 둘 다 기본 체크로 본다."""
|
||||||
|
if request.query_params.get(_FILTER_MARK) is None:
|
||||||
|
return True, True
|
||||||
|
return _checked(request, "display"), _checked(request, "selling")
|
||||||
|
|
||||||
|
|
||||||
def _list_query(request: Request, *, selected: int | None = None) -> str:
|
def _list_query(request: Request, *, selected: int | None = None) -> str:
|
||||||
"""현재 검색·필터를 유지한 목록 URL 쿼리스트링."""
|
"""현재 검색·필터를 유지한 목록 URL 쿼리스트링."""
|
||||||
params: list[tuple[str, str]] = []
|
params: list[tuple[str, str]] = []
|
||||||
keyword = (request.query_params.get("q") or "").strip()
|
keyword = (request.query_params.get("q") or "").strip()
|
||||||
if keyword:
|
if keyword:
|
||||||
params.append(("q", keyword))
|
params.append(("q", keyword))
|
||||||
|
if request.query_params.get(_FILTER_MARK) is not None:
|
||||||
|
# 해제 상태까지 그대로 이어지도록 표식을 함께 남긴다.
|
||||||
|
params.append((_FILTER_MARK, "1"))
|
||||||
for flag in ("display", "selling"):
|
for flag in ("display", "selling"):
|
||||||
if _checked(request, flag):
|
if _checked(request, flag):
|
||||||
params.append((flag, "1"))
|
params.append((flag, "1"))
|
||||||
@@ -144,8 +160,7 @@ def product_list(request: Request) -> HTMLResponse:
|
|||||||
st, user = checked
|
st, user = checked
|
||||||
|
|
||||||
keyword = (request.query_params.get("q") or "").strip()
|
keyword = (request.query_params.get("q") or "").strip()
|
||||||
only_display = _checked(request, "display")
|
only_display, only_selling = _filter_flags(request)
|
||||||
only_selling = _checked(request, "selling")
|
|
||||||
|
|
||||||
api = build_cafe24_api(st)
|
api = build_cafe24_api(st)
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
@@ -226,6 +241,7 @@ def product_apply(
|
|||||||
base_fingerprint: str = Form(""),
|
base_fingerprint: str = Form(""),
|
||||||
memo: str = Form(""),
|
memo: str = Form(""),
|
||||||
list_query: str = Form(""),
|
list_query: str = Form(""),
|
||||||
|
apply_mobile: str = Form(""),
|
||||||
):
|
):
|
||||||
"""편집한 HTML 을 카페24에 즉시 적용한다.
|
"""편집한 HTML 을 카페24에 즉시 적용한다.
|
||||||
|
|
||||||
@@ -285,9 +301,13 @@ def product_apply(
|
|||||||
status_code=303,
|
status_code=303,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 미분리 상품은 모바일도 함께 맞춘다(PC 만 바꾸면 모바일이 어긋난다).
|
# 미분리 상품은 모바일도 함께 맞춘다 — PC 만 바꾸면 모바일 상세가 어긋난다.
|
||||||
# 분리 상품은 모바일을 건드리지 않는다(화면에 별도 반영 안내를 띄운다).
|
# 분리 상품은 화면의 「모바일도 함께」 체크에 따른다(두 내용이 같으면 기본 체크,
|
||||||
mobile_html = None if current.separated_mobile else submitted
|
# 다르면 기본 해제 — 일부러 다르게 만든 모바일 페이지를 덮어쓰지 않기 위해).
|
||||||
|
if current.separated_mobile:
|
||||||
|
mobile_html = submitted if apply_mobile else None
|
||||||
|
else:
|
||||||
|
mobile_html = submitted
|
||||||
|
|
||||||
if submitted == current.description and (
|
if submitted == current.description and (
|
||||||
mobile_html is None or mobile_html == current.mobile_description
|
mobile_html is None or mobile_html == current.mobile_description
|
||||||
|
|||||||
@@ -32,8 +32,15 @@
|
|||||||
{% if desc %}
|
{% if desc %}
|
||||||
<p class="cf24-note">
|
<p class="cf24-note">
|
||||||
{% if desc.separated_mobile %}
|
{% if desc.separated_mobile %}
|
||||||
<strong>PC/모바일 분리 사용 상품입니다.</strong> 아래 적용은 PC 만 바꿉니다 —
|
<strong>PC/모바일 분리 사용 상품입니다.</strong>
|
||||||
모바일({{ desc.mobile_description | length }}자)은 카페24 관리자에서 따로 반영해야 합니다.
|
모바일({{ desc.mobile_description | length }}자)은 아래 <strong>「모바일도 함께」</strong>
|
||||||
|
체크박스로 같은 내용을 반영할 수 있습니다.
|
||||||
|
{% if desc.mobile_differs %}
|
||||||
|
<span class="cf24-warn">지금 모바일 내용이 PC와 달라 기본 해제되어 있습니다</span> —
|
||||||
|
체크하면 모바일이 PC와 같은 내용으로 덮어써집니다.
|
||||||
|
{% else %}
|
||||||
|
현재 두 내용이 같아 기본 체크되어 있습니다.
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
모바일은 PC와 동일 설정이라 적용 시 <strong>함께 반영</strong>됩니다.
|
모바일은 PC와 동일 설정이라 적용 시 <strong>함께 반영</strong>됩니다.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -55,7 +62,17 @@
|
|||||||
<input type="hidden" name="list_query" value="{{ list_query }}" />
|
<input type="hidden" name="list_query" value="{{ list_query }}" />
|
||||||
|
|
||||||
<div class="cf24-editor-bar">
|
<div class="cf24-editor-bar">
|
||||||
<span class="cf24-muted">PC 상세설명 HTML · {{ desc.description | length }}자</span>
|
<span class="cf24-muted">
|
||||||
|
PC 상세설명 HTML · {{ desc.description | length }}자
|
||||||
|
{% if desc.separated_mobile %}
|
||||||
|
<label class="cf24-check-inline"
|
||||||
|
title="분리 사용 상품이라 기본적으로 PC 만 바뀝니다. 체크하면 모바일 상세설명도 같은 내용으로 함께 반영합니다.">
|
||||||
|
<input type="checkbox" name="apply_mobile" value="1"
|
||||||
|
{% if not desc.mobile_differs %}checked{% endif %} />
|
||||||
|
모바일도 함께
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
<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="변경 메모 (버전 이력에 남습니다)" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814k" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814k" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
{# ── 왼쪽: 상품 목록 ──────────────────────────────────────── #}
|
{# ── 왼쪽: 상품 목록 ──────────────────────────────────────── #}
|
||||||
<aside class="erp-card cf24-pane cf24-pane-list">
|
<aside class="erp-card cf24-pane cf24-pane-list">
|
||||||
<form class="cf24-filters" method="get" action="/cafe24/" id="cf24-filter-form">
|
<form class="cf24-filters" method="get" action="/cafe24/" id="cf24-filter-form">
|
||||||
|
{# 폼이 제출됐음을 알리는 표식. 없으면 체크 해제를 "첫 방문"과 구분할 수 없다. #}
|
||||||
|
<input type="hidden" name="f" value="1" />
|
||||||
{% if selected %}<input type="hidden" name="selected" value="{{ selected }}" />{% endif %}
|
{% if selected %}<input type="hidden" name="selected" value="{{ selected }}" />{% endif %}
|
||||||
<input class="cf24-search" type="search" name="q" value="{{ keyword }}"
|
<input class="cf24-search" type="search" name="q" value="{{ keyword }}"
|
||||||
placeholder="상품명 검색" />
|
placeholder="상품명 검색" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814k" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814j" />
|
<link rel="stylesheet" href="/static/cafe24.css?v=20260814k" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -240,6 +240,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cf24-check-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: var(--sp-12, 12px);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-rich-black, #0a0a0a);
|
||||||
|
}
|
||||||
|
|
||||||
.cf24-memo {
|
.cf24-memo {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
padding: 6px var(--sp-10, 10px);
|
padding: 6px var(--sp-10, 10px);
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ app/modules/cafe24/ ← 상품관리 모듈
|
|||||||
해당 상품이 빠지기 때문이다.
|
해당 상품이 빠지기 때문이다.
|
||||||
- **필터**는 `진열중`/`판매중` 체크박스이며 **중복 선택 시 AND** 다. 문서에 없는 API
|
- **필터**는 `진열중`/`판매중` 체크박스이며 **중복 선택 시 AND** 다. 문서에 없는 API
|
||||||
파라미터에 기대지 않고 받아온 뒤 파이썬에서 걸러낸다.
|
파라미터에 기대지 않고 받아온 뒤 파이썬에서 걸러낸다.
|
||||||
|
**기본값은 둘 다 체크**다. 체크박스는 해제 상태면 아무 값도 보내지 않으므로,
|
||||||
|
폼에 표식(`f=1`)을 함께 넣어 "첫 방문"과 "사용자가 일부러 해제함"을 구분한다.
|
||||||
|
표식이 없으면 기본값(둘 다 체크)으로 보고, 있으면 실제 체크 상태를 따른다.
|
||||||
|
이 표식은 `_list_query` 가 링크·리다이렉트에도 이어 붙여 해제 상태가 유지된다.
|
||||||
- **정렬**은 제목행 클릭(오름↔내림 토글). 브라우저에서 처리하므로 전체를 받아둔
|
- **정렬**은 제목행 클릭(오름↔내림 토글). 브라우저에서 처리하므로 전체를 받아둔
|
||||||
덕분에 목록 전체가 대상이 된다.
|
덕분에 목록 전체가 대상이 된다.
|
||||||
- **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지
|
- **상품 클릭 시 오른쪽만 교체**한다(`/pane` 조각을 fetch → 삽입). 목록을 다시 받지
|
||||||
@@ -253,7 +257,11 @@ cafe24_oauth_tokens 저장
|
|||||||
- 빈 내용은 거부한다(상세페이지 전체를 날리는 실수 방지).
|
- 빈 내용은 거부한다(상세페이지 전체를 날리는 실수 방지).
|
||||||
- **PC/모바일 미분리(`separated_mobile_description='F'`) 상품은 모바일 필드도
|
- **PC/모바일 미분리(`separated_mobile_description='F'`) 상품은 모바일 필드도
|
||||||
같은 HTML 로 함께 쓴다.** PC 만 바꾸면 모바일이 어긋난다.
|
같은 HTML 로 함께 쓴다.** PC 만 바꾸면 모바일이 어긋난다.
|
||||||
분리(`'T'`) 상품은 모바일을 건드리지 않고, 화면에 "모바일은 따로 반영" 을 알린다.
|
- **분리(`'T'`) 상품은 편집기의 「모바일도 함께」 체크박스에 따른다.**
|
||||||
|
현재 두 내용이 같으면 **기본 체크**(그대로 두면 함께 반영), 다르면 **기본 해제**하고
|
||||||
|
경고를 띄운다 — 일부러 다르게 만든 모바일 페이지를 조용히 덮어쓰지 않기 위함이다.
|
||||||
|
일괄수정(`routes_bulk`)은 분리 상품도 항상 모바일까지 바꾼다(대상이 `<style>` 블록
|
||||||
|
하나로 한정되어 있어 내용을 잃을 위험이 없다).
|
||||||
- 실패해도 BACKUP 은 이미 남아 있으므로 오류 메시지에 버전 번호를 알려준다.
|
- 실패해도 BACKUP 은 이미 남아 있으므로 오류 메시지에 버전 번호를 알려준다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user