diff --git a/PLAN.md b/PLAN.md
index cd29226..706e97b 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -393,6 +393,36 @@ P2-E (누락기능) ── 병렬 │
---
+## Phase Y: MDX 외부 컴포넌트 인라인 삽입
+
+> 근거: MDX에서 `import ... from '*.astro'`로 불러오는 외부 컴포넌트(표, 다이어그램 등)가 파이프라인에서 누락됨. import문은 제거되고 `` 같은 태그는 사라져서 콘텐츠 손실 발생.
+
+### Y-1: import문 파싱 — 컴포넌트명:파일경로 매핑
+- **파일:** `src/mdx_normalizer.py`
+- **내용:** `import Foo from '../../components/foo.astro'` → `{"Foo": 절대경로}` 매핑 추출
+- **의존성:** base_path (MDX 원본 파일 위치, pipeline.py에서 전달)
+- **완료 기준:** import문에서 컴포넌트명→절대경로 dict 반환
+
+### Y-2: .astro 파일 파싱 — HTML + CSS 추출
+- **파일:** `src/mdx_normalizer.py`
+- **내용:** .astro 파일에서 `---` frontmatter 제거, HTML 본문 + `` 반환
+
+### Y-3: 셀프클로징 태그 교체 — 인라인 삽입
+- **파일:** `src/mdx_normalizer.py`
+- **내용:** `` 태그를 Y-2에서 추출한 HTML+CSS로 교체
+- **의존성:** Y-1, Y-2
+- **완료 기준:** MDX 정규화 결과에 외부 컴포넌트 HTML이 인라인으로 포함
+
+### Y-4: Astro 특수 문법 정리
+- **파일:** `src/mdx_normalizer.py`
+- **내용:** Astro의 멀티라인 태그(`
텍스트 | ` 줄바꿈 패턴), `style="letter-spacing: -0.9px"` 등 인라인 스타일 정리
+- **의존성:** Y-2
+- **완료 기준:** 추출된 HTML이 브라우저에서 정상 렌더링
+
+---
+
## 의존 관계
```
diff --git a/scripts/run_from_stage1b.py b/scripts/run_from_stage1b.py
index c89834a..08d434c 100644
--- a/scripts/run_from_stage1b.py
+++ b/scripts/run_from_stage1b.py
@@ -22,13 +22,8 @@ async def main(run_dir: str):
# Stage 1B context 로드
ctx_json = json.loads((run / "stage_1b_context.json").read_text(encoding="utf-8"))
- # MDX 원본: samples에서 직접 읽기 (최신 원본 사용)
- samples_dir = Path(__file__).parent.parent / "samples"
- mdx_file = samples_dir / "mdx" / "01. 건설산업 DX의 올바른 이해(0127).mdx"
- if mdx_file.exists():
- raw_content = mdx_file.read_text(encoding="utf-8")
- else:
- raw_content = ctx_json.get("raw_content", "")
+ # MDX 원본: context에서 가져옴 (어떤 MDX든 대응)
+ raw_content = ctx_json.get("raw_content", "")
# Stage 1A 결과를 manual_layout으로 전달 (Stage 1A 스킵)
# page_structure가 {"roles": {...}} 형태이면 roles 안쪽을 직접 전달
@@ -41,9 +36,11 @@ async def main(run_dir: str):
"page_structure": ps,
"core_message": ctx_json.get("analysis", {}).get("core_message", ""),
"title": ctx_json.get("analysis", {}).get("title", ""),
+ "layout_template": ctx_json.get("analysis", {}).get("layout_template", "A"),
}
- print(f"=== Stage 1B 데이터 고정: {run.name} ===")
+ layout = manual_layout.get("layout_template", "A")
+ print(f"=== Stage 1B 데이터 고정: {run.name} (유형 {layout}) ===")
print(f" topics: {len(ctx_json['topics'])}개")
for t in ctx_json["topics"]:
print(f" 꼭지{t['id']}: {t['title']} (st={len(t.get('structured_text',''))}자)")
@@ -51,8 +48,8 @@ async def main(run_dir: str):
# pipeline.py의 generate_slide() 호출
from src.pipeline import generate_slide
- # 이미지 base_path: samples/images/
- base_path = str(samples_dir / "images")
+ # 이미지 base_path: context에서 가져옴
+ base_path = ctx_json.get("base_path", "")
async for event in generate_slide(raw_content, manual_layout=manual_layout, base_path=base_path):
ev_type = event.get("event", "")
ev_data = event.get("data", "")
diff --git a/src/block_assembler.py b/src/block_assembler.py
index 22132de..10b2331 100644
--- a/src/block_assembler.py
+++ b/src/block_assembler.py
@@ -367,7 +367,15 @@ def assemble_slide_html(ctx: "PipelineContext", title_text: str = "") -> str:
"""전체 슬라이드를 조립하여 HTML 반환.
filled, assembled, stage_2 모두 이 함수를 호출.
+ layout_template에 따라 유형 A/B 분기.
"""
+ if ctx.analysis.layout_template == "B":
+ return _assemble_slide_html_type_b(ctx, title_text)
+ return _assemble_slide_html_type_a(ctx, title_text)
+
+
+def _assemble_slide_html_type_a(ctx: "PipelineContext", title_text: str = "") -> str:
+ """유형 A 전체 슬라이드 조립 (기존 코드 그대로)."""
from src.fit_verifier import _load_design_tokens
tokens = _load_design_tokens()
pad = tokens["spacing_page"]
@@ -443,3 +451,378 @@ body{{background:#e5e5e5;padding:10px;font-family:'Pretendard Variable','Noto Sa
{role_htmls.get("결론", "")}