"""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 성과로 측정
BIM 상세 정의 BIM은 Building Information Modeling의 약어이다.
## 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)