디시인사이드 갤러리

마이너 갤러리 이슈박스, 최근방문 갤러리

갤러리 본문 영역

[free] 채팅방 입력용 html 파일

몬발켜갤로그로 이동합니다. 2026.04.02 21:07:47
조회 39 추천 0 댓글 2

인간은 대화를 통해서 정보를 전달합니다.

설명문으로 가득한 책보다 대화가 훨씬 더 쉽게 느껴집니다.

대화를 모방하여 질문-대답 공책을 만들려고 하다가,

카카오톡을 보고 약간 변경하게 되었습니다. 


저는 프로그래머가 아니라서, 바로 입력하는 방식을 선택하지 못하였습니다. 

1단계로 채팅을 입력하고, 

2단계로 입력된 채팅을 붙여넣어서 채팅하는 것처럼 만들고, 이를 캡처하는 방식으로 동작합니다. 


1단계의 채팅 입력은 수정과 삭제와 이동 기능이 구현되어야 하고, 

파일이 화자1과 화자2로 2열로 구별이 되어야 합니다. 

그래서 html의 표table 기능을 활용하면 되겠다 싶었습니다. 


이 코드는 인공지능 구글 제미나이를 사용해서 만들었습니다. 

이 코드는 퍼블릭 도메인입니다.

누구나 사용할 수 있고, 누구나 수정할 수 있고, 누구나 복사할 수 있고, 심지어는 실행 프로그램으로 만들어서 팔아먹어도 됩니다. ^ ^


<추가>

html 파일을 불러오는 기능을 추가했습니다. 4월4일 추가함.

<추가>

html 파일을 저장할 때 기본적으로 <다운로드 폴더>에 저장되도록 코드를 수정하였습니다.

<추가>

html 파일을 불러와서 특정 채팅을 선택하여 바로 수정할 수 있도록 코드를 수정하였습니다.

<추가>

html 파일을 불러와서 수정하다가 프로그램을 종료하면, 자동으로 저장되는 기능을 추가하였습니다.

열심히 수정해 놓고 자동으로 저장되지 않아서 수정 내용을 잃어 버리는 일이 없도록 만들었습니다. 



38ad8072b48276b660b8f68b12d21a1d64f550731d




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.[...])

추천 비추천

0

고정닉 0

0

원본 첨부파일 1

댓글 영역

전체 댓글 0
본문 보기

하단 갤러리 리스트 영역

왼쪽 컨텐츠 영역

갤러리 리스트 영역

