블록 선택 방식(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>
178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
"""Phase S-2: 디자인 토큰 + CSS 패턴 + 레이아웃 규칙을 프롬프트용으로 추출.
|
||
|
||
AI(Claude Sonnet)가 HTML을 생성할 때 참고하는 디자인 시스템 컨텍스트.
|
||
블록을 "선택"하는 것이 아니라, CSS 스타일을 "참고"하는 것.
|
||
|
||
Phase S 검증 결과 기반: 이미지 배치, 들여쓰기, 팝업 규칙 포함.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).parent.parent
|
||
|
||
def load_design_tokens() -> str:
|
||
"""tokens.css 전문을 반환."""
|
||
tokens_path = ROOT / "static" / "tokens.css"
|
||
if tokens_path.exists():
|
||
return tokens_path.read_text(encoding="utf-8")
|
||
return ""
|
||
|
||
|
||
def load_base_css() -> str:
|
||
"""base.css 전문을 반환."""
|
||
base_path = ROOT / "static" / "base.css"
|
||
if base_path.exists():
|
||
return base_path.read_text(encoding="utf-8")
|
||
return ""
|
||
|
||
|
||
def get_block_css_patterns() -> str:
|
||
"""기존 블록에서 자주 쓰이는 CSS 패턴을 발췌하여 참고용 텍스트로 반환.
|
||
|
||
블록을 "선택"하기 위한 것이 아니라, AI가 HTML 생성 시 스타일을 "참고"하기 위한 것.
|
||
"""
|
||
return """
|
||
## 블록 CSS 패턴 참고 (스타일만 참고, 구조는 AI가 결정)
|
||
|
||
### 다크 배경 패턴
|
||
배경: linear-gradient(135deg, #1e293b, #0f172a)
|
||
텍스트: #ffffff, 제목: #93c5fd, 강조: #fbbf24
|
||
border-radius: 8px, padding: 14px 20px
|
||
불릿 마커: #60a5fa
|
||
카드 내부: rgba(255,255,255,0.06), border-left: 3px solid #60a5fa
|
||
|
||
### 밝은 카드 패턴
|
||
배경: #f8fafc, 테두리: 1px solid #e2e8f0
|
||
border-radius: 8px, padding: 12px 16px
|
||
제목: 15px bold #1e293b, 설명: 13px #475569
|
||
원형 번호: 32px, background: #2563eb, color: white
|
||
|
||
### 포함 관계 시각화 패턴
|
||
외곽 박스: border: 3px solid #2563eb, border-radius: 14px
|
||
배경: linear-gradient(180deg, #eff6ff, #dbeafe)
|
||
라벨 배지: background: #2563eb, color: white, border-radius: 10px, position: absolute top: -11px
|
||
내부 카드: background: white, border: 2px solid #93c5fd, border-radius: 8px
|
||
아이콘 원: background: linear-gradient(135deg, #93c5fd, #2563eb), 36px, border-radius: 50%
|
||
|
||
### 강조 메시지 패턴
|
||
배경: #f0f9ff, 테두리: 2px solid #bae6fd
|
||
텍스트: #0c4a6e, 강조 색상: #dc2626 (danger)
|
||
border-radius: 8px, padding: 10px 16px, text-align: center
|
||
|
||
### 배너/결론 패턴
|
||
배경: linear-gradient(135deg, #006aff, #00aaff)
|
||
텍스트: white, font-size: 15px bold
|
||
border-radius: 8px, padding: 14px 30px, text-align: center
|
||
|
||
### 테이블 패턴
|
||
헤더: background: #1e293b, color: white, padding: 8px 12px
|
||
행: border-bottom: 1px solid #e2e8f0, 짝수행: background: #f8fafc
|
||
첫 열: font-weight: 600, color: #1e293b
|
||
|
||
### 구분선 패턴
|
||
좌우 선: height: 1px, background: #cbd5e1
|
||
중앙 텍스트: 13px bold #64748b
|
||
|
||
### Sidebar 정의 카드 패턴
|
||
배경: #f8fafc, 테두리: 1px solid #e2e8f0
|
||
번호: 22px 원형, background: #2563eb
|
||
제목: 14px bold, 설명: 12px #475569
|
||
출처: 10px italic #94a3b8
|
||
"""
|
||
|
||
|
||
def get_layout_rules() -> str:
|
||
"""Phase S 검증 결과 기반 레이아웃 규칙."""
|
||
return """
|
||
## 레이아웃 규칙 (검증 결과 기반 — 반드시 따를 것)
|
||
|
||
### 들여쓰기 규칙 (절대 규칙)
|
||
- 불릿(•) 다음 줄은 반드시 불릿 옆 글자 시작 위치에 맞춤
|
||
- CSS: padding-left: 14px; text-indent: -14px;
|
||
- 하위 불릿(◦): padding-left: 28px; text-indent: -14px;
|
||
- 줄바꿈은 단어 기준 (word-break: keep-all)
|
||
|
||
### 이미지 배치 규칙
|
||
- 이미지가 있으면 float: right 또는 left로 텍스트와 어우러지게 (텍스트 감싸기)
|
||
- 이미지 크기: 컨테이너 너비의 30-40% (이미지 비율에 따라 판단)
|
||
- 이미지 border 없이 깔끔하게
|
||
- 이미지 바로 아래에 캡션 (이미지에 가까이 붙임, margin-top: 1-2px)
|
||
- 이미지 위치: 상단 텍스트 1-2줄은 전체 너비 → 그 아래에서 이미지 시작 (margin-top으로 조정)
|
||
- 이미지 아래 빈 공간에 관련 요소 배치 (팝업 링크, 부가 정보 등)
|
||
|
||
### 팝업 규칙
|
||
- 비교표, 상세 데이터 등은 <details>/<summary>로 팝업
|
||
- 팝업 링크: 상단 오른쪽에 텍스트 링크 (박스 불필요)
|
||
예: <summary>📊 DX와 BIM의 상세 비교</summary>
|
||
- 팝업 내용: position: absolute, background: white, box-shadow, z-index: 10
|
||
|
||
### 여백 규칙
|
||
- 빈 공간 최소화, 컨테이너를 꽉 채움
|
||
- 패딩/간격을 과도하게 넣지 않는다
|
||
- 이미지 아래 빈 공간에는 관련 요소(캡션, 팝업 링크 등) 배치
|
||
|
||
### 텍스트 보존 규칙 (절대 규칙)
|
||
- 원본 MDX의 80-95%를 그대로 사용
|
||
- source_data에 있는 텍스트를 그대로 가져온다
|
||
- 축약/요약/재작성 금지
|
||
- 삭제는 공간 부족 시에만, 뒤에서부터 자르기
|
||
- "상위개념", "하위기술" 같은 임의 라벨을 붙이지 않는다
|
||
"""
|
||
|
||
|
||
def build_design_context(container_specs: dict, images: list[dict] | None = None) -> str:
|
||
"""AI HTML 생성을 위한 전체 디자인 컨텍스트를 구성한다.
|
||
|
||
Args:
|
||
container_specs: 역할별 ContainerSpec
|
||
images: 이미지 정보 리스트 [{"path", "width", "height", "ratio", "topic_id"}, ...]
|
||
|
||
Returns:
|
||
AI 프롬프트에 포함할 디자인 시스템 텍스트
|
||
"""
|
||
if images is None:
|
||
images = []
|
||
tokens = load_design_tokens()
|
||
patterns = get_block_css_patterns()
|
||
|
||
# 컨테이너 정보
|
||
container_lines = []
|
||
for role, spec in container_specs.items():
|
||
container_lines.append(
|
||
f"- {role} ({spec.zone}): {spec.height_px}px × {spec.width_px}px, "
|
||
f"topics={spec.topic_ids}, weight={spec.weight}"
|
||
)
|
||
container_text = "\n".join(container_lines)
|
||
|
||
# 이미지 정보
|
||
image_text = ""
|
||
if images:
|
||
img_lines = []
|
||
for img in images:
|
||
img_lines.append(
|
||
f"- 경로: {img.get('path', '')}\n"
|
||
f" 크기: {img.get('width', '?')}px × {img.get('height', '?')}px\n"
|
||
f" 비율: {img.get('ratio', '?')} ({'가로형' if img.get('ratio', 1) > 1.5 else '세로형' if img.get('ratio', 1) < 0.7 else '정방형'})\n"
|
||
f" 소속 topic: {img.get('topic_id', '?')}"
|
||
)
|
||
image_text = "\n## 이미지 정보\n" + "\n".join(img_lines)
|
||
|
||
return f"""## 디자인 토큰 (CSS 변수 — 이 패턴을 참고)
|
||
{tokens}
|
||
|
||
## 슬라이드 프레임
|
||
- 크기: 1280px × 720px (고정)
|
||
- 폰트: Pretendard Variable
|
||
- 한국어 word-break: keep-all
|
||
- padding: 36px 40px 24px
|
||
|
||
## 컨테이너 스펙 (각 영역의 크기)
|
||
{container_text}
|
||
{image_text}
|
||
|
||
{patterns}
|
||
|
||
{get_layout_rules()}
|
||
"""
|