누구나 그대로 따라 만들어서 빌드/배포까지 할 수 있도록 단계별로 정리했습니다.
1) 환경 준비
- Windows 10/11, macOS 최신 버전 권장
- Python 3.10.x 설치
- 가상환경 권장
Bash
# (선택) 가상환경
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS/Linux
source .venv/bin/activate2) 의존성 설치
Bash
pip install PyQt5 aspose-cells pyinstaller- PyQt5: GUI 프레임워크
- aspose-cells: Excel → Markdown 저장
- PyInstaller: 배포용 실행파일 생성
3) 프로젝트 구조 만들기
excel2md/
├── excel2md.py
└── img/
└── icon/
└── excel2md_transparent.ico
아이콘 파일은 반드시 img/icon/excel2md_transparent.ico에 두세요.
4) 전체 로직
- 각 시트를 개별 .md로 저장
- “Evaluation Only” 줄 제거
- 전체 통합 파일도 함께 생성
Python
from PyQt5.QtWidgets import (
QApplication, QWidget, QPushButton, QLabel, QFileDialog,
QVBoxLayout, QHBoxLayout, QMessageBox, QProgressBar, QFrame
)
from PyQt5.QtGui import QIcon, QFont, QPalette, QColor
from PyQt5.QtCore import Qt
from aspose.cells import Workbook
import os
import sys
import platform
def get_resource_path(relative_path):
"""PyInstaller 환경에서 리소스 경로 가져오기"""
if hasattr(sys, '_MEIPASS'):
# PyInstaller 임시 디렉토리
return os.path.join(sys._MEIPASS, relative_path)
else:
# 일반 실행 환경
return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
class ExcelToMarkdownApp(QWidget):
def __init__(self):
super().__init__()
self.setup_window()
self.file_path = ""
self.output_dir = ""
self.init_ui()
def setup_window(self):
"""윈도우 설정"""
self.setWindowTitle("Excel to Markdown Converter")
# PyInstaller 환경에서 아이콘 찾기
icon_path = get_resource_path(os.path.join("img", "icon", "excel2md_transparent.ico"))
if os.path.exists(icon_path):
try:
self.setWindowIcon(QIcon(icon_path))
except Exception as e:
print(f"아이콘 로드 실패: {e}")
else:
print(f"아이콘 파일을 찾을 수 없습니다: {icon_path}")
self.resize(500, 600)
self.setMinimumSize(450, 300)
def init_ui(self):
"""UI 초기화"""
# 메인 레이아웃
main_layout = QVBoxLayout()
main_layout.setSpacing(10)
main_layout.setContentsMargins(20, 20, 20, 20)
# 제목
title = QLabel("Excel to Markdown Converter")
title.setFont(QFont("맑은 고딕", 18, QFont.Bold))
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
# 서브타이틀
subtitle = QLabel("Excel 파일을 시트별/통합 Markdown 파일로 변환합니다")
subtitle.setFont(QFont("맑은 고딕", 11))
subtitle.setAlignment(Qt.AlignCenter)
subtitle.setStyleSheet("color: #7f8c8d; margin-bottom: 20px;")
# 파일 선택 영역
file_section = self.create_file_section()
# 출력 폴더 선택 영역
output_section = self.create_output_section()
# 진행 상태
progress_section = self.create_progress_section()
# 변환 버튼
convert_button = QPushButton("변환 시작")
convert_button.setFont(QFont("맑은 고딕", 12, QFont.Bold))
convert_button.setMinimumHeight(45)
convert_button.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 6px;
padding: 0px;
}
QPushButton:hover {
background-color: #2980b9;
}
QPushButton:pressed {
background-color: #21618c;
}
""")
convert_button.clicked.connect(self.run_convert)
# 상태 라벨
self.status_label = QLabel("")
self.status_label.setFont(QFont("맑은 고딕", 10))
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #7f8c8d;")
# 레이아웃에 추가
main_layout.addWidget(title)
main_layout.addWidget(subtitle)
main_layout.addWidget(file_section)
main_layout.addWidget(output_section)
main_layout.addWidget(progress_section)
main_layout.addWidget(convert_button)
main_layout.addWidget(self.status_label)
self.setLayout(main_layout)
def create_file_section(self):
"""파일 선택 섹션"""
frame = QFrame()
frame.setFrameStyle(QFrame.StyledPanel)
frame.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0px;
}
""")
layout = QVBoxLayout()
layout.setSpacing(2)
layout.setContentsMargins(15, 15, 15, 15)
# 제목
title = QLabel("📁 Excel 파일 선택")
title.setFont(QFont("맑은 고딕", 12, QFont.Bold))
title.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
# 파일 경로 표시
self.file_label = QLabel("파일을 선택하세요")
self.file_label.setFont(QFont("맑은 고딕", 10))
self.file_label.setStyleSheet("""
color: #6c757d;
background-color: white;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0px;
min-height: 25px;
""")
# 선택 버튼
select_button = QPushButton("파일 선택")
select_button.setFont(QFont("맑은 고딕", 10))
select_button.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
border-radius: 4px;
padding: 0px;
min-height: 30px;
}
QPushButton:hover {
background-color: #5a6268;
}
""")
select_button.clicked.connect(self.pick_file)
layout.addWidget(title)
layout.addWidget(self.file_label)
layout.addWidget(select_button)
frame.setLayout(layout)
return frame
def create_output_section(self):
"""출력 폴더 선택 섹션"""
frame = QFrame()
frame.setFrameStyle(QFrame.StyledPanel)
frame.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0px;
}
""")
layout = QVBoxLayout()
layout.setSpacing(2)
layout.setContentsMargins(15, 15, 15, 15)
# 제목
title = QLabel("📂 출력 폴더 선택")
title.setFont(QFont("맑은 고딕", 12, QFont.Bold))
title.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
# 폴더 경로 표시
self.output_label = QLabel("폴더를 선택하세요")
self.output_label.setFont(QFont("맑은 고딕", 10))
self.output_label.setStyleSheet("""
color: #6c757d;
background-color: white;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0px;
min-height: 25px;
""")
# 선택 버튼
select_button = QPushButton("폴더 선택")
select_button.setFont(QFont("맑은 고딕", 10))
select_button.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
border-radius: 4px;
padding: 0px;
min-height: 30px;
}
QPushButton:hover {
background-color: #5a6268;
}
""")
select_button.clicked.connect(self.pick_output)
layout.addWidget(title)
layout.addWidget(self.output_label)
layout.addWidget(select_button)
frame.setLayout(layout)
return frame
def create_progress_section(self):
"""진행 상태 섹션"""
frame = QFrame()
frame.setFrameStyle(QFrame.StyledPanel)
frame.setStyleSheet("""
QFrame {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0px;
}
""")
layout = QVBoxLayout()
layout.setSpacing(2)
layout.setContentsMargins(15, 15, 15, 15)
# 제목
title = QLabel("진행 상태")
title.setFont(QFont("맑은 고딕", 12, QFont.Bold))
title.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
# 프로그레스 바
self.progress = QProgressBar()
self.progress.setTextVisible(True)
self.progress.setStyleSheet("""
QProgressBar {
border: 1px solid #ced4da;
border-radius: 4px;
background-color: white;
height: 25px;
text-align: center;
font-weight: bold;
color: #2c3e50;
padding: 0px;
}
QProgressBar::chunk {
background-color: #3498db;
border-radius: 3px;
margin: 1px;
}
""")
layout.addWidget(title)
layout.addWidget(self.progress)
frame.setLayout(layout)
return frame
def pick_file(self):
"""파일 선택"""
fname, _ = QFileDialog.getOpenFileName(
self,
"Excel 파일 선택",
"",
"Excel Files (*.xlsx *.xls);;All Files (*)"
)
if fname:
self.file_path = fname
filename = os.path.basename(fname)
self.file_label.setText(f"선택됨: {filename}")
self.file_label.setFont(QFont("맑은 고딕", 10))
self.file_label.setStyleSheet("""
color: #28a745;
background-color: white;
border: 1px solid #28a745;
border-radius: 4px;
padding: 0px;
min-height: 25px;
""")
self.update_status("Excel 파일이 선택되었습니다")
def pick_output(self):
"""출력 폴더 선택"""
folder = QFileDialog.getExistingDirectory(self, "출력 폴더 선택")
if folder:
self.output_dir = folder
# 절대 경로 전체 표시
self.output_label.setText(f"선택됨: {folder}")
self.output_label.setFont(QFont("맑은 고딕", 10))
self.output_label.setStyleSheet("""
color: #28a745;
background-color: white;
border: 1px solid #28a745;
border-radius: 4px;
padding: 0px;
min-height: 25px;
""")
self.update_status("출력 폴더가 선택되었습니다")
def update_status(self, message):
"""상태 메시지 업데이트"""
self.status_label.setText(message)
self.status_label.setFont(QFont("맑은 고딕", 10))
self.status_label.setStyleSheet("color: #28a745; font-weight: bold;")
def update_progress(self, value, maximum):
"""진행률 업데이트"""
self.progress.setMaximum(maximum)
self.progress.setValue(value)
if value > 0:
percentage = int((value / maximum) * 100)
self.update_status(f"변환 중... {percentage}% 완료")
def run_convert(self):
"""변환 실행"""
if not self.file_path:
QMessageBox.warning(self, "경고", "Excel 파일을 선택해주세요.")
return
if not self.output_dir:
QMessageBox.warning(self, "경고", "출력 폴더를 선택해주세요.")
return
try:
self.update_status("변환을 시작합니다...")
self.progress.setValue(0)
workbook = Workbook(self.file_path)
sheet_count = len(workbook.worksheets)
if sheet_count == 0:
QMessageBox.warning(self, "경고", "Excel 파일에 시트가 없습니다.")
return
self.update_progress(0, sheet_count)
combined_content = []
for i in range(sheet_count):
sheet = workbook.worksheets[i]
# 안전한 파일명 생성
safe_name = sheet.name
for ch in ['<', '>', ':', '"', '/', '\\', '|', '?', '*', ' ']:
safe_name = safe_name.replace(ch, '_')
md_filename = os.path.join(self.output_dir, f"{i+1:02d}_{safe_name}.md")
# 새 워크북 생성하여 시트 복사
new_wb = Workbook()
new_sheet = new_wb.worksheets[0]
new_sheet.name = sheet.name
# 데이터 복사
max_row = sheet.cells.max_data_row
max_col = sheet.cells.max_data_column
for r in range(max_row + 1):
for c in range(max_col + 1):
val = sheet.cells.get(r, c).value
new_sheet.cells.get(r, c).put_value(val)
# 파일 저장
new_wb.save(md_filename)
# 파일 내용 읽기 및 정리
with open(md_filename, 'r', encoding='utf-8') as f:
content = f.read()
# Evaluation Only 라인 제거
lines = content.splitlines()
filtered_lines = []
for line in lines:
if "# Evaluation" in line or "Evaluation Only" in line:
break
filtered_lines.append(line)
content = "\n".join(filtered_lines).rstrip('\n')
# 정리된 내용으로 다시 저장
with open(md_filename, 'w', encoding='utf-8') as f:
f.write(content)
combined_content.append(content)
self.update_progress(i + 1, sheet_count)
QApplication.processEvents()
# 통합 파일 생성
base_name = os.path.splitext(os.path.basename(self.file_path))[0]
combined_filename = os.path.join(self.output_dir, f"{base_name}_ALL_COMBINED.md")
with open(combined_filename, 'w', encoding='utf-8') as f:
f.write("# Excel to Markdown 변환 결과\n\n")
f.write("## 목차\n\n")
for i in range(sheet_count):
sheet_name = workbook.worksheets[i].name
anchor_name = sheet_name.replace(' ', '-').lower()
f.write(f"{i+1}. [{sheet_name}](#{i+1:02d}-{anchor_name})\n")
f.write("\n---\n\n")
for i, content in enumerate(combined_content):
if content.strip():
sheet_name = workbook.worksheets[i].name
anchor_name = sheet_name.replace(' ', '-').lower()
f.write(f"<div id=\"{i+1:02d}-{anchor_name}\"></div>\n\n")
f.write(f"## {i+1:02d}. {sheet_name}\n\n")
f.write(content.replace(f"# {sheet_name}", ""))
f.write("\n\n---\n\n")
self.progress.setValue(0)
self.update_status("변환이 완료되었습니다! 🎉")
QMessageBox.information(
self,
"변환 완료",
f"모든 시트 변환이 완료되었습니다!\n\n"
f"📊 총 {sheet_count}개 시트\n"
f"📁 {sheet_count + 1}개 파일 생성\n"
f"📂 출력 위치: {self.output_dir}"
)
except Exception as e:
self.progress.setValue(0)
self.update_status("변환 중 오류가 발생했습니다")
QMessageBox.critical(
self,
"오류",
f"변환 중 오류가 발생했습니다:\n\n{str(e)}"
)
if __name__ == "__main__":
app = QApplication(sys.argv)
# Windows에서 한글 폰트 설정
if platform.system() == "Windows":
app.setFont(QFont("맑은 고딕", 9))
elif platform.system() == "Darwin": # macOS
app.setFont(QFont("Apple SD Gothic Neo", 10))
else: # Linux
app.setFont(QFont("Arial", 9))
window = ExcelToMarkdownApp()
window.show()
sys.exit(app.exec_())5) 빌드/배포
리소스(아이콘 폴더)를 실행 파일에 동봉해야 런타임에 접근 가능합니다.
Bash
# 리소스 포함 권장 (img 폴더)
pyinstaller --onefile --windowed --add-data "img;img" -i img\icon\excel2md_transparent.ico excel2md.py- 결과 파일: dist/excel2md.exe (Windows)
- 실행 중 임시 디렉토리 예: C:\Users\{사용자}\AppData\Local\Temp\_MEIxxxxx
6) 동작 확인 체크리스트
- 앱 타이틀/아이콘 정상 표시
- Windows: 글꼴이 “맑은 고딕” (굴림체 아님)
- 컴포넌트 패딩 0, 텍스트 잘 보임
- 출력 폴더 라벨에 절대 경로 그대로 표시
- 변환 후 개별 md + 통합 md 생성
7) 문제 해결 팁
- PyInstaller 경고:
- Hidden import “sip” 경고는 보통 무시 가능
- 필요 시 –hidden-import sip 추가
- -i …ico: 실행 파일 자체의 아이콘 (탐색기에서 보임)
- self.setWindowIcon(…): 앱 창/작업표시줄/Alt+Tab에 보이는 런타임 아이콘 둘 다 설정해야 일관됩니다.
- 요약
- 리소스 경로: get_resource_path() 필수
- 폰트: OS별 한글 폰트 지정
- UI: padding 0 + min-height + 레이아웃 마진
- 빌드: –add-data “img;img” 꼭 포함
Bash
pyinstaller --onefile --windowed --add-data "img;img" -i img\icon\excel2md_transparent.ico excel2md.py
# 또는 아래 (결과 용량 같음)
pyinstaller --onefile --windowed --add-data "img;img" --hidden-import sip -i img\icon\excel2md_transparent.ico excel2md.py부록: 실행 파일 아이콘과 창 아이콘 차이
소스파일
실행파일
