메뉴 여닫기
개인 메뉴 토글
로그인하지 않음
만약 지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

카렛 프로그래밍

데브카페
 

import os
import sqlite3
import threading
import time
import tkinter as tk
from tkinter import ttk
import keyboard
import pywinauto
from pywinauto.keyboard import send_keys

DB_FILE = "caret_data.db"

class CaretReaderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Fast Caret DB v1.3")
        self.root.geometry("360x160")
        self.root.attributes("-topmost", True)
        self.root.resizable(False, False)

        # 1. 성능 최적화: 메모리 캐시 딕셔너리 초기화
        self.memory_cache = {}

        # 2. 성능 최적화: 고성능 DB 초기화 및 커넥션 풀링 유지
        self.db_conn = None
        self.init_database()

        # UI 요소 배치
        style = ttk.Style()
        style.theme_use("clam")

        self.label_info = ttk.Label(root, text="[Ctrl + Shift + F12]를 누르면\n커서 위치 문자를 고속 조회합니다.", justify="center", font=("Malgun Gothic", 10))
        self.label_info.pack(pady=10)

        self.label_result = ttk.Label(root, text="읽은 문자: 없음", font=("Malgun Gothic", 11, "bold"), foreground="#555555")
        self.label_result.pack(pady=2)

        self.label_db_content = ttk.Label(root, text="DB 내용: 매칭 데이터 없음", font=("Malgun Gothic", 12, "bold"), foreground="#0078D7", wraplength=340, justify="center")
        self.label_db_content.pack(pady=8)

        # 단축키 리스너 등록 (데몬 스레드)
        self.hotkey_thread = threading.Thread(target=self.setup_hotkey, daemon=True)
        self.hotkey_thread.start()

    def init_database(self):
        """SQLite 고속 읽기 튜닝 및 인덱스 설정"""
        is_new = not os.path.exists(DB_FILE)
        
        # 상시 연결 유지 (매 요청마다 open/close를 하지 않음)
        self.db_conn = sqlite3.connect(DB_FILE, check_same_thread=False)
        cursor = self.db_conn.cursor()
        
        # 성능 향상을 위한 SQLite PRAGMA 튜닝 설정
        cursor.execute("PRAGMA journal_mode=WAL;")  # Write-Ahead Logging 모드로 읽기 성능 향상
        cursor.execute("PRAGMA synchronous=NORMAL;") # 디스크 동기화 오버헤드 감소
        cursor.execute("PRAGMA cache_size=-2000;")  # 2MB 캐시 지정
        
        # 테이블 생성
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS dict_data (
                character TEXT PRIMARY KEY,
                content TEXT
            )
        """)
        
        # 고속 조회를 위한 인덱스 생성
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_char ON dict_data(character);")
        
        if is_new:
            samples = [
                ('A', '알파벳의 첫 번째 글자 (Apple, Ace)'),
                ('B', '알파벳의 두 번째 글자 (Banana, Best)'),
                ('C', '알파벳의 세 번째 글자 (Cherry, Cool)'),
                ('한', '대한민국의 국호이자 한글의 첫 글자'),
                ('1', '숫자의 시작, 최초 혹은 최고를 의미'),
                ('?', '질문이나 의문을 나타내는 문장 부호')
            ]
            cursor.executemany("INSERT OR IGNORE INTO dict_data (character, content) VALUES (?, ?)", samples)
            self.db_conn.commit()
            
        print("[성공] SQLite 고속 데이터베이스 인프라 튜닝 및 캐시 활성화 완료.")

    def setup_hotkey(self):
        keyboard.add_hotkey("ctrl+shift+f12", self.trigger_caret_reading)
        keyboard.wait()

    def trigger_caret_reading(self):
        # 무거운 작업 분리
        threading.Thread(target=self.read_caret_pipeline, daemon=True).start()

    def read_caret_pipeline(self):
        # 1단계: 고성능 UI Automation 백엔드 시도
        character = self.try_uia_backend()
        
        # 2단계: 실패 시 고속 폴백 매크로 시도
        if not character:
            character = self.try_cursor_right_fast_fallback()

        if character and character.strip() != "":
            cleaned_char = character.strip()
            
            # 3단계: 고속 조회 패턴 (메모리 캐시 확인 -> DB 파일 확인)
            db_content = self.query_database_fast(cleaned_char)
            
            display_char = cleaned_char.replace("\n", "\\n").replace("\r", "\\r")
            self.root.after(0, lambda: self.update_ui(f"읽은 문자: '{display_char}'", db_content))
        else:
            self.root.after(0, lambda: self.update_ui("읽은 문자: 추출 실패", "매칭 데이터 없음"))

    def query_database_fast(self, char):
        """메모리 캐시 ➔ 상시 개방된 DB 인덱스 스캔을 통한 Ultra-fast 조회"""
        # 1단계: 가장 빠른 메모리 딕셔너리 캐시 탐색
        if char in self.memory_cache:
            return self.memory_cache[char]
            
        # 2단계: 캐시 미스 시, 상시 열려있는 고성능 DB 인덱스 조회
        try:
            cursor = self.db_conn.cursor()
            cursor.execute("SELECT content FROM dict_data WHERE character = ?", (char,))
            row = cursor.fetchone()
            
            if row:
                res = f"DB 내용: {row[0]}"
            else:
                res = "DB 내용: 등록되지 않은 문자입니다."
                
            # 결과를 다음 조회를 위해 메모리 캐시에 적재
            self.memory_cache[char] = res
            return res
        except Exception as e:
            return f"DB 조회 에러: {e}"

    def update_ui(self, result_text, db_text):
        self.label_result.config(text=result_text)
        self.label_db_content.config(text=db_text)
        if "등록되지 않은" in db_text or "없음" in db_text:
            self.label_db_content.config(foreground="#E81123")
        else:
            self.label_db_content.config(foreground="#0078D7")

    def try_uia_backend(self):
        try:
            app = pywinauto.Application(backend="uia").connect(active_only=True)
            focused_control = app.top_window().set_focus()
            text_pattern = focused_control.get_text_block()
            selection = text_pattern.get_selection()
            
            if selection:
                caret_range = selection
                caret_range.expand_to_enclosing_unit("Character")
                return caret_range.get_text()
        except Exception:
            pass
        return None

    def try_cursor_right_fast_fallback(self):
        """딜레이 오버헤드를 극한으로 줄인 고속 매크로"""
        try:
            import win32clipboard
            original_text = ""
            try:
                win32clipboard.OpenClipboard()
                if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_UNICODETEXT):
                    original_text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
                win32clipboard.CloseClipboard()
            except Exception:
                pass

            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()
            win32clipboard.CloseClipboard()

            # 성능 최적화: OS 타겟 윈도우 부하 타이밍 최소화 (기존 0.06초 -> 0.02초)
            send_keys("+{RIGHT}")
            time.sleep(0.02)
            send_keys("^c")
            time.sleep(0.02)

            target_char = None
            win32clipboard.OpenClipboard()
            try:
                if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_UNICODETEXT):
                    target_char = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
            except Exception:
                pass
            finally:
                win32clipboard.CloseClipboard()

            send_keys("{LEFT}")

            if original_text:
                try:
                    win32clipboard.OpenClipboard()
                    win32clipboard.EmptyClipboard()
                    win32clipboard.SetClipboardText(original_text, win32clipboard.CF_UNICODETEXT)
                    win32clipboard.CloseClipboard()
                except Exception:
                    pass

            return target_char
        except Exception:
            pass
        return None

    def __del__(self):
        # 프로그램 종료 시 안전하게 DB 세션 닫기
        if self.db_conn:
            self.db_conn.close()

if __name__ == "__main__":
    root = tk.Tk()
    app = CaretReaderApp(root)
    root.mainloop()