- 프로그램명 : XLS2PDF.py
- 제작기간 : '25. 7. 14.(1일)
- 제작자 : REDUCTO
- 사용언어 : Python 3.13

- 사용라이브러리 : 하단 requirements.txt 참조

- 버전 : v1.0


소개

기술사공부를 하다가 평소 gemini AI와의 대화(Live)로 연습을 하는데 얘한테 input source로 제가 공부하고 있는 파일을 주고 싶어서 만들어 보았습니다. 사실 지난번에 JSON으로 빼는걸 만들었는데, 계층이 너무 깊어지기도 하고 PDF로 빼면 이번에 출시된 NoteBook LLM에 이용할 수 있지 않을까 해서 구채여 만들어 보았습니다 

* 무단배포는 금지합니다.(댓글달아주세용)
* 기능에 커스터마이징이 필요하시다면 댓글달아주세용


사용법

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import sys
import threading
import queue
import time

from tkinterdnd2 import TkinterDnD, DND_FILES
from ttkthemes import ThemedStyle

def convert_by_text_extraction(xlsx_path, pdf_path, sheet_to_convert, progress_queue):
    try:
        from openpyxl import load_workbook
        from reportlab.pdfgen import canvas
        from reportlab.lib.pagesizes import letter
        from reportlab.pdfbase import pdfmetrics
        from reportlab.pdfbase.ttfonts import TTFont
    except ImportError:
        progress_queue.put(('error', '텍스트 변환에 필요한 라이브러리(openpyxl, reportlab)가 없습니다.'))
        return

    progress_queue.put(('status', '폰트 설정 및 파일 분석 중...'))
    FONT_PATH = "c:/Windows/Fonts/malgun.ttf"
    FONT_NAME = "MalgunGothic"
    if os.path.exists(FONT_PATH):
        pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
    else:
        progress_queue.put(('error', f'한글 폰트를 찾을 수 없습니다:\n{FONT_PATH}'))
        return

    try:
        workbook = load_workbook(xlsx_path, read_only=True)
        c = canvas.Canvas(pdf_path, pagesize=letter)
        width, height = letter
        
        sheets_to_process = []
        if sheet_to_convert == "모든 시트":
            sheets_to_process = workbook.sheetnames
        elif sheet_to_convert in workbook.sheetnames:
            sheets_to_process.append(sheet_to_convert)
        else:
            progress_queue.put(('error', f"'{sheet_to_convert}' 시트를 찾을 수 없습니다."))
            return

        total_sheets = len(sheets_to_process)
        for i, sheet_name in enumerate(sheets_to_process):
            progress_queue.put(('status', f"'{sheet_name}' 시트 변환 중 ({i+1}/{total_sheets})..."))
            progress_queue.put(('progress', int((i / total_sheets) * 100)))
            
            sheet = workbook[sheet_name]
            if i > 0: c.showPage()
            
            y_pos = height - 50
            c.setFont(FONT_NAME, 14)
            c.drawString(50, y_pos, f"--- {sheet.title} ---")
            y_pos -= 30
            c.setFont(FONT_NAME, 9)

            for row in sheet.iter_rows():
                if y_pos < 50:
                    c.showPage()
                    c.setFont(FONT_NAME, 9)
                    y_pos = height - 50
                line = '    '.join(str(cell.value if cell.value is not None else '') for cell in row)
                c.drawString(50, y_pos, line)
                y_pos -= 14
        
        c.save()
        progress_queue.put(('done', f"성공적으로 변환했습니다:\n{pdf_path}"))

    except Exception as e:
        progress_queue.put(('error', f"텍스트 변환 중 오류 발생:\n{e}"))

def convert_by_excel_export(xlsx_path, pdf_path, sheet_to_convert, progress_queue):
    try:
        import win32com.client
    except ImportError:
        progress_queue.put(('error', "'엑셀로 인쇄' 방식을 사용하려면 pywin32 라이브러리가 필요합니다."))
        return
        
    excel = None
    workbook = None
    try:
        progress_queue.put(('status', 'Excel 프로그램 실행 중...'))
        progress_queue.put(('progress', 10))
        excel = win32com.client.Dispatch("Excel.Application")
        excel.Visible = False
        excel.DisplayAlerts = False

        progress_queue.put(('status', f"'{os.path.basename(xlsx_path)}' 파일 여는 중..."))
        progress_queue.put(('progress', 30))
        workbook = excel.Workbooks.Open(xlsx_path)
        
        progress_queue.put(('status', 'PDF로 내보내는 중...'))
        progress_queue.put(('progress', 70))

        if sheet_to_convert == "모든 시트":
            workbook.ExportAsFixedFormat(0, pdf_path, 0, True, False)
        elif sheet_to_convert in [s.Name for s in workbook.Sheets]:
            ws = workbook.Worksheets[sheet_to_convert]
            ws.ExportAsFixedFormat(0, pdf_path, 0, True, False)
        else:
            progress_queue.put(('error', f"'{sheet_to_convert}' 시트를 찾을 수 없습니다."))
            return

        progress_queue.put(('done', f"성공적으로 변환했습니다:\n{pdf_path}"))

    except Exception as e:
        progress_queue.put(('error', f"엑셀 변환 중 오류 발생:\n{e}\n\nExcel이 설치되어 있는지 확인하세요."))
    finally:
        if workbook:
            workbook.Close(SaveChanges=False)
        if excel:
            excel.Quit()

