Phase U: 하드코딩 10건 제거 — 범용 프롬프트 시스템

제거:
- _get_popup_data() 함수 삭제 (DX/BIM 비교표 하드코딩)
- "📊 DX와 BIM의 상세 비교" 팝업 링크 → Claude 자율 판단
- "BIM ≠ DX" 예시 → core_message 변수만
- "상위개념/하위기술/포함관계" 금지어 → 범용 "임의 라벨 금지"
- fallback 키워드 ["혼용","사례"], ["관계","핵심기술","DX"] → source_hint 동적 추출
- "사례 카드" → "토픽" 범용화
- "BIM (Building Information Modeling)" 예시 → 제거

추가:
- _extract_keywords_from_hints(): source_hint에서 섹션명 키워드 동적 추출
- 팝업: 원본에 비교 구조 있으면 Claude가 자체 판단, 없으면 팝업 없음
- content_verifier: body_bg overflow 패턴 OR 수정, popup-link 필수 해제

회귀 테스트: 기존 MDX 전체 PASS (1차 시도)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 10:44:45 +09:00
parent 83f589ca52
commit 24eb1bc5ad
2 changed files with 43 additions and 31 deletions

View File

@@ -284,7 +284,7 @@ def detect_invented_text(
""" """
# 허용 예외 (구조적 라벨) # 허용 예외 (구조적 라벨)
allowed_labels = { allowed_labels = {
"용어 정의", "핵심 메시지", "상세 비교", "DX와 BIM의 상세 비교", "용어 정의", "핵심 메시지", "상세 비교",
} }
html_texts = extract_text_from_html(generated_html) html_texts = extract_text_from_html(generated_html)
@@ -377,12 +377,10 @@ def verify_no_forbidden_content(
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
REQUIRED_PATTERNS: dict[str, list[str]] = { REQUIRED_PATTERNS: dict[str, list[str]] = {
"body_bg": ["overflow:hidden", "overflow: hidden"], "body_bg": ["overflow:hidden|overflow: hidden"],
"body_core": [ "body_core": [
"overflow:hidden|overflow: hidden", "overflow:hidden|overflow: hidden",
"float:right|float: right",
"key-msg", "key-msg",
"popup-link",
], ],
"sidebar": [ "sidebar": [
"overflow:hidden|overflow: hidden", "overflow:hidden|overflow: hidden",

View File

@@ -52,10 +52,10 @@ BG_PROMPT = """다음 콘텐츠를 배경(보조) 영역 HTML로 만들어라.
- 전체 padding: 10px 14px (여백 최소화) - 전체 padding: 10px 14px (여백 최소화)
- 제목: 12px bold #334155, margin-bottom: 4px - 제목: 12px bold #334155, margin-bottom: 4px
- 본문: 11px #475569, line-height: 1.4, 핵심 키워드 <strong style="color:#1e293b"> 처리 - 본문: 11px #475569, line-height: 1.4, 핵심 키워드 <strong style="color:#1e293b"> 처리
- 사례가 여러 이면 가로로 나란히 (flex, gap:8px) - 토픽이 여러 이면 가로로 나란히 (flex, gap:8px)
- 사례 카드: background:#ffffff, border-left: 2px solid #94a3b8, padding: 6px 8px (여백 최소화) - 각 토픽 구분: background:#ffffff, border-left: 2px solid #94a3b8, padding: 6px 8px (여백 최소화)
- 사례 제목: 10px bold #334155, margin-bottom: 2px - 토픽 제목: 10px bold #334155, margin-bottom: 2px
- 사례 내용: 9px #64748b, line-height: 1.3 - 토픽 내용: 9px #64748b, line-height: 1.3
- 들여쓰기: 불릿은 인라인 style만 사용. CSS class 사용 금지 (<style> 블록 금지). - 들여쓰기: 불릿은 인라인 style만 사용. CSS class 사용 금지 (<style> 블록 금지).
불릿 마커: 텍스트로 "" 직접 삽입 (::before 금지) 불릿 마커: 텍스트로 "" 직접 삽입 (::before 금지)
들여쓰기: style="padding-left:14px; text-indent:-14px;" 인라인으로. 들여쓰기: style="padding-left:14px; text-indent:-14px;" 인라인으로.
@@ -174,7 +174,6 @@ CORE_PROMPT = """다음 콘텐츠를 본심 영역 HTML로 만들어라.
<div class="core"> <div class="core">
<div class="core-header"> <div class="core-header">
<div class="core-label">제목 라벨</div> <div class="core-label">제목 라벨</div>
<span class="popup-link">📊 DX와 BIM의 상세 비교</span>
</div> </div>
<div class="core-text"> <div class="core-text">
<div class="fi"> <div class="fi">
@@ -184,16 +183,14 @@ CORE_PROMPT = """다음 콘텐츠를 본심 영역 HTML로 만들어라.
<div class="bp">메인 불릿 1</div> <div class="bp">메인 불릿 1</div>
<div class="bp">메인 불릿 2</div> <div class="bp">메인 불릿 2</div>
<div class="sp"><b>하위 항목</b> : 설명</div> <div class="sp"><b>하위 항목</b> : 설명</div>
<div class="sp"><b>하위 항목</b> : 설명</div>
</div> </div>
<div class="key-msg"> <div class="key-msg">
<em>BIM ≠ DX</em> — 핵심 메시지 (analysis.core_message 사용) 핵심 메시지 텍스트
</div> </div>
주의: "상위개념", "하위기술", "포함관계" 같은 임의 라벨을 넣지 마라. core_message 텍스트를 그대로 사용.
</div> </div>
``` ```
## 핵심 메시지 (하단 key-msg 박스에 이 텍스트 그대로 사용) ## 핵심 메시지 (하단 key-msg 박스에 이 텍스트 그대로 사용. 임의 라벨/요약을 넣지 말고 아래 텍스트 그대로.)
{core_message} {core_message}
## 콘텐츠 (축약/요약/삭제 금지. 원본 텍스트 80-95% 그대로 사용.) ## 콘텐츠 (축약/요약/삭제 금지. 원본 텍스트 80-95% 그대로 사용.)
@@ -204,17 +201,19 @@ CORE_PROMPT = """다음 콘텐츠를 본심 영역 HTML로 만들어라.
2. 마침표(.)로 끝나는 문장이 2개 이상이면 각각 별도 불릿(•)으로 분리. 2. 마침표(.)로 끝나는 문장이 2개 이상이면 각각 별도 불릿(•)으로 분리.
3. 개조식 어미 변환: 문장 끝 1-2글자만 변환. 그 외 단어는 절대 건드리지 마라. 3. 개조식 어미 변환: 문장 끝 1-2글자만 변환. 그 외 단어는 절대 건드리지 마라.
- "~있다""~있음", "~한다""~함", "~이다" → 삭제, "~된다""~됨" - "~있다""~있음", "~한다""~함", "~이다" → 삭제, "~된다""~됨"
예: "실현 가능한 상위개념이다""실현 가능한 상위개념" (끝의 "이다"만 삭제) 예: "활용하는 과정이다""활용하는 과정" (끝만 변환)
4. 원본에 없는 텍스트를 추가하지 마라. 4. 원본에 없는 텍스트를 추가하지 마라.
{img_instruction} {img_instruction}
## 팝업 비교표 데이터 (<details>/<summary>로 구현) ## 팝업 (<details>/<summary>)
{popup_data} 원본 콘텐츠에 비교표/대조 구조가 있으면 <details>/<summary>로 팝업 구현하라.
없으면 팝업을 만들지 마라. 팝업 제목은 콘텐츠에 맞게 자동 결정.
팝업은 우측 상단에 1개만. 중복 금지.
## 절대 규칙 ## 절대 규칙
- "DX와 BIM의 상세 비교" 팝업 링크는 우측 상단에 1개만. 하단이나 다른 곳에 중복으로 넣지 마라.
- .core에 margin-top을 넣지 마라 (간격은 외부에서 처리됨). margin-top: 0 필수. - .core에 margin-top을 넣지 마라 (간격은 외부에서 처리됨). margin-top: 0 필수.
- 원본에 없는 텍스트나 라벨을 임의로 넣지 마라.
HTML + inline <style>만 반환. 위 CSS와 HTML 구조를 정확히 따르라. 설명 없이 코드만.""" HTML + inline <style>만 반환. 위 CSS와 HTML 구조를 정확히 따르라. 설명 없이 코드만."""
@@ -229,7 +228,7 @@ SIDEBAR_PROMPT = """다음 용어 정의를 sidebar 카드로 만들어라. {wid
2. 상단에 "용어 정의" 라벨 (카드 좌측 시작점에 맞춰 좌측 정렬, 10px #64748b) 2. 상단에 "용어 정의" 라벨 (카드 좌측 시작점에 맞춰 좌측 정렬, 10px #64748b)
3. 각 용어를 카드로: 3. 각 용어를 카드로:
- 배경: #f8fafc, 테두리: 1px solid #e2e8f0, border-radius: 8px, padding: 14px - 배경: #f8fafc, 테두리: 1px solid #e2e8f0, border-radius: 8px, padding: 14px
- 용어명: 11px bold #1e293b (예: "BIM (Building Information Modeling)") - 용어명: 11px bold #1e293b
- 부제 금지: 원본에 없는 텍스트를 만들어 넣지 마라. 용어명 아래에 임의 설명을 추가하지 마라. - 부제 금지: 원본에 없는 텍스트를 만들어 넣지 마라. 용어명 아래에 임의 설명을 추가하지 마라.
- 불릿: 10px #475569, line-height: 1.6, 불릿 마커 "" - 불릿: 10px #475569, line-height: 1.6, 불릿 마커 ""
- 들여쓰기: 인라인 style만 사용 (CSS class 사용 금지). - 들여쓰기: 인라인 style만 사용 (CSS class 사용 금지).
@@ -319,7 +318,9 @@ async def generate_slide_html(
if bg_topics: if bg_topics:
logger.info("[Phase S] 배경 생성...") logger.info("[Phase S] 배경 생성...")
sections = _slice_mdx_sections(content) sections = _slice_mdx_sections(content)
bg_content = _map_sections_for_role(sections, bg_topics, ["혼용", "사례"]) bg_content = _map_sections_for_role(
sections, bg_topics, _extract_keywords_from_hints(bg_topics),
)
prompt = BG_PROMPT.format( prompt = BG_PROMPT.format(
height=bg_h, height=bg_h,
content_block=bg_content, content_block=bg_content,
@@ -332,8 +333,9 @@ async def generate_slide_html(
# ── 본심 ── # ── 본심 ──
if core_topics: if core_topics:
logger.info("[Phase S] 본심 생성...") logger.info("[Phase S] 본심 생성...")
core_content = _map_sections_for_role(sections, core_topics, ["관계", "핵심기술", "DX"]) core_content = _map_sections_for_role(
popup = _get_popup_data(content) sections, core_topics, _extract_keywords_from_hints(core_topics),
)
img_instruction = "" img_instruction = ""
img_margin = 60 img_margin = 60
@@ -354,7 +356,6 @@ async def generate_slide_html(
core_message=analysis.get("core_message", ""), core_message=analysis.get("core_message", ""),
content_block=core_content, content_block=core_content,
img_instruction=img_instruction, img_instruction=img_instruction,
popup_data=popup,
) )
html = await _call_claude(client, prompt) html = await _call_claude(client, prompt)
if html: if html:
@@ -470,15 +471,28 @@ def _map_sections_for_role(
return result.strip() return result.strip()
def _get_popup_data(content: str) -> str: def _extract_keywords_from_hints(topics: list[dict]) -> list[str]:
"""팝업 비교표 데이터.""" """source_hint에서 섹션명 키워드를 동적 추출.
return """비교표 (<details>/<summary> 팝업으로):
| 기준 | DX | BIM | 예: "원본의 '용어의 혼용' 섹션 전체 내용" → ["용어의", "혼용"]
| 범위 | BIM << DX (Engineering + Management 통합) | Only 3D (형상 구현 중심) | 하드코딩 fallback 키워드를 대체한다.
| 프로세스 | 근본적 문제의식을 통한 개선 | 기존 2D 설계 방식 유지 | """
| 활용 | 설계/시공 생산성 혁신 | 3D 모델에 의한 일반적 이해 향상 | keywords = []
| 확장성 | 전 생애주기 활용 시스템 | (설계/시공/운영) 분야별 단절 | for t in topics:
| 주체 | 자체 수행 능력 — 지속가능성 확보 | S/W 제작사 판매 정책에 의존 |""" hint = t.get("source_hint", "")
if not hint:
continue
# 따옴표 안의 섹션명 추출
match = re.search(r"['\"]([^'\"]+)['\"]", hint)
if match:
section_name = match.group(1)
words = [w for w in section_name.split() if len(w) >= 2]
keywords.extend(words)
else:
# 따옴표 없으면 hint에서 2글자 이상 단어 추출
words = [w for w in hint.split() if len(w) >= 2]
keywords.extend(words[:3]) # 최대 3개
return keywords
def _get_definitions(content: str) -> str: def _get_definitions(content: str) -> str: