03번 B' 정상 동작: 가로 3단 카드 + overflow 해소 + 하단 균형

- block_assembler B': 카드 3개 이상 + 이미지 없음 → 가로(row) 배치
- block_assembler B': section title이 카드 제목, D1은 카드 내 bold 불릿
- block_assembler: overflow:auto → overflow:hidden, [핵심요약:] 마커 필터
- block_assembler: \x01 바이트 수정
- pipeline: Selenium 실측 기반 zone 간 재배분 (allocated-scrollHeight로 slack 계산)
- pipeline: surplus 최대 50%만 이전 (하단 최소 공간 보장)
- pipeline: bottom_left/bottom_right → Selenium bottom zone 매핑
- kei_client: 상단은 팝업 대상 제외, 하단에서만 팝업 분리

결과: 02번/03번 모두 overflow 없이 정상 출력

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 10:32:14 +09:00
parent bc7c08e575
commit b13df8b176
4 changed files with 79 additions and 1319 deletions

View File

@@ -590,6 +590,8 @@ def _assemble_slide_html_type_b(ctx: "PipelineContext", title_text: str = "") ->
continue
if re.search(r'\[이미지:', stripped) or re.match(r'^!\[', stripped):
continue
if re.search(r'\[핵심요약:', stripped):
continue
content_lines.append(stripped)
popup_html = _popup_links_html(popup_titles, font_size)
@@ -1012,11 +1014,17 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
continue
if re.search(r'\[이미지:', stripped) or re.match(r'^!\[', stripped):
continue
if re.search(r'\[핵심요약:', stripped):
continue
content_lines.append(stripped)
popup_html = _popup_links_html(popup_titles, font_size)
# 소제목(### 또는 D1:) + 불릿(D2:)을 카드형으로 분리
# B': ### (section title) = 카드 제목. D1/D2는 카드 내부 불릿.
# ###이 있으면 카드는 section 단위. D1은 카드 안의 bold 불릿.
# ###이 없으면 D1이 카드 제목 (02번 방식).
has_section_titles = any(line.startswith("### ") for line in content_lines)
sections = []
current_section = ("", [])
for line in content_lines:
@@ -1025,11 +1033,15 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
sections.append(current_section)
current_section = (line.lstrip("# ").strip(), [])
elif re.match(r'^D1:\s*', line):
# D1 = 1단 불릿 = 소제목 (카드 제목)
title_text = re.sub(r'^D1:\s*', '', line).lstrip("")
if current_section[0] or current_section[1]:
sections.append(current_section)
current_section = (_bold(title_text, rn), [])
if has_section_titles:
# ### 카드 안의 bold 불릿
current_section[1].append(f'<strong>{_bold(title_text, rn)}</strong>')
else:
# 02번 방식: D1이 카드 제목
if current_section[0] or current_section[1]:
sections.append(current_section)
current_section = (_bold(title_text, rn), [])
elif re.match(r'^D[2-9]:\s*', line):
# D2+ = 하위 불릿 = 본문
clean = re.sub(r'^D[2-9]:\s*', '', line).lstrip("")
@@ -1055,17 +1067,13 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
card_gap = max(3, int(font_size * 0.4))
indent_body = int(font_size * 1.2)
# B': 상단이 popup 대상이면 소제목만 유지, 하위 불릿 제거
top_is_popup = rn in popup_roles
# B': 상단은 핵심이므로 항상 불릿 표시 (팝업 대상 아님)
bullets = ""
if len(sections) > 1 and sections[0][0]:
for ci, (sec_title, sec_items) in enumerate(sections):
bg, text_color = _card_colors[ci % len(_card_colors)]
if top_is_popup:
items_html = ""
else:
items_html = "".join(
items_html = "".join(
f'<div style="padding-left:{indent_body}px;margin-bottom:1px;">'
f'<span style="color:{text_color};font-size:{font_size-1}px;line-height:1.5;">• {item}</span></div>'
for item in sec_items
@@ -1112,15 +1120,28 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
topic_title = _bold(topic_title_from_section or rn, rn)
top_html = (
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
f'display:flex;flex-direction:column;justify-content:space-between;">'
f'{popup_html}'
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>'
f'<div style="display:flex;gap:{max(6, int(font_size*0.8))}px;align-items:flex-start;flex:1;">'
f'<div style="flex:1;font-size:{font_size}px;line-height:1.55;color:#333;">{bullets}</div>'
f'{img_block}</div></div>'
)
# B': 카드 3개 이상 + 이미지 없음 → 가로 배치
card_count = len(sections) if len(sections) > 1 and sections[0][0] else 0
use_row = card_count >= 3 and not (has_image and img_html)
if use_row:
top_html = (
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
f'display:flex;flex-direction:column;">'
f'{popup_html}'
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>'
f'<div style="display:flex;flex-direction:row;gap:{card_gap}px;flex:1;">{bullets}</div></div>'
)
else:
top_html = (
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
f'display:flex;flex-direction:column;justify-content:space-between;">'
f'{popup_html}'
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>'
f'<div style="display:flex;gap:{max(6, int(font_size*0.8))}px;align-items:flex-start;flex:1;">'
f'<div style="flex:1;font-size:{font_size}px;line-height:1.55;color:#333;">{bullets}</div>'
f'{img_block}</div></div>'
)
# ── 하단: normalized.sections에서 직접 매핑 ──
bottom_title = ""
@@ -1200,6 +1221,8 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
clean_plain = re.sub(r'<[^>]+>', '', clean).strip()
if clean_plain in table_cell_texts or clean_plain == "":
continue
if re.search(r'\[핵심요약:', clean):
continue
if clean:
clean = _bold(clean, rn)
_pad = bl_indent * depth
@@ -1208,7 +1231,7 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
bul += f'<div style="padding-left:{_pad}px;font-size:{fs}px;margin-bottom:2px;{weight}">• {clean}</div>\n'
bl_html = (
f'<div style="height:100%;padding:{gap_small}px;box-sizing:border-box;overflow-y:auto;">'
f'<div style="height:100%;padding:{gap_small}px;box-sizing:border-box;overflow:hidden;">'
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{_bold(sub_title, rn)}</div>'
f'{table_html_bl}'
f'<div style="line-height:1.55;color:#333;">{bul}</div></div>'
@@ -1232,6 +1255,8 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
depth = int(dm.group(1))
stripped = re.sub(r'^D\d+:\s*', '', stripped)
clean = stripped.lstrip("- ").lstrip("")
if re.search(r'\[핵심요약:', clean):
continue
if clean:
clean = _bold(clean, rn)
_pad = bl_indent * depth