class App(ttk.Frame):
    def __init__(self, master):
        super().__init__(master, padding="10")
        self.master = master
        self.progress_queue = queue.Queue()
        self.create_widgets()
        self.master.after(100, self.process_queue)

    def create_widgets(self):
        drop_zone = ttk.Label(self, text="이곳에 XLSX 파일을 드래그하세요", relief="solid", padding="20", anchor=tk.CENTER)
        drop_zone.pack(fill=tk.X, pady=(0, 10))
        drop_zone.drop_target_register(DND_FILES)
        drop_zone.dnd_bind('<<Drop>>', self.handle_drop)

        file_frame = ttk.Frame(self)
        file_frame.pack(fill=tk.X, expand=True, pady=5)
        ttk.Label(file_frame, text="엑셀 파일:").pack(side=tk.LEFT, padx=(0, 5))
        self.entry_file_path = ttk.Entry(file_frame)
        self.entry_file_path.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 10))
        self.btn_select_file = ttk.Button(file_frame, text="파일 찾기", command=self.select_file)
        self.btn_select_file.pack(side=tk.LEFT)
        
        options_frame = ttk.Frame(self)
        options_frame.pack(fill=tk.X, expand=True, pady=5)
        ttk.Label(options_frame, text="시트 선택:").pack(side=tk.LEFT, padx=(0, 10))
        self.sheet_combobox = ttk.Combobox(options_frame, state="readonly")
        self.sheet_combobox.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 20))
        
        self.conversion_method = tk.StringVar(value="excel")
        ttk.Radiobutton(options_frame, text="엑셀로 인쇄", variable=self.conversion_method, value="excel").pack(side=tk.LEFT)
        ttk.Radiobutton(options_frame, text="텍스트 추출", variable=self.conversion_method, value="text").pack(side=tk.LEFT, padx=(5,0))
        
        self.btn_convert = ttk.Button(self, text="PDF로 변환", command=self.start_conversion, style='Accent.TButton')
        self.btn_convert.pack(fill=tk.X, ipady=5, pady=10)
        
        status_frame = ttk.Frame(self, padding="10 0 0 0")
        status_frame.pack(fill=tk.X, expand=True)
        self.status_label = ttk.Label(status_frame, text="준비 완료")
        self.status_label.pack(fill=tk.X)
        self.progress_bar = ttk.Progressbar(status_frame, mode='determinate')
        self.progress_bar.pack(fill=tk.X, pady=5)

    def handle_drop(self, event):
        file_path = self.master.tk.splitlist(event.data)[0]
        if file_path.lower().endswith('.xlsx'):
            self.entry_file_path.delete(0, tk.END)
            self.entry_file_path.insert(0, os.path.abspath(file_path))
            self.update_sheet_list()
        else:
            messagebox.showwarning("파일 오류", "XLSX 파일만 드롭할 수 있습니다.")

    def select_file(self):
        file_path = filedialog.askopenfilename(title="변환할 XLSX 파일 선택", filetypes=[("XLSX files", "*.xlsx")])
        if file_path:
            self.entry_file_path.delete(0, tk.END)
            self.entry_file_path.insert(0, os.path.abspath(file_path))
            self.update_sheet_list()

    def update_sheet_list(self):
        from openpyxl import load_workbook
        xlsx_path = self.entry_file_path.get()
        if os.path.exists(xlsx_path):
            try:
                workbook = load_workbook(xlsx_path, read_only=True)
                sheet_names = workbook.sheetnames
                self.sheet_combobox['values'] = ["모든 시트"] + sheet_names
                self.sheet_combobox.current(0)
                workbook.close()
            except Exception as e:
                self.sheet_combobox['values'] = []
                messagebox.showerror("파일 오류", f"엑셀 파일을 분석할 수 없습니다:\n{e}")
        else:
            self.sheet_combobox['values'] = []
    
    def start_conversion(self):
        xlsx_path = self.entry_file_path.get()
        if not xlsx_path:
            messagebox.showwarning("입력 오류", "먼저 XLSX 파일을 선택해주세요.")
            return
        selected_sheet = self.sheet_combobox.get()
        if not selected_sheet:
            messagebox.showwarning("입력 오류", "변환할 시트를 선택해주세요.")
            return

        self.btn_convert.config(state=tk.DISABLED)
        self.progress_bar['value'] = 0
        
        base_name = os.path.splitext(os.path.basename(xlsx_path))[0]
        dir_name = os.path.dirname(xlsx_path)
        sheet_name_for_file = "전체" if selected_sheet == "모든 시트" else selected_sheet
        pdf_path = os.path.join(dir_name, f"{base_name}_{sheet_name_for_file}.pdf")

        method = self.conversion_method.get()
        target_func = convert_by_excel_export if method == "excel" else convert_by_text_extraction
        
        thread = threading.Thread(
            target=target_func,
            args=(xlsx_path, pdf_path, selected_sheet, self.progress_queue)
        )
        thread.daemon = True
        thread.start()

    def process_queue(self):
        try:
            message = self.progress_queue.get_nowait()
            msg_type, msg_data = message
            if msg_type == 'status':
                self.status_label.config(text=msg_data)
            elif msg_type == 'progress':
                self.progress_bar['value'] = msg_data
            elif msg_type == 'done':
                self.status_label.config(text="변환 완료!")
                self.progress_bar['value'] = 100
                messagebox.showinfo("성공", msg_data)
                self.btn_convert.config(state=tk.NORMAL)
            elif msg_type == 'error':
                self.status_label.config(text="오류 발생")
                self.progress_bar['value'] = 0
                messagebox.showerror("오류", msg_data)
                self.btn_convert.config(state=tk.NORMAL)
        except queue.Empty:
            pass
        finally:
            self.master.after(100, self.process_queue)

if __name__ == "__main__":
    root = TkinterDnD.Tk()
    root.title("XLSX to PDF 변환기")
    root.geometry("600x400")

    style = ThemedStyle(root)
    style.set_theme("arc")

    style.configure('Accent.TButton', font=('Helvetica', 10, 'bold'))

    app = App(root)
    app.pack(fill="both", expand=True)

    root.mainloop()

 

필요 라이브러리

requirement.txt
0.00MB


* 사전준비

 - python이 설치되어있어야합니다. 코드를 실행하기 위해서 해당 requirement.txt를 pip install 해주세요

# PIP 라이브러리 설치하기
pip install -r requirement.txt

# XLS2PDF 실행하식
python .\XlS2PDF.py

 - 필자의 환경은 python 3.13.0입니다. 가급적 맞추어주시거나 상위버전을 사용해 주시는게 좋습니다.

 

