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>
269 lines
11 KiB
Python
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이 개념적으로 명확히 정립되지 않은채 혼용되어 사용되고 있다.
|
|
이로 인해 건설산업 현장에서 오해가 발생하고 있다.
|
|
혼용 때문에 정책 문서마다 서로 다른 정의를 사용하는 문제가 야기된다.
|
|
|
|

|
|
*[사진 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)
|