diff --git a/scripts/assemble_stage2.py b/scripts/assemble_stage2.py
index 2658e25..9da4b50 100644
--- a/scripts/assemble_stage2.py
+++ b/scripts/assemble_stage2.py
@@ -677,15 +677,19 @@ def _assemble_type_b(run: Path, ctx: dict):
bottom_h = column_bottom - bottom_top
bottom_col_w = (inner_w - gap_block) // 2
- # ── 역할별 HTML 조립 ──
+ # ── normalized.sections에서 직접 텍스트 가져오기 ──
+ norm_sections = ctx.get("normalized", {}).get("sections", [])
font_size = fh.get("core", 12)
- # 상단 (텍스트 + 이미지 나란히)
+ # 상단 (텍스트 + 이미지 나란히) — sections[0] 사용
top_html = ""
if top_role:
rn, info = top_role
tids = info.get("topic_ids", [])
- all_text = "\n".join(get_text(topic_map.get(tid, {})) for tid in tids if topic_map.get(tid))
+ # MDX 원본 sections에서 직접 가져오기 (Kei structured_text 대신)
+ top_section = norm_sections[0] if norm_sections else {}
+ all_text = top_section.get("content", "")
+ topic_title_from_section = top_section.get("title", "")
# 마크다운 bold → HTML
all_text_clean = re.sub(r'\*\*(.+?)\*\*', r'\1', all_text)
@@ -792,9 +796,8 @@ def _assemble_type_b(run: Path, ctx: dict):
f'{caption_html}'
)
- # 제목
- primary_topic = topic_map.get(tids[0], {}) if tids else {}
- topic_title = bold(primary_topic.get("title", ""), rn)
+ # 제목 — MDX 원본 section 제목 사용
+ topic_title = bold(topic_title_from_section or rn, rn)
# X'-4: 상단 컨테이너 — 내용을 전체 높이에 균등 배분
top_html = (
@@ -807,75 +810,69 @@ def _assemble_type_b(run: Path, ctx: dict):
f'{img_block}'
)
- # 하단 좌측
+ # 하단: normalized.sections에서 직접 매핑
+ # sections 구조: [level=2 상단, level=2 하단대목차, level=3 하단좌, level=3 하단우, ...]
+ # 하단 대목차 = level=2 두 번째
+ # 하단 소목차들 = level=3
+ bottom_title = ""
+ sub_sections_from_norm = [] # [(제목, content)]
+ for s in norm_sections[1:]: # 상단 제외
+ if s["level"] == 2:
+ bottom_title = s.get("title", "")
+ elif s["level"] == 3:
+ sub_sections_from_norm.append((s.get("title", ""), s.get("content", "")))
+
+ bl_indent = int(font_size * 1.2)
+
+ # 하단 좌측 = 첫 번째 소목차 (level=3)
bl_html = ""
- if bottom_left_role:
- rn, info = bottom_left_role
- tids = info.get("topic_ids", [])
- all_text = "\n".join(get_text(topic_map.get(tid, {})) for tid in tids if topic_map.get(tid))
- all_text = re.sub(r'\*\*(.+?)\*\*', r'\1', all_text)
+ if sub_sections_from_norm and bottom_left_role:
+ rn = bottom_left_role[0]
+ sub_title, sub_content = sub_sections_from_norm[0]
+ sub_content = re.sub(r'\*\*(.+?)\*\*', r'\1', sub_content)
- primary_topic = topic_map.get(tids[0], {}) if tids else {}
- topic_title = bold(primary_topic.get("title", ""), rn)
-
- # X'-2: 들여쓰기 계층 (소제목+불릿)
- bl_indent = int(font_size * 1.2)
bullets = ""
- for line in all_text.split("\n"):
- stripped = line.strip()
- if not stripped or re.search(r'\[팝업:|\[이미지:', stripped):
- continue
- if stripped.startswith("### "):
- # 소제목
- sub_title = stripped.lstrip("# ").strip()
- bullets += f'
{bold(sub_title, rn)}
\n'
- else:
- clean = stripped.lstrip("• ")
- clean = bold(clean, rn)
- bullets += f'• {clean}
\n'
-
- bl_html = (
- f''
- f'
{topic_title}
'
- f'
{bullets}
'
- )
-
- # 하단 우측
- br_html = ""
- if bottom_right_role:
- rn, info = bottom_right_role
- tids = info.get("topic_ids", [])
- all_text = "\n".join(get_text(topic_map.get(tid, {})) for tid in tids if topic_map.get(tid))
- all_text = re.sub(r'\*\*(.+?)\*\*', r'\1', all_text)
-
- primary_topic = topic_map.get(tids[0], {}) if tids else {}
- topic_title = bold(primary_topic.get("title", ""), rn)
-
- # 팝업 분리
- popup_titles_br = []
- content_lines_br = []
- for line in all_text.split("\n"):
+ for line in sub_content.split("\n"):
stripped = line.strip()
if not stripped:
continue
- popup_match = re.search(r'\[팝업:\s*([^\]]+)\]', stripped)
- if popup_match:
- popup_titles_br.append(popup_match.group(1))
- continue
- if re.search(r'\[이미지:', stripped):
- continue
- content_lines_br.append(stripped)
-
- popup_html_br = ""
- if popup_titles_br:
- links = " ".join(f'[{t}→]' for t in popup_titles_br)
- popup_html_br = f'{links}
'
-
- bullets = ""
- for line in content_lines_br:
- clean = line.lstrip("• ")
+ clean = stripped.lstrip("- ").lstrip("• ")
clean = bold(clean, rn)
- bullets += f'•{clean}
\n'
+ bullets += f'• {clean}
\n'
+
+ bl_html = (
+ f''
+ f'
{bold(sub_title, rn)}
'
+ f'
{bullets}
'
+ )
+
+ # 하단 우측 = 두 번째 소목차 (level=3) + 표 요약
+ br_html = ""
+ if bottom_right_role and len(sub_sections_from_norm) > 1:
+ rn = bottom_right_role[0]
+ sub_title_br, sub_content_br = sub_sections_from_norm[1]
+ sub_content_br = re.sub(r'\*\*(.+?)\*\*', r'\1', sub_content_br)
+
+ content_lines_br = [l.strip() for l in sub_content_br.split("\n") if l.strip()]
+
+ # 팝업 링크 — 소목차 제목으로 팝업 링크 생성
+ popup_html_br = ""
+ popup_link_title = f"{sub_title_br} 바로가기"
+ popup_html_br = (
+ f''
+ f'[{popup_link_title} →]
'
+ )
+
+ # 불릿 — table_summaries가 있으면 표 데이터는 Kei 요약으로 대체되므로 불릿은 간략하게
+ table_summaries = enh.get("table_summaries", {})
+ bullets = ""
+ if not table_summaries:
+ # 표 요약 없으면 content 그대로
+ for line in content_lines_br:
+ clean = line.strip().lstrip("- ").lstrip("• ")
+ if clean:
+ clean = bold(clean, rn)
+ bullets += f'• {clean}
\n'
# X'-6: 본문 표 요약이 있으면 하단 우측에 추가
table_summaries = enh.get("table_summaries", {})
@@ -918,7 +915,7 @@ def _assemble_type_b(run: Path, ctx: dict):
f''
f'{popup_html_br}'
- f'
{topic_title}
'
+ f'
{bold(sub_title_br, rn)}
'
f'
{bullets}
'
f'{table_html_br}
'
)
@@ -951,11 +948,15 @@ body{{background:#e5e5e5;padding:10px;font-family:'Pretendard Variable','Noto Sa
{top_html}
-
+
+
{bold(bottom_title, "")}
+
{footer_html}
diff --git a/src/mdx_normalizer.py b/src/mdx_normalizer.py
index 6556276..3236761 100644
--- a/src/mdx_normalizer.py
+++ b/src/mdx_normalizer.py
@@ -229,15 +229,18 @@ def _extract_structure(text: str) -> dict[str, Any]:
current_section_title = ""
current_section_lines = []
+ current_section_level = 2
+
def _flush_section():
- nonlocal current_section_title, current_section_lines
+ nonlocal current_section_title, current_section_lines, current_section_level
if current_section_title:
sections.append({
- "level": 2,
+ "level": current_section_level,
"title": current_section_title,
"content": "\n".join(current_section_lines).strip(),
})
current_section_lines = []
+ current_section_level = 2
for i, token in enumerate(tokens):
# 이미지 추출 (inline children)
@@ -283,12 +286,13 @@ def _extract_structure(text: str) -> dict[str, Any]:
if table["headers"] or table["rows"]:
tables.append(table)
- # 섹션 추출 (## 기준)
- if token.type == "heading_open" and token.tag == "h2":
+ # 섹션 추출 (## 및 ### 기준 — 대목차/소목차 모두)
+ if token.type == "heading_open" and token.tag in ("h2", "h3"):
_flush_section()
# 다음 토큰이 inline (제목 텍스트)
if i + 1 < len(tokens) and tokens[i + 1].type == "inline":
current_section_title = tokens[i + 1].content
+ current_section_level = 2 if token.tag == "h2" else 3
elif current_section_title and token.type in ("paragraph_open", "bullet_list_open",
"ordered_list_open", "fence"):
# 섹션 내용 수집 — inline 토큰의 content만