단순하게 생겼죠? Drag N Drop을 지원하고 시트를 선택해서 엑셀로 출력할 수 있습니다. 텍스트만 뽑을 수도 있습니다. 주의사항으로 확장자가 .xlsx만 받아들여지니 참고해주세용


* 동작이 converter.log에 기록됩니다 참고해주세요

- 프로그램명 :  ExamPlanner.exe
- 제작기간 : '25. 6. 20 ~ 22.(3일)
- 제작자 : REDUCTO
- 사용언어 : JAVA
- 사용라이브러리 : JavaFX, Eclipse

* Gemini를 사용하여 만들어진 Program입니다.

* Eclipse Maven 프로젝트로 Runnable JAR를 뽑아내니 더럽게 무거운(FAT Jar)로 뽑아야해서 실행프로그램은 첨부하지 않습니다. 실행프로그램 필요 시 댓글로 문의 주십쇼!

- 버전 : v1.0


소개

시험을 여러가지 보고 자격증을 취득하는게 취미입니다. 그런관점에서 시험일정을 관리하고 공부하는 커스텀 앱이 필요하지 않을까 생각이 들어서 만들게 되었습니다.

 

* 무단배포는 금지합니다.(댓글달아주세용)

* 기능에 커스터마이징이 필요하시다면 댓글달아주세용


사용법

프로그램은 한 개의 화면으로 구성되어있습니다.

 

새로운 실험을 좌측하단에 버튼을 통해서 추가할 수 있습니다

해당 시험을 우클릭 해서 혹은 달력을 통해 공부기간 / 시험일정을 입력할 수 있습니다. 입력된 일정은 핟나 다가오는일정 혹은 달력에서 확인할 수 있습니다.

mypersonnelpalette.zip
0.04MB

 

- 프로그램명 :  mypersonnelpalette.exe
- 제작기간 : '25. 6. 15.(1일)
- 제작자 : REDUCTO
- 사용언어 : JAVA
- 사용라이브러리 : Swing

* Gemini를 사용하여 만들어진 Program입니다.

- 버전 : v1.0


소개

디자인 작업을 가끔 할 때가 있습니다. PPT나 한글작업 등에도 아니어도 여러가지로 팔레트 앱을 사용할 일이 많아 개인적으로 필요한 어플리케이션이어서 만들어보앗브니다. 그냥 팔레트만 추출하는 어플리케이션은 심심하니까 명도와 채도를 조금씩 바꾸어서 추천하는 색상조합과 유사색을 출력하는 기능도 추가해보았습니다.

 

* 무단배포는 금지합니다.(댓글달아주세용)

* 기능에 커스터마이징이 필요하시다면 댓글달아주세용
* JRE 1.8.0 이상을 요구합니다.


사용법

프로그램은 한 개의 화면으로 구성되어있습니다.

스포이드로 색상 선택하면 화면에 있는 픽셀을 선택할 수 있습니다. 하단에 추천색상 5가지는 클릭해서 컬러코드를 복사할 수 있습니다.

PDFMerger.zip
4.06MB

 

- 프로그램명 : PDFMerger.exe
- 제작기간 : '25. 6. 1.(1일)
- 제작자 : REDUCTO
- 사용언어 : JAVA
- 사용라이브러리 : Swing, PDFBox

* Gemini를 사용하여 만들어진 Program입니다.

 

- 버전 : v1.0


소개

사진을 이용해서 PDF를 여러개로 나누고 다시 합치는 일이 있었는데, 온라인 툴에 광고가 많아서 직접 만들어쓰려고 만들어 보았습니다.

 

* 무단배포는 금지합니다.(댓글달아주세용)

* 기능에 커스터마이징이 필요하시다면 댓글달아주세용
* JRE 1.8.0 이상을 요구합니다.


사용법

프로그램은 한 개의 화면으로 구성되어있습니다.

사용자는 여러개의 PDF를 드래그 앤 드롭으로 옮길 수 있습니다.

개별 PDF의 출력할 Page를 지정할 수 있고 도 순서를 변경할 수 있습니다. 하단에 PDF 병합 버튼을 이용해서 최종적인 합성도 수행할 수 있습니다.

 

