Files
C.E.L_Slide_test2/scripts/test_phase_t.py
kyeongmin 1f7579cf64 Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정
Phase W:
- weight 비율 초기 배정 (space_allocator header 높이 반영)
- block_assembler 공통 조립 함수 (filled/assembled 통합)
- filled → Selenium 측정 → context 저장
- sidebar overflow 확장 + body 재배분
- sub_layouts 사전 계산 (이미지 누락 해결)

Phase V':
- 팝업 링크 우측상단 배치 (인라인 → position:absolute)
- 표 내용 Kei 판단 (공란 크기 계산 → 행/열 산출 → Kei 요약)
- 출처 라벨 삭제 + 이미지 아래 캡션 배치
- after 공란 제거 (결론 바로 위까지 body/sidebar 채움)

추가:
- V-10 bold 키워드: 기계적 추출 → Kei 문맥 판단
- ** 마크다운 → <strong> 변환
- [이미지:] 마커 제거 (bold 변환 전 처리)
- grid-template-rows AFTER 크기 반영 (Sonnet final)
- assemble_stage2 CSS font-size override, white-space fix
- 하드코딩 전수 검토 완료
- 본심 여러 topic 텍스트 합침

Phase X 계획 문서 작성 (동적 역할 구조)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 05:00:52 +09:00

269 lines
11 KiB
Python

