| {cell} | ') html_lines.append('
|---|
| {cell} | ') html_lines.append('
변환"""
# 빈 줄이면 무시
if not text.strip():
return ""
# **text** →
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
# *text* →
text = re.sub(r'\*(.+?)\*', r'\1', text)
# `code` → {text} {{cover_date}} {{cover_author}} {{cover_dept}}
text = re.sub(r'`(.+?)`', r'\1', text)
return f'{title}
')
i += 1
continue
# H2 (## 대목차)
h2_match = re.match(r'^##\s+(.+)$', line)
if h2_match:
title = h2_match.group(1)
num_match = re.match(r'^(\d+)\s+', title)
if num_match:
current_chapter = num_match.group(1)
html_parts.append(f'{title}
') # H1으로 변환 (페이지 분리 트리거)
i += 1
continue
# H3 (### 중목차)
h3_match = re.match(r'^###\s+(.+)$', line)
if h3_match:
html_parts.append(f'{h3_match.group(1)}
')
i += 1
continue
# H4 (#### 소목차/꼭지)
h4_match = re.match(r'^####\s+(.+)$', line)
if h4_match:
html_parts.append(f'{h4_match.group(1)}
')
i += 1
continue
# 이미지 플레이스홀더 {{IMG:xxx}}
img_match = re.match(r'^\{\{IMG:(.+?)\}\}$', line)
if img_match:
html_parts.append(self.convert_image_placeholder(line, current_chapter))
i += 1
continue
# 이미지 캡션 *(참고: ...)* - figure 바로 뒤에 나오면 무시 (이미 figcaption으로 처리)
if line.startswith('*(') and line.endswith(')*'):
i += 1
continue
# 테이블 감지 (| 로 시작)
if line.startswith('|') or (line.startswith('**[표') and i + 1 < len(lines)):
# 표 제목 캡션
caption = ""
if line.startswith('**[표'):
caption_match = re.match(r'^\*\*(\[표.+?\].*?)\*\*$', line)
if caption_match:
caption = caption_match.group(1)
i += 1
if i >= len(lines):
break
line = lines[i].strip()
# 테이블 본문 수집
table_lines = []
while i < len(lines) and (lines[i].strip().startswith('|') or
re.match(r'^[\|\s\-:]+$', lines[i].strip())):
table_lines.append(table_lines) # Fixed from list.append(table_lines) to line in thinking
table_lines.append(lines[i])
i += 1
if table_lines:
table_md = '\n'.join(table_lines)
html_parts.append(self.convert_table(table_md, caption, current_chapter))
continue
# 리스트 감지 (* 또는 - 또는 1. 로 시작)
if re.match(r'^[\*\-]\s+', line) or re.match(r'^\d+\.\s+', line):
list_lines = [line]
i += 1
while i < len(lines):
next_line = lines[i].strip()
if re.match(r'^[\*\-]\s+', next_line) or re.match(r'^\d+\.\s+', next_line):
list_lines.append(next_line)
i += 1
elif not next_line:
i += 1
break
else:
break
html_parts.append(self.convert_list('\n'.join(list_lines)))
continue
# 일반 문단
para_lines = [line]
i += 1
while i < len(lines):
next_line = lines[i].strip()
# 다음이 특수 요소면 문단 종료
if (not next_line or
next_line.startswith('#') or
next_line.startswith('|') or
next_line.startswith('**[표') or
next_line.startswith('{{IMG:') or
next_line.startswith('*(') or
re.match(r'^[\*\-]\s+', next_line) or
re.match(r'^\d+\.\s+', next_line)):
break
para_lines.append(next_line)
i += 1
para_text = ' '.join(para_lines)
if para_text:
html_parts.append(self.convert_paragraph(para_text))
return '\n'.join(html_parts)
# ===== HTML 템플릿 =====
def get_html_template() -> str:
"""A4 보고서 HTML 템플릿 반환"""
return '''
{{main_title}}
{{sub_title}}
요약
\\n{{summary_converter.convert_full_content(sec.generated_text)}}"
break
# box-content (본문)
box_content = content_html
# 6. 템플릿에 주입
template = get_html_template()
html_output = template.format(
report_title=report_title,
box_cover=box_cover,
box_toc=box_toc,
box_summary=box_summary,
box_content=box_content
)
# 7. 파일 저장
output_path.write_text(html_output, encoding='utf-8')
log(f"")
log(f"═══════════════════════════════════════════════════")
log(f"HTML 보고서 생성 완료!")
log(f" 출력 파일: {{output_path}}")
log(f" 파일 크기: {{output_path.stat().st_size / 1024:.1f}} KB")
log(f"═══════════════════════════════════════════════════")
log("=== Step 9 종료 ===")
return output_path
def main():
"""CLI 진입점"""
parser = argparse.ArgumentParser(
description='MD + JSON → A4 HTML 보고서 변환',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
예시:
python 9_md_to_html_publisher.py
python 9_md_to_html_publisher.py --md report_draft.md --json report_sections.json
python 9_md_to_html_publisher.py --no-toc --no-summary
python 9_md_to_html_publisher.py --cover-date "2026.01.15" --cover-author "홍길동"
'''
)
parser.add_argument('--md', type=Path, default=DEFAULT_MD_PATH,
help='입력 마크다운 파일 경로')
parser.add_argument('--json', type=Path, default=DEFAULT_JSON_PATH,
help='입력 JSON 파일 경로')
parser.add_argument('--output', '-o', type=Path, default=DEFAULT_OUTPUT_PATH,
help='출력 HTML 파일 경로')
parser.add_argument('--no-toc', action='store_true',
help='목차 페이지 제외')
parser.add_argument('--no-summary', action='store_true',
help='요약 페이지 제외')
parser.add_argument('--cover-date', type=str, default=None,
help='표지 날짜 (예: 2026.01.15)')
parser.add_argument('--cover-author', type=str, default=None,
help='표지 작성자')
parser.add_argument('--cover-dept', type=str, default=None,
help='표지 부서명')
args = parser.parse_args()
# 표지 정보 구성
cover_info = {}
if args.cover_date:
cover_info['date'] = args.cover_date
if args.cover_author:
cover_info['author'] = args.cover_author
if args.cover_dept:
cover_info['department'] = args.cover_dept
# 변환 실행
generate_report_html(
md_path=args.md,
json_path=args.json,
output_path=args.output,
include_toc=not args.no_toc,
include_summary=not args.no_summary,
cover_info=cover_info if cover_info else None
)
if __name__ == "__main__":
main()