MyBookshelf.zip
2.06MB

 

- 프로그램명 :  MyBookshelf.exe
- 제작기간 : '25. 6. 1.(1일)
- 제작자 : REDUCTO
- 사용언어 : JAVA
- 사용라이브러리 : Swing

* Gemini를 사용하여 만들어진 Program입니다.

- 버전 : v1.0


소개

저는 개인적으로 독서가 취미입니다. 며칠 간 출장지에 가있으면서 많은양의 도서를 꽤나 했는데, 그 목록을 정리하고 싶어서 개인적으로 만들어 본 프로그램입니다. 기존에 했던 프로젝트와 크게 다르지 않기 때문에,  많이 배운건 없고 Java 실력 유지보수용으로 만든 기분이네요. 

 

* 무단배포는 금지합니다.(댓글달아주세용)

* 기능에 커스터마이징이 필요하시다면 댓글달아주세용
* JRE 1.8.0 이상을 요구합니다.


사용법

프로그램은 한 개의 화면으로 구성되어있습니다

3개의 영역으로 구성이 되어있는데요. 

직관적이어서 어렵진 않을겁니다. 하단에 [새 도서추가] 버튼을 통해서 새로운 도서를 넣을 수 있습니다. 추가된 도서의 표지는 c:\tmp에 저장됩니다. 데이터파일인 json도 그곳에 저장됩니다.

 

* 소스파일은 저자의 github에 Private Repository로 올려두었습니다. 댓글달아주시면 링크 보내드리겠습니다.

- 프로그램명 : TRIZSolver
- 제작기간 : '25. 5. 10.(1일)
- 제작자 : REDUCTO
- 사용언어 : Python, CustomTkinter

- 버전 : v1.0


소개

우리는 항상 아이디어에 굶주려 있습니다. 저또한 아이디어를 무엇을 만들어야하나 항상 고민을 하고 있었는데, 기술사공부를 하면서 봐왔던 아이디어 기법인 TRIZ와 SCAMPER기법을 섞은 데스크탑 어플리케이션이 있으면 좋겠다 생각했었습니다. 대 AI시대 gemini API를 이용해서 이를 고도화하면 좋은 어플리케이션이 나오지 않을까 한 단계 생각이 퍼져나갔고 한번 만들어 보았습니다.
 

* 무단배포는 금지합니다.(댓글달아주세용)
* 기능에 커스터마이징이 필요하시다면 댓글달아주세용


사용법
평소에는 소스코드를 올리지만 구조가 단일 코드 구조가 아니어서 별도의 사용방법을 올리겠습니다. 

 

* 사전준비

 - python이 설치되어있어야합니다.(https://www.python.org/downloads/)

 - 필자의 환경은 python 3.13.0입니다. 가급적 맞추어주시거나 상위버전을 사용해 주시는게 좋습니다.

 

1. 아래 파일을 다운로드 받아주시거나 git을 사용하시는 분은 "git clone https://github.com/ace30126/TRIZSolver.git"으로 project를 clone 해주십시오

TRIZSolver.zip
0.02MB

 

2. 압축해제(혹은 git clone)이후에는 TRIZSolver.zip경로에서 cmd를 켜고 아래의 명령어를 bash에서 입력해서 외부 라이브러리를 가져옵니다

  1. cmd 켜기(윈도우 버튼 -> cmd)
  2. 아까전에 경로를 복사하고 cd "[아까 그 경로]"
  3. 아래 명령어 입력
pip install -r requirements.txt

 

 

3. 설치가 완료되었으면 아래의 명령어로 코드의 실행이 가능합니다

python main.py

 

<main 실행화면>

예시로 SCAMPER를 눌러보면 아래와 같이 여러 기법을 눌러볼 수 있습니다

 

문제점/아이디어를 입력하고 각 기법을 누르면 다음과 같이 제안을 해줍니다

Gemini API키를 우측상단에 넣어두셨다면 Gemini 아이디어 버튼을 통해서 Gemini에게 해당 아이디어의 조언을 받을 수 있습니다.

+ Recent posts