import sys
import os
import re
import html
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *
class HTMLEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("질문-대답 공책 HTML 에디터 (자동 저장 모드)")
self.resize(1200, 950)
self.current_speaker = 0
self.cut_buffer = []
self.current_file_path = None # 현재 열려 있는 파일 경로를 기억함
self.setup_ui()
self.update_speaker_buttons()
def setup_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.setSpacing(10)
# 1. 화자 이름 설정
name_group = QGroupBox("화자 이름 설정")
name_layout = QHBoxLayout()
self.name1_edit = QLineEdit("화자 1")
self.name2_edit = QLineEdit("화자 2")
self.name1_edit.textChanged.connect(self.update_speaker_buttons)
self.name2_edit.textChanged.connect(self.update_speaker_buttons)
name_layout.addWidget(QLabel("화자 1 (왼쪽):"))
name_layout.addWidget(self.name1_edit)
name_layout.addSpacing(30)
name_layout.addWidget(QLabel("화자 2 (오른쪽):"))
name_layout.addWidget(self.name2_edit)
name_group.setLayout(name_layout)
layout.addWidget(name_group)
# 2. 화자 선택 버튼
speaker_layout = QHBoxLayout()
self.btn_speaker1 = QPushButton()
self.btn_speaker1.setFixedHeight(50)
self.btn_speaker1.setFont(QFont("맑은 고딕", 14, QFont.Weight.Bold))
self.btn_speaker1.clicked.connect(lambda: self.set_speaker(0))
self.btn_speaker2 = QPushButton()
self.btn_speaker2.setFixedHeight(50)
self.btn_speaker2.setFont(QFont("맑은 고딕", 14, QFont.Weight.Bold))
self.btn_speaker2.clicked.connect(lambda: self.set_speaker(1))
speaker_layout.addWidget(self.btn_speaker1)
speaker_layout.addWidget(self.btn_speaker2)
layout.addLayout(speaker_layout)
# 3. 내용 입력창
self.text_input = QTextEdit()
self.text_input.setFont(QFont("맑은 고딕", 20))
self.text_input.setPlaceholderText("내용 입력 후 추가하거나, 아래 테이블에서 항목을 클릭하여 수정하세요.")
self.text_input.setMinimumHeight(150)
layout.addWidget(self.text_input)
# 4. 추가 및 수정 버튼
add_edit_layout = QHBoxLayout()
self.add_btn = QPushButton("새 채팅으로 추가 (Ctrl + Enter)")
self.add_btn.setFixedHeight(45)
self.add_btn.setStyleSheet("background-color: #f0f0f0; font-weight: bold;")
self.add_btn.clicked.connect(self.add_chat)
self.apply_edit_btn = QPushButton("선택한 행의 내용 수정 완료")
self.apply_edit_btn.setFixedHeight(45)
self.apply_edit_btn.setStyleSheet("background-color: #FFCC80; font-weight: bold;")
self.apply_edit_btn.clicked.connect(self.apply_edit_to_row)
add_edit_layout.addWidget(self.add_btn)
add_edit_layout.addWidget(self.apply_edit_btn)
layout.addLayout(add_edit_layout)
# 5. 편집 도구 모음
edit_tools_layout = QHBoxLayout()
self.btn_insert = QPushButton("위에 빈 줄 삽입")
self.btn_insert.clicked.connect(self.insert_row)
self.btn_delete = QPushButton("선택 항목 삭제")
self.btn_delete.clicked.connect(self.delete_selected_rows)
self.btn_cut = QPushButton("선택 항목 잘라내기")
self.btn_cut.clicked.connect(self.cut_selected_rows)
self.btn_paste_before = QPushButton("선택 항목 앞에 붙여넣기")
self.btn_paste_before.clicked.connect(self.paste_rows_before)
self.btn_paste_before.setEnabled(False)
for btn in [self.btn_insert, self.btn_delete, self.btn_cut, self.btn_paste_before]:
btn.setFixedHeight(35)
edit_tools_layout.addWidget(btn)
layout.addLayout(edit_tools_layout)
# 6. 테이블 표시부
self.table = QTableWidget(0, 3)
self.table.setHorizontalHeaderLabels(["번호", "화자 1", "화자 2"])
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.table.setColumnWidth(0, 60)
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.table.setFont(QFont("맑은 고딕", 12))
self.table.itemClicked.connect(self.load_row_to_editor)
layout.addWidget(self.table)
# 7. 하단 파일 제어 버튼
file_btn_layout = QHBoxLayout()
self.load_btn = QPushButton("HTML 파일 불러오기")
self.load_btn.setFixedHeight(55)
self.load_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;")
self.load_btn.clicked.connect(self.load_from_html)
self.save_btn = QPushButton("HTML 파일로 저장하기")
self.save_btn.setFixedHeight(55)
self.save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
self.save_btn.clicked.connect(self.save_to_html)
file_btn_layout.addWidget(self.load_btn)
file_btn_layout.addWidget(self.save_btn)
layout.addLayout(file_btn_layout)
QShortcut(QKeySequence("Ctrl+Return"), self, self.add_chat)
def set_speaker(self, idx):
self.current_speaker = idx
self.update_speaker_buttons()
self.text_input.setFocus()
def update_speaker_buttons(self):
n1, n2 = self.name1_edit.text(), self.name2_edit.text()
self.btn_speaker1.setText(f"{n1} (왼쪽)")
self.btn_speaker2.setText(f"{n2} (오른쪽)")
self.table.setHorizontalHeaderLabels(["번호", n1, n2])
s1 = "background: white; border: 4px solid #333;" if self.current_speaker == 0 else "background: #EEE;"
s2 = "background: #FEE500; border: 4px solid #333;" if self.current_speaker == 1 else "background: #FFF9C4;"
self.btn_speaker1.setStyleSheet(s1)
self.btn_speaker2.setStyleSheet(s2)
def renumber(self):
for r in range(self.table.rowCount()):
num_item = QTableWidgetItem(str(r + 1))
num_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
num_item.setFlags(num_item.flags() ^ Qt.ItemFlag.ItemIsEditable)
self.table.setItem(r, 0, num_item)
def add_chat(self):
text = self.text_input.toPlainText().strip()
if not text: return
row = self.table.rowCount()
self.table.insertRow(row)
self.set_row_data(row, self.current_speaker, text)
self.text_input.clear()
self.table.scrollToBottom()
self.renumber()
def set_row_data(self, row, speaker, text):
content_item = QTableWidgetItem(text)
if speaker == 0:
content_item.setBackground(QColor("white"))
self.table.setItem(row, 1, content_item)
self.table.setItem(row, 2, QTableWidgetItem(""))
else:
content_item.setBackground(QColor("#FEE500"))
self.table.setItem(row, 2, content_item)
self.table.setItem(row, 1, QTableWidgetItem(""))
def load_row_to_editor(self, item):
row = item.row()
s1_text = self.table.item(row, 1).text() if self.table.item(row, 1) else ""
s2_text = self.table.item(row, 2).text() if self.table.item(row, 2) else ""
if s1_text:
self.text_input.setPlainText(s1_text)
self.set_speaker(0)
elif s2_text:
self.text_input.setPlainText(s2_text)
self.set_speaker(1)
def apply_edit_to_row(self):
row = self.table.currentRow()
if row < 0: return
text = self.text_input.toPlainText().strip()
if not text: return
self.set_row_data(row, self.current_speaker, text)
# --- [핵심 기능: 종료 시 자동 저장] ---
def closeEvent(self, event):
"""프로그램 종료 시 호출되는 이벤트"""
if self.current_file_path and self.table.rowCount() > 0:
# 현재 작업 중인 파일이 있으면 대화상자 없이 바로 저장
self.perform_actual_save(self.current_file_path)
print(f"종료 전 자동 저장 완료: {self.current_file_path}")
event.accept()
def perform_actual_save(self, path):
"""실제 파일 쓰기 로직 (중복 제거를 위해 분리)"""
n1, n2 = self.name1_edit.text(), self.name2_edit.text()
html_content = f"""
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: '맑은 고딕', sans-serif; text-align: center; }}
.copy-btn {{ padding: 15px 30px; font-size: 20px; background: #FEE500; border: 2px solid #333; cursor: pointer; border-radius: 10px; }}
table {{ border-collapse: collapse; width: 100%; margin-top: 20px; table-layout: fixed; }}
th, td {{ border: 1px solid #ccc; padding: 12px; text-align: left; word-wrap: break-word; }}
th {{ background: #f2f2f2; }}
.q-cell {{ background: #ffffff; }}
.a-cell {{ background: #FEE500; }}
</style>
<script>
function copyTable() {{
const range = document.createRange();
range.selectNode(document.getElementById('data-table'));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand('copy');
alert('복사되었습니다!');
}}
</script>
</head>
<body>
<button class="copy-btn" onclick="copyTable()">텍스트 데이터 전체 복사하기</button>
<table id="data-table">
<thead><tr><th style="width:60px;">번호</th><th>{n1}</th><th>{n2}</th></tr></thead>
<tbody>
"""
for r in range(self.table.rowCount()):
num = self.table.item(r, 0).text()
s1 = self.table.item(r, 1).text() if self.table.item(r, 1) else ""
s2 = self.table.item(r, 2).text() if self.table.item(r, 2) else ""
if s1: html_content += f'<tr><td>{num}</td><td class="q-cell">{s1.replace(chr(10), "<br>")}</td><td></td></tr>'
elif s2: html_content += f'<tr><td>{num}</td><td></td><td class="a-cell">{s2.replace(chr(10), "<br>")}</td></tr>'
html_content += "</tbody></table></body></html>"
with open(path, "w", encoding="utf-8") as f:
f.write(html_content)
def save_to_html(self):
if self.table.rowCount() == 0: return
downloads_path = os.path.join(os.path.expanduser("~"), "Downloads")
path, _ = QFileDialog.getSaveFileName(self, "HTML 저장", downloads_path, "HTML Files (*.html)")
if not path: return
self.current_file_path = path # 저장한 경로를 현재 경로로 기억
self.perform_actual_save(path)
QMessageBox.information(self, "완료", "HTML 파일이 저장되었습니다.")
def load_from_html(self):
downloads_path = os.path.join(os.path.expanduser("~"), "Downloads")
path, _ = QFileDialog.getOpenFileName(self, "HTML 파일 불러오기", downloads_path, "HTML Files (*.html)")
if not path: return
self.current_file_path = path # 불러온 경로를 현재 경로로 기억
try:
with open(path, "r", encoding="utf-8") as f: content = f.read()
headers = re.findall(r'<th[^>]*>(.*?)</th>', content, re.IGNORECASE)
if len(headers) >= 3:
self.name1_edit.setText(headers[1].strip())
self.name2_edit.setText(headers[2].strip())
tbody_match = re.search(r'<tbody>(.*?)</tbody>', content, re.DOTALL | re.IGNORECASE)
if not tbody_match: return
rows_html = re.findall(r'<tr>(.*?)</tr>', tbody_match.group(1), re.DOTALL)
self.table.setRowCount(0)
for row_html in rows_html:
cells = re.findall(r'<td[^>]*>(.*?)</td>', row_html, re.DOTALL)
if len(cells) < 3: continue
s1_text = html.unescape(re.sub(r'<br\s*/?>', '\n', cells[1].strip(), flags=re.IGNORECASE))
s2_text = html.unescape(re.sub(r'<br\s*/?>', '\n', cells[2].strip(), flags=re.IGNORECASE))
row = self.table.rowCount()
self.table.insertRow(row)
if s1_text: self.set_row_data(row, 0, s1_text)
elif s2_text: self.set_row_data(row, 1, s2_text)
self.renumber()
self.update_speaker_buttons()
except Exception as e: QMessageBox.critical(self, "오류", f"로딩 실패: {e}")
# 기존 편집 관련 함수들 (insert_row, delete_selected_rows 등)은 동일함...
def insert_row(self):
curr = self.table.currentRow()
idx = curr if curr >= 0 else self.table.rowCount()
self.table.insertRow(idx)
self.table.setItem(idx, 1, QTableWidgetItem(""))
self.table.setItem(idx, 2, QTableWidgetItem(""))
self.renumber()
def delete_selected_rows(self):
selected = self.table.selectionModel().selectedRows()
if not selected: return
indices = sorted([index.row() for index in selected], reverse=True)
for i in indices: self.table.removeRow(i)
self.renumber()
def cut_selected_rows(self):
selected = self.table.selectionModel().selectedRows()
if not selected: return
indices = sorted([index.row() for index in selected])
self.cut_buffer = []
for i in indices:
row_data = {
's1': self.table.item(i, 1).text() if self.table.item(i, 1) else "",
's2': self.table.item(i, 2).text() if self.table.item(i, 2) else "",
'bg1': self.table.item(i, 1).background().color() if self.table.item(i, 1) else QColor("transparent"),
'bg2': self.table.item(i, 2).background().color() if self.table.item(i, 2) else QColor("transparent")
}
self.cut_buffer.append(row_data)
for i in sorted(indices, reverse=True): self.table.removeRow(i)
self.renumber()
self.btn_paste_before.setEnabled(True)
self.btn_paste_before.setText(f"{len(self.cut_buffer)}개 항목 붙여넣기")
def paste_rows_before(self):
if not self.cut_buffer: return
curr = self.table.currentRow()
idx = curr if curr >= 0 else self.table.rowCount()
for data in reversed(self.cut_buffer):
self.table.insertRow(idx)
item1 = QTableWidgetItem(data['s1'])
item1.setBackground(data['bg1'])
item2 = QTableWidgetItem(data['s2'])
item2.setBackground(data['bg2'])
self.table.setItem(idx, 1, item1)
self.table.setItem(idx, 2, item2)
self.renumber()
self.cut_buffer = []
self.btn_paste_before.setEnabled(False)
if __name__ == "__main__":
app = QApplication(sys.argv)
editor = HTMLEditor()
editor.show()
sys.exit(app.[...])
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.