feat(cafe24): 문법 강조 편집기 + 소스 자동 정리, 목록 550px
1) 검색 입력란이 거대했던 버그 .cf24-filters 가 세로 flex 인데 .cf24-search 에 flex:0 1 320px 을 줬다. 세로 방향에서는 flex-basis 가 '높이'로 적용돼 입력란이 320px 짜리 상자가 됐다. height:32px 로 한 줄에 고정했고, 그만큼 목록이 더 보인다. 2) 목록 550px 요청대로 왼쪽을 550px 로 넓혔다. 남은 폭(약 290px)이 상품명 몫이라 대부분 한 줄에 들어가고, 수정일도 월-일 시:분까지 보여준다. 컬럼은 번호·상품명·진열·판매·수정 5개 그대로다. 3) 문법 강조 편집기 색칠된 <pre> 위에 투명한 <textarea> 를 겹치는 방식으로 직접 구현했다. 외부 라이브러리를 쓰지 않는 이유는 자체 호스팅 원칙이다(CDN 의존 금지). 태그·속성이름· 속성값·주석·기호를 색으로 구분하고 Tab 은 들여쓰기로 쓴다. 두 층의 글자가 어긋나지 않으려면 폰트·줄높이·padding·줄바꿈 규칙이 완전히 같아야 한다. 특히 높이는 <pre> 의 scrollHeight 를 기준으로 textarea 에 지정한다 — textarea 의 scrollHeight 를 쓰면 두 줄쯤 더 잡혀 어긋난다(실측 830 vs 792, 브라우저에서 확인 후 수정). 20만 자를 넘으면 강조를 끈다. 4) 소스 정리(포맷)와 저장 반영 store.format_html 을 추가했다. 화면 표시와 저장에 같은 함수를 쓰므로 화면에서 본 정리된 소스가 그대로 카페24에 저장된다. 렌더링을 바꾸지 않는 것을 최우선으로 했다. HTML 에서 공백은 의미가 있어서 인라인 요소 사이에 줄바꿈을 넣으면 화면에 공백이 생긴다 — 이미지 사이가 벌어지는 고전적인 사고다. 그래서 블록 요소 경계에서만 줄을 나누고 img·br·span·a 는 블록 목록에서 일부러 뺐다. <style>·<script>·<pre>·<textarea> 안쪽은 한 글자도 건드리지 않는다. 내용이 한 줄뿐인 짧은 블록은 다시 한 줄로 합친다. 멱등성을 테스트로 고정했다. 처음 구현은 <style> 안 빈 줄이 실행마다 한 줄씩 늘어나 멱등이 깨졌고(테스트가 잡음), 앞뒤 빈 줄을 버리도록 고쳤다. 편집하지 않고 다시 적용해도 저장값이 계속 달라지면 버전 이력이 의미를 잃는다. 닫는 태그가 빠진 HTML 이 흔하므로 들여쓰기 상한(12)을 뒀고, 어떤 이유로든 실패하면 원본을 그대로 돌려준다. 검증: 유닛테스트 41개 통과(신규 8개 — 블록 분리·인라인 보존(이미지 붙음)·style 원문 보존·멱등·짧은 블록 합치기·깨진 HTML 내성·속성값 미변경·정리+인코딩 왕복). 브라우저 실측: 검색란 32px, 목록 550px/편집기 750px, 오버레이 두 층 높이 일치 (편집 전 792=792, 20줄 추가 후 1175=1175), 토큰 색상 적용, 가로 스크롤 없음. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -428,6 +428,74 @@ def test_invalid_utf8_sequence_left_alone():
|
||||
assert store.decode_html_urls(html) == html
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 소스 정리(포맷) — 렌더링을 바꾸지 않는 것이 최우선
|
||||
# ════════════════════════════════════════════════════════════
|
||||
_MESSY = (
|
||||
'<style>\n\t/* 주석 */\n\t.v{max-width:100%}\n</style>'
|
||||
'<div class="wrap"><p>안녕<span>하세요</span> 여름 특가</p>'
|
||||
'<img src="/web/a.gif"><img src="/web/b.gif">'
|
||||
"<table><tr><td>1</td><td>2</td></tr></table></div>"
|
||||
)
|
||||
|
||||
|
||||
def test_format_breaks_block_tags():
|
||||
out = store.format_html(_MESSY)
|
||||
lines = out.split("\n")
|
||||
assert '<div class="wrap">' in lines
|
||||
assert "</div>" in lines
|
||||
# 블록 안쪽은 들여쓴다.
|
||||
assert any(line.startswith(" <table>") for line in lines)
|
||||
assert any(line.startswith(" <td>") for line in lines)
|
||||
|
||||
|
||||
def test_format_keeps_inline_elements_together():
|
||||
"""이미지 사이에 줄바꿈이 들어가면 화면에 공백이 생긴다 — 붙여둬야 한다."""
|
||||
out = store.format_html(_MESSY)
|
||||
assert '<img src="/web/a.gif"><img src="/web/b.gif">' in out
|
||||
assert "<p>안녕<span>하세요</span> 여름 특가</p>" in out
|
||||
|
||||
|
||||
def test_format_preserves_style_content_verbatim():
|
||||
out = store.format_html(_MESSY)
|
||||
assert "\t/* 주석 */" in out
|
||||
assert "\t.v{max-width:100%}" in out
|
||||
|
||||
|
||||
def test_format_is_idempotent():
|
||||
"""편집하지 않고 다시 적용해도 저장값이 계속 바뀌면 안 된다."""
|
||||
once = store.format_html(_MESSY)
|
||||
assert store.format_html(once) == once
|
||||
assert store.format_html(store.format_html(once)) == once
|
||||
|
||||
|
||||
def test_format_collapses_short_blocks():
|
||||
assert store.format_html("<td>1</td>") == "<td>1</td>"
|
||||
# 길면 나눈다.
|
||||
long_text = "가" * 200
|
||||
assert "\n" in store.format_html("<td>%s</td>" % long_text)
|
||||
|
||||
|
||||
def test_format_survives_broken_html():
|
||||
"""닫는 태그 누락·꺾쇠 조각이 있어도 예외 없이 뭔가를 돌려준다."""
|
||||
for bad in ("<div><p>열고 안 닫음", "a < b 그리고 c > d", "<<>>", "<div", ""):
|
||||
assert isinstance(store.format_html(bad), str)
|
||||
|
||||
|
||||
def test_format_does_not_touch_urls():
|
||||
"""포맷은 속성값을 건드리지 않는다(인코딩과 서로 간섭하지 않게)."""
|
||||
html = '<div><img src="/web/%EC%9A%A9%EA%B8%B0(a)_1.gif"></div>'
|
||||
assert "/web/%EC%9A%A9%EA%B8%B0(a)_1.gif" in store.format_html(html)
|
||||
|
||||
|
||||
def test_format_then_encode_roundtrip():
|
||||
"""화면 표시(디코딩+정리) → 저장(인코딩+정리) 순서에서 URL 이 원형을 지킨다."""
|
||||
raw = store.format_html(_REAL_HTML)
|
||||
shown = store.format_html(store.decode_html_urls(raw))
|
||||
saved = store.format_html(store.encode_html_urls(shown))
|
||||
assert saved == raw
|
||||
|
||||
|
||||
def test_fingerprint_detects_change():
|
||||
a = store.fingerprint("<p>A</p>")
|
||||
assert a == store.fingerprint("<p>A</p>")
|
||||
|
||||
Reference in New Issue
Block a user