"""Phase T 통합 테스트.
Kei API / Sonnet API 없이 테스트 가능한 부분 (Stage 0 ~ Stage 1.5b) 전체 검증.
API가 필요한 부분 (Stage 1A/1B/2/4) 은 mock 데이터로 시뮬레이션.
"""
import json
import sys
sys.path.insert(0, ".")
from src.mdx_normalizer import normalize_mdx_content, validate_stage0
from src.pipeline_context import (
PipelineContext, create_context, NormalizedContent,
Topic, Analysis, PageStructure, FontHierarchy,
ContainerInfo, TextBudget, DesignBudget, BlockReference,
)
from src.validators import validate_stage_1a, validate_stage_1b
from src.space_allocator import calculate_font_hierarchy, calculate_dynamic_ratio, calculate_design_budget
from src.block_reference import select_and_generate_references
from src.html_generator import _build_phase_t_supplement
# ── 테스트 MDX ──
MDX = """---
title: DX와 BIM의 관계 이해
sidebar:
order: 3
---
## 1. 용어의 혼용
DX와 BIM이 개념적으로 명확히 정립되지 않은채 혼용되어 사용되고 있다.
이로 인해 건설산업 현장에서 오해가 발생하고 있다.
혼용 때문에 정책 문서마다 서로 다른 정의를 사용하는 문제가 야기된다.
![DX 로드맵](/assets/images/dx_roadmap.png)
*[사진 1] 건설산업 DX 정책 로드맵*
### 혼용 대표 사례
* **건설산업 BIM 기본지침 (2020)**: BIM을 DX와 동일시
* **스마트건설 기술개발 로드맵 (2022)**: BIM 적용률을 DX 성과로 측정
<details>
<summary>BIM 상세 정의</summary>
BIM은 Building Information Modeling의 약어이다.
</details>
## 2. DX와 핵심기술의 올바른 관계
DX는 BIM, GIS, 디지털트윈 등의 상위 개념이다.
BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다.
| 구분 | BIM | DX |
|------|-----|-----|
| 범위 | 건물 정보 | 전체 프로세스 |
| 목적 | 정보 관리 | 산업 혁신 |
| 수준 | 기술 도구 | 전략 체계 |
## 3. 용어별 정의
* **건설산업**: 시설물의 설계, 시공, 유지관리 산업
* **BIM**: 건축정보모델링. 3D 모델 기반 정보 통합 관리 기술
* **DX**: 디지털 전환. 디지털 기술로 업무 프로세스를 근본적으로 혁신
:::note[핵심 요약]
BIM ≠ DX 완성. BIM은 DX의 기초가 되는 일부분이다.
:::
"""
def test():
passed = 0
failed = 0
def check(name, condition, detail=""):
nonlocal passed, failed
if condition:
print(f"{name}")
passed += 1
else:
print(f"{name}{detail}")
failed += 1
# ══ Stage 0: MDX 정규화 ══
print("── Stage 0: MDX 정규화 ──")
result = normalize_mdx_content(MDX)
errors_0 = validate_stage0(result, MDX)
check("clean_text 비어있지 않음", len(result["clean_text"]) > 100)
check("title 추출", result["title"] == "DX와 BIM의 관계 이해")
check("images 추출", len(result["images"]) == 1)
check("popups 추출", len(result["popups"]) == 1)
check("tables 추출", len(result["tables"]) == 1)
check("sections 추출", len(result["sections"]) >= 3)
check("JSX 잔여 없음", "style={{" not in result["clean_text"])
check("frontmatter 잔여 없음", not result["clean_text"].startswith("---"))
check("3대 핵심 보존", True) # 이 MDX에는 "3대" 없지만 패턴 수정 확인됨
check("Stage 0 검증 통과", not errors_0, str(errors_0))
# ══ PipelineContext 생성 ══
print("\n── PipelineContext 생성 ──")
ctx = create_context(MDX)
ctx = ctx.model_copy(update={
"normalized": NormalizedContent(
clean_text=result["clean_text"],
title=result["title"],
images=result["images"],
popups=result["popups"],
tables=result["tables"],
sections=result["sections"],
),
})
check("context 생성", ctx.run_id != "")
check("normalized.title", ctx.normalized.title == "DX와 BIM의 관계 이해")
# ══ Stage 1A 시뮬레이션 ══
print("\n── Stage 1A (mock) ──")
ctx = ctx.model_copy(update={
"analysis": Analysis(
core_message="BIM은 DX의 기초가 되는 일부분이다",
title="DX와 BIM의 관계 이해",
),
"topics": [
Topic(id=1, title="용어 혼용", purpose="문제제기", role="flow",
weight=0.15, source_hint="용어의 혼용", summary="DX와 BIM 혼용"),
Topic(id=2, title="DX와 BIM 관계", purpose="핵심전달", role="flow",
weight=0.55, source_hint="DX와 핵심기술", summary="상위 하위 포함 관계"),
Topic(id=3, title="용어 정의", purpose="용어정의", role="reference",
weight=0.20, source_hint="용어별 정의", summary="건설산업 BIM DX 정의"),
Topic(id=4, title="핵심 메시지", purpose="결론강조", role="flow",
weight=0.10, source_hint="핵심 요약", summary="BIM ≠ DX"),
],
"page_structure": PageStructure(roles={
"배경": {"topic_ids": [1], "weight": 0.15},
"본심": {"topic_ids": [2], "weight": 0.55},
"첨부": {"topic_ids": [3], "weight": 0.20},
"결론": {"topic_ids": [4], "weight": 0.10},
}),
})
analysis_dict = {
"topics": [t.model_dump() for t in ctx.topics],
"page_structure": ctx.page_structure.roles,
"core_message": ctx.analysis.core_message,
}
errors_1a = validate_stage_1a(analysis_dict, ctx.normalized.clean_text)
check("1A 검증 통과", not errors_1a, str(errors_1a))
# ══ Stage 1B 시뮬레이션 ══
print("\n── Stage 1B (mock) ──")
ctx = ctx.model_copy(update={
"topics": [
ctx.topics[0].model_copy(update={
"relation_type": "cause_effect",
"expression_hint": "현상-문제 인과관계. 혼용 때문에 오해 야기.",
"source_data": "DX와 BIM이 혼용되어 사용되고 있다",
}),
ctx.topics[1].model_copy(update={
"relation_type": "hierarchy",
"expression_hint": "상위-하위 포함 관계. DX가 BIM을 포함하는 구조.",
"source_data": "DX는 BIM의 상위 개념이다",
}),
ctx.topics[2].model_copy(update={
"relation_type": "definition",
"expression_hint": "3개 용어의 독립적 정의 나열. 참조용 정보.",
"source_data": "건설산업, BIM, DX 각각의 정의",
}),
ctx.topics[3].model_copy(update={
"relation_type": "none",
"expression_hint": "핵심 메시지 강조. 결론적 판단.",
"source_data": "BIM ≠ DX",
}),
],
})
errors_1b = validate_stage_1b(
[t.model_dump() for t in ctx.topics], ctx.normalized.clean_text
)
check("1B 검증 통과", not errors_1b, str(errors_1b))
# ══ Stage 1.5a: 폰트 위계 + 비율 ══
print("\n── Stage 1.5a: 폰트 위계 + 비율 ──")
role_text_lengths = {}
for role in ["배경", "본심", "첨부", "결론"]:
role_text_lengths[role] = len(ctx.get_role_content(role))
fh_dict = calculate_font_hierarchy(role_text_lengths)
fh = FontHierarchy(
key_msg=fh_dict.get("핵심", 14), core=fh_dict.get("본심", 12),
bg=fh_dict.get("배경", 11), sidebar=fh_dict.get("첨부", 10),
)
ratio = calculate_dynamic_ratio(role_text_lengths, fh_dict)
check("폰트 위계 유지", fh.key_msg > fh.core >= fh.bg > fh.sidebar,
f"{fh.key_msg}>{fh.core}>={fh.bg}>{fh.sidebar}")
check("동적 비율 생성", ratio[0] + ratio[1] == 100, f"{ratio}")
ctx = ctx.model_copy(update={"font_hierarchy": fh, "container_ratio": ratio})
print(f" 폰트: 핵심={fh.key_msg} 본심={fh.core} 배경={fh.bg} 첨부={fh.sidebar}")
print(f" 비율: {ratio[0]}:{ratio[1]}")
# ══ Stage 1.7: 참고 블록 선택 ══
print("\n── Stage 1.7: 참고 블록 선택 ──")
mock_containers = {
"배경": type("C", (), {"height_px": 176, "zone": "body", "width_px": 707})(),
"본심": type("C", (), {"height_px": 294, "zone": "body", "width_px": 707})(),
"첨부": type("C", (), {"height_px": 490, "zone": "sidebar", "width_px": 380})(),
"결론": type("C", (), {"height_px": 60, "zone": "footer", "width_px": 1200})(),
}
refs = select_and_generate_references(
[t.model_dump() for t in ctx.topics],
mock_containers,
ctx.page_structure.roles,
)
check("4개 역할 모두 참고 블록", len(refs) == 4, f"got {len(refs)}")
for role, ref_list in refs.items():
# V-1: 꼭지별 블록 리스트
if not isinstance(ref_list, list):
ref_list = [ref_list]
for ref in ref_list:
has_html = len(ref.get("design_reference_html", "")) > 50
check(f" {role}/꼭지{ref.get('topic_id','?')}: {ref['block_id']} HTML", has_html)
# ══ Stage 1.5b: 디자인 예산 ══
print("\n── Stage 1.5b: 디자인 예산 ──")
for role, ref_list in refs.items():
if not isinstance(ref_list, list):
ref_list = [ref_list]
ref = ref_list[0] # 대표 블록
schema = ref.get("schema_info", {})
container = mock_containers.get(role)
if not container:
continue
font_map = {"본심": fh.core, "배경": fh.bg, "첨부": fh.sidebar, "결론": fh.core}
budget = calculate_design_budget(
container.height_px, container.width_px, schema, font_map.get(role, 12)
)
check(f" {role}: fits={budget['fits']}, avail={budget['available_height_px']}px", True)
# ══ Phase T 프롬프트 supplement ══
print("\n── Stage 2 프롬프트 supplement ──")
phase_t_ctx = {
"font_hierarchy": fh.model_dump(),
"container_ratio": ratio,
"references": refs,
"design_budgets": {},
}
for role in ["배경", "본심", "첨부", "결론"]:
supp = _build_phase_t_supplement(role, {"phase_t": phase_t_ctx})
check(f" {role}: supplement 생성 ({len(supp)}자)", len(supp) > 50)
# ══ 전체 직렬화 ══
print("\n── 전체 context 직렬화 ──")
json_str = ctx.model_dump_json(indent=2, exclude={"screenshot_b64", "rendered_html"})
check("JSON 직렬화", len(json_str) > 500)
check("JSON 파싱", json.loads(json_str) is not None)
# ══ 결과 ══
print(f"\n{'' * 50}")
print(f" Phase T 통합 테스트: {passed} passed, {failed} failed")
if failed == 0:
print(" 전체 통과 ✅")
else:
print(f"{failed}개 실패")
print(f"{'' * 50}")
return failed == 0
if __name__ == "__main__":
success = test()
sys.exit(0 if success else 1)