diff --git a/app/modules/cafe24/router.py b/app/modules/cafe24/router.py index 31b6bf8..fe8a8b0 100644 --- a/app/modules/cafe24/router.py +++ b/app/modules/cafe24/router.py @@ -9,7 +9,8 @@ 라우트가 많아 기능별 파일로 나눈다(다른 모듈의 단일 router.py 패턴을 규모 때문에 확장한 것). 여기서는 루트 라우터를 만들고 서브 라우터를 결합한다. - routes_products 상품 목록/검색 · 상세설명 조회 + routes_products 상품 목록/검색 · 상세설명 조회·편집·적용 + routes_bulk 일괄수정 (검사 → 선택 적용) routes_system 연결(OAuth)·상태·API 로그·작업 로그 (Phase 5) routes_schedules """ @@ -22,6 +23,7 @@ from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from .common import base_ctx, guard +from .routes_bulk import bulk_router from .routes_products import products_router from .routes_system import system_router @@ -30,6 +32,7 @@ logger = logging.getLogger("cafe24.router") router = APIRouter(prefix="/cafe24", tags=["cafe24"]) router.include_router(products_router) +router.include_router(bulk_router) router.include_router(system_router) diff --git a/app/modules/cafe24/routes_bulk.py b/app/modules/cafe24/routes_bulk.py new file mode 100644 index 0000000..2888f03 --- /dev/null +++ b/app/modules/cafe24/routes_bulk.py @@ -0,0 +1,194 @@ +"""카페24 일괄수정 — 상세페이지 맨 위 `" + + +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` | 일괄수정 화면 (`