From 3522fc211980ccf7b1b630d4a580c2237ccf4c4e Mon Sep 17 00:00:00 2001 From: king Date: Fri, 14 Aug 2026 14:12:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(cafe24):=20=EC=9D=BC=EA=B4=84=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=E2=80=94=20=EC=83=81=EC=84=B8=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20 그냥 덮어쓰지 않고 검사 → 선택 → 적용 2단계로 만들었다. 상품 131번의 style 안에는 "비디오 태그 모바일 반응형 스타일" 같은 CSS 가 들어 있어서, 무엇이 지워지는지 보지 않고 87건을 일괄 실행하면 필요한 규칙이 조용히 사라진다. 검사 결과 표에 지금 들어 있는 CSS 를 그대로 보여주고, 변경이 필요한 상품만 자동 선택한다(이미 같은 내용이면 「이미 동일」로 제외). 맨 앞 " + + +def _preview(html: str) -> dict[str, Any]: + """현재 ", re.IGNORECASE | re.DOTALL) + + +def find_style_blocks(html: str) -> list[str]: + """상세설명 안의 블록 전체(원문 그대로).""" + return _STYLE_BLOCK_RE.findall(html or "") + + +def replace_first_style_block(html: str, new_block: str) -> str: + """맨 앞 " + + +def test_find_style_blocks(): + html = "
x
" + blocks = store.find_style_blocks(html) + assert len(blocks) == 2 + assert blocks[0] == "" + + +def test_replace_first_style_block_only(): + """두 번째 이후
x
" + out = store.replace_first_style_block(html, _NEW_STYLE) + assert "OLD" not in out + assert "" in out + assert out.startswith(_NEW_STYLE) + assert "
x
" in out + + +def test_replace_style_inserts_when_missing(): + out = store.replace_first_style_block("
x
", _NEW_STYLE) + assert out.startswith(_NEW_STYLE) + assert "
x
" in out + + +def test_replace_style_is_idempotent(): + once = store.replace_first_style_block("
x
", _NEW_STYLE) + assert store.replace_first_style_block(once, _NEW_STYLE) == once + + +def test_replace_style_survives_formatting(): + """정리(포맷)를 거쳐도
x
", _NEW_STYLE) + ) + assert "\tdiv {" in formatted + assert "\t\ttext-align: center;" in formatted + assert len(store.find_style_blocks(formatted)) == 1 + + +def test_replace_style_keeps_multiline_original_shape(): + """원문이 여러 줄이어도 앞 블록만 정확히 잘라낸다.""" + html = '\n' + out = store.replace_first_style_block(html, _NEW_STYLE) + assert "비디오" not in out + assert '' in out + + def test_fingerprint_detects_change(): a = store.fingerprint("

A

") assert a == store.fingerprint("

A

") diff --git a/docs/CAFE24_MODULE.md b/docs/CAFE24_MODULE.md index 6b48768..b39a180 100644 --- a/docs/CAFE24_MODULE.md +++ b/docs/CAFE24_MODULE.md @@ -26,13 +26,15 @@ app/integrations/cafe24/ ← 공통 (상품관리 + 향후 주문관리 app/modules/cafe24/ ← 상품관리 모듈 ├─ router.py 루트 라우터(prefix=/cafe24) + 서브 라우터 결합 ├─ routes_products.py 2분할 화면 · 편집기 조각 · 적용(쓰기) +├─ routes_bulk.py 일괄수정 (검사 → 선택 적용) ├─ routes_system.py 연결(OAuth)·상태·API 로그·작업 로그 ├─ common.py 가드/컨텍스트 헬퍼 (순환 import 방지로 분리) ├─ db.py Cafe24Store (cafe24_db, psycopg3 raw SQL) ├─ store.py 순수 로직 — 상수·상태전이·재시도 규칙·검증 ├─ tests/ DB/네트워크 없는 유닛테스트 └─ templates/cafe24/ _nav.html · products.html(2분할) · - _editor.html(오른쪽 조각) · schedules.html · system.html + _editor.html(오른쪽 조각) · bulk.html · + schedules.html · system.html ``` **규칙: 라우터에서 `httpx`/`requests` 를 직접 부르지 않는다.** 반드시 @@ -52,6 +54,9 @@ app/modules/cafe24/ ← 상품관리 모듈 | `GET /cafe24/products/{product_no}/pane` | 오른쪽 편집기 조각 (JS 가 가져감) | `cafe24` | | `GET /cafe24/products/{product_no}` | → `/cafe24/?selected=` 로 리다이렉트(옛 주소) | `cafe24` | | `POST /cafe24/products/{product_no}/apply` | 편집한 HTML 을 카페24에 즉시 적용 | `cafe24` | +| `GET /cafe24/bulk` | 일괄수정 화면 (`