갤러리 리스트
번호 말머리 제목 글쓴이 작성일 조회 추천
- 설문 잘못한 것보다 더 욕먹은 것 같은 스타는? 운영자 26/04/06 - -
- AD 게임에 진심인 당신을 위해~!! 운영자 26/03/05 - -
1 공지 프로그램 소스 코드의 공유/수정/판매 [2]
몬발켜갤로그로 이동합니다.
24.12.24 292 2
35 fre 채팅방 캡처 전체 채팅 저장 프로그램 [1]
몬발켜갤로그로 이동합니다.
04.02 34 0
34 fre 채팅방 캡처 프로그램 [1]
몬발켜갤로그로 이동합니다.
04.02 18 0
fre 채팅방 입력용 html 파일 [2]
몬발켜갤로그로 이동합니다.
04.02 39 0
32 fre png 그림 파일로 mp4 동영상 만들기
몬발켜갤로그로 이동합니다.
03.01 24 0
31 fre 중국웹소설 html viewer 프로그램 코드(2차 수정)
몬발켜갤로그로 이동합니다.
02.03 58 0
30 fre 웹소설 번역에 사용하는 프로그램 코드 [1]
몬발켜갤로그로 이동합니다.
01.30 21 0
29 fre 유튜브 댓글 일괄 저장 프로그램
몬발켜갤로그로 이동합니다.
01.07 45 0
28 일반 크롬 브라우저 확장 프로그램 만들기 경험담 [1]
몬발켜갤로그로 이동합니다.
25.11.11 50 0
27 fre 안 지워지는 파일 삭제하기
몬발켜갤로그로 이동합니다.
25.11.04 72 0
26 fre 동영상 파일 재생 후 접두어 추가하는 탐색기 프로그램 [1]
몬발켜갤로그로 이동합니다.
25.11.03 38 0
25 fre html 160KB 이하로 합치기 + 업그레이드1
몬발켜갤로그로 이동합니다.
25.10.29 20 0
24 fre 동영상 파일 이름 바꿔 분류하기 프로그램
몬발켜갤로그로 이동합니다.
25.10.27 18 0
23 fre html 파일 160KB 이하로 합치기 [1]
몬발켜갤로그로 이동합니다.
25.09.22 33 0
22 fre 동영상 파일들 이름 바꾸기
몬발켜갤로그로 이동합니다.
25.09.17 19 0
21 fre 여러 개의 html 파일을 하나의 html 파일로 합치는 코드 [1]
몬발켜갤로그로 이동합니다.
25.09.11 57 0
20 fre 빨간 공 파란 공 랜덤으로 그리는 프로그램
몬발켜갤로그로 이동합니다.
25.05.15 37 0
19 fre textmovie -자막만 있는 동영상 [2]
몬발켜갤로그로 이동합니다.
25.04.10 39 0
18 fre novel 9을 또 수정했습니다
몬발켜갤로그로 이동합니다.
25.02.27 30 0
17 fre html 파일 분할 (장 나누기)
몬발켜갤로그로 이동합니다.
25.02.21 36 0
16 fre 제미나이 복붙 novel 9을 또 수정했습니다
몬발켜갤로그로 이동합니다.
25.02.13 57 0
15 fre 제미나이 Flash 2.0을 위한 코드를 수정하였습니다 [4]
몬발켜갤로그로 이동합니다.
25.02.06 69 0
14 fre 1206, flash 1.5를 위한 코드를 수정했습니다
몬발켜갤로그로 이동합니다.
25.02.01 30 0
13 fre 누락된 파일명 찾아내기
몬발켜갤로그로 이동합니다.
25.01.31 21 0
12 fre 순서가 뒤죽박죽인 html 파일 이름 바꾸기 코드
몬발켜갤로그로 이동합니다.
25.01.31 36 0
11 fre 순서가 뒤죽박죽인 html 파일들 해결하기
몬발켜갤로그로 이동합니다.
25.01.30 38 0
10 fre 1206, Flash 1.5를 위한 코드를 또 수정했습니다
몬발켜갤로그로 이동합니다.
25.01.28 32 0
9 fre 1206 복붙 코드를 수정하였습니다
몬발켜갤로그로 이동합니다.
25.01.17 42 0
8 fre 1206을 위한 복붙 [2]
몬발켜갤로그로 이동합니다.
25.01.13 100 0
7 일반 이런갤도 있네
프갤러(223.38)
25.01.10 27 0
6 일반 북스캔 코드에 관한 설명을 추가합니다
몬발켜갤로그로 이동합니다.
25.01.09 72 0
5 fre 북스캔 book scan
몬발켜갤로그로 이동합니다.
25.01.07 323 1
4 fre 검색어로 폴더와 그 하위 폴더 안의 파일을 찾아주는 프로그램
몬발켜갤로그로 이동합니다.
24.12.24 138 0
3 fre 검색어로 폴더 안의 파일을 찾아주는 프로그램 [2]
몬발켜갤로그로 이동합니다.
24.12.24 206 0
2 fre 파일명이 (1)로 끝나는 파일만 찾아주는 프로그램
몬발켜갤로그로 이동합니다.
24.12.24 135 1
1
갤러리 내부 검색
제목+내용게시물 정렬 옵션

오른쪽 컨텐츠 영역

실시간 베스트

1/8

디시미디어

디시이슈

1/2