블록 선택 방식(Phase P/Q/R) 폐기 → Claude Sonnet이 영역별 HTML 직접 생성. 생성-검증 분리: content_verifier.py로 텍스트 보존/금지 콘텐츠/구조를 코드 검증. 주요 변경: - src/html_generator.py: 4개 프롬프트 템플릿(BG/CORE/SIDEBAR/FOOTER) + 영역별 Claude 호출 - src/content_verifier.py: L1 텍스트 보존, L2 금지 콘텐츠, L3 구조 검증 + 재시도 루프 - src/html_validator.py: 보안 검증(script/iframe 제거) - src/renderer.py: render_slide_from_html() 추가, area div overflow:hidden - scripts/test_phase_s.py: generate_with_retry() 통합, step2b_verification 결과 저장 - 배경 라이트 디자인(#f8fafc), 개조식 어미 변환, 축약 금지 규칙 다음 과제: 폰트 위계(핵심14>본문12>배경10-12>첨부9-11) + 동적 컨테이너 계산 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""Phase R'-6: AI 생성 HTML 정화 + 토큰 위반 검증.
|
|
|
|
AI가 생성한 HTML에서:
|
|
1. 위험 태그 제거 (<script>, <iframe> 등)
|
|
2. CSS에 하드코딩 값이 있는지 경고 (토큰 위반)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def validate_and_clean_html(generated: dict[str, str]) -> dict[str, str]:
|
|
"""AI 생성 HTML을 정화하고 검증한다.
|
|
|
|
Args:
|
|
generated: {"body_html": "...", "sidebar_html": "...", "footer_html": "..."}
|
|
|
|
Returns:
|
|
정화된 generated dict
|
|
"""
|
|
cleaned = {}
|
|
for key in ("body_html", "sidebar_html", "footer_html"):
|
|
html = generated.get(key, "")
|
|
html = _remove_dangerous_tags(html)
|
|
_check_token_violations(html, key)
|
|
cleaned[key] = html
|
|
|
|
# reasoning은 그대로 유지
|
|
cleaned["reasoning"] = generated.get("reasoning", "")
|
|
|
|
return cleaned
|
|
|
|
|
|
def _remove_dangerous_tags(html: str) -> str:
|
|
"""위험 태그 제거."""
|
|
# <script>...</script> 제거
|
|
html = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
# <iframe> 제거
|
|
html = re.sub(r"<iframe[^>]*>.*?</iframe>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
# on* 이벤트 핸들러 제거
|
|
html = re.sub(r'\s+on\w+="[^"]*"', "", html, flags=re.IGNORECASE)
|
|
html = re.sub(r"\s+on\w+='[^']*'", "", html, flags=re.IGNORECASE)
|
|
return html
|
|
|
|
|
|
def _check_token_violations(html: str, area_name: str) -> None:
|
|
"""CSS에서 디자인 토큰을 사용하지 않고 하드코딩한 값을 경고한다.
|
|
|
|
정보용 경고만. 생성을 차단하지는 않음 (AI가 블록 CSS 패턴을 참고하므로 하드코딩이 일부 있을 수 있음).
|
|
"""
|
|
# <style> 블록 추출
|
|
style_blocks = re.findall(r"<style[^>]*>(.*?)</style>", html, re.DOTALL | re.IGNORECASE)
|
|
if not style_blocks:
|
|
return
|
|
|
|
all_css = "\n".join(style_blocks)
|
|
|
|
# 허용된 하드코딩 색상 (블록 CSS 패턴에서 자주 쓰이는 것)
|
|
allowed_colors = {
|
|
"#1e293b", "#0f172a", "#2563eb", "#93c5fd", "#60a5fa",
|
|
"#ffffff", "#f8fafc", "#e2e8f0", "#475569", "#64748b",
|
|
"#94a3b8", "#cbd5e1", "#1e40af", "#bae6fd", "#f0f9ff",
|
|
"#006aff", "#00aaff", "#dc2626", "#fbbf24", "#0c4a6e",
|
|
"#334155", "#0088cc", "#86efac", "#fde047", "#16a34a",
|
|
"#fecaca", "#fef2f2",
|
|
}
|
|
|
|
# 하드코딩 색상 검출 (#으로 시작하는 hex)
|
|
found_colors = set(re.findall(r"#[0-9a-fA-F]{3,8}", all_css))
|
|
unknown_colors = found_colors - allowed_colors
|
|
|
|
if unknown_colors:
|
|
logger.info(
|
|
f"[R' 검증] {area_name}: 디자인 토큰 외 색상 {len(unknown_colors)}개 "
|
|
f"(참고: {list(unknown_colors)[:5]})"
|
|
)
|