import sys import requests import json from datetime import datetime from pathlib import Path from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QLabel, QListWidget, QListWidgetItem, QMessageBox, QGroupBox, QFormLayout, QDialog, QTextBrowser, QScrollArea, QComboBox, QFrame ) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl, QSettings from PyQt5.QtGui import QFont, QDesktopServices
class ApiGuideDialog(QDialog): """API 키 생성 가이드 다이얼로그""" def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('Google API 키 생성 가이드') self.setMinimumSize(650, 550) self.init_ui()
def init_ui(self): layout = QVBoxLayout() layout.setContentsMargins(20, 20, 20, 20)
# 가이드 내용 guide_text = QTextBrowser() guide_text.setOpenExternalLinks(True) guide_text.setFont(QFont('맑은 고딕', 10)) guide_text.setHtml(''' <h2 style="color: #4285f4;">Google Custom Search API 설정 가이드</h2>
<h3>1단계: Google Cloud 프로젝트 생성</h3> <ol> <li><a href="https://console.cloud.google.com/">Google Cloud Console</a> 접속</li> <li>Google 계정으로 로그인</li> <li>상단의 프로젝트 선택 → <b>"새 프로젝트"</b> 클릭</li> <li>프로젝트 이름 입력 후 <b>"만들기"</b> 클릭</li> </ol>
<h3>2단계: Custom Search API 활성화</h3> <ol> <li>좌측 메뉴에서 <b>"API 및 서비스"</b> → <b>"라이브러리"</b> 클릭</li> <li>검색창에 <b>"Custom Search API"</b> 검색</li> <li><b>"Custom Search API"</b> 선택 후 <b>"사용"</b> 버튼 클릭</li> </ol>
<h3>3단계: API 키 생성</h3> <ol> <li>좌측 메뉴에서 <b>"API 및 서비스"</b> → <b>"사용자 인증 정보"</b> 클릭</li> <li>상단의 <b>"+ 사용자 인증 정보 만들기"</b> → <b>"API 키"</b> 선택</li> <li>생성된 API 키를 복사하여 앱에 입력</li> </ol>
<h3>4단계: 검색 엔진 ID (CX) 생성</h3> <ol> <li><a href="https://programmablesearchengine.google.com/">Programmable Search Engine</a> 접속</li> <li><b>"추가"</b> 클릭</li> <li>검색 엔진 이름 입력</li> <li><b>"전체 웹 검색"</b> 옵션 선택</li> <li><b>"만들기"</b> 클릭</li> <li>생성 후 아래와 같은 코드가 표시됩니다:</li> </ol> <div style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 11px;"> <script async src="https://cse.google.com/cse.js?<b style="color: #ea4335;">cx=a1b2c3d4e5f6g7h8i</b>"><br> </script> </div> <p style="margin-top: 10px;"> <b style="color: #4285f4;">cx= 뒤의 값</b>이 검색 엔진 ID입니다.<br> 위 예시에서는 <code style="background-color: #e8f0fe; padding: 2px 6px; border-radius: 3px;">a1b2c3d4e5f6g7h8i</code> 부분을 복사하여 앱에 입력하세요. </p>
<hr> <h3 style="color: #34a853;">무료 사용량</h3> <ul> <li><b>하루 100회</b> 무료 검색 가능</li> <li>초과 시 1,000회당 $5 과금</li> </ul>
<h3 style="color: #ea4335;">주의사항</h3> <ul> <li>API 키는 외부에 노출되지 않도록 주의하세요</li> <li>필요시 API 키에 제한을 설정하세요 (IP, 리퍼러 등)</li> </ul> ''') layout.addWidget(guide_text)
# 버튼 레이아웃 btn_layout = QHBoxLayout()
# Cloud Console 바로가기 cloud_btn = QPushButton('Google Cloud Console 열기') cloud_btn.setStyleSheet(''' QPushButton { background-color: #4285f4; color: white; border: none; border-radius: 4px; padding: 10px 20px; font-size: 12px; } QPushButton:hover { background-color: #3367d6; } ''') cloud_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl('https://console.cloud.google.com/'))) btn_layout.addWidget(cloud_btn)
# Search Engine 바로가기 search_btn = QPushButton('Programmable Search Engine 열기') search_btn.setStyleSheet(''' QPushButton { background-color: #34a853; color: white; border: none; border-radius: 4px; padding: 10px 20px; font-size: 12px; } QPushButton:hover { background-color: #2d8e47; } ''') search_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl('https://programmablesearchengine.google.com/'))) btn_layout.addWidget(search_btn)
# 닫기 버튼 close_btn = QPushButton('닫기') close_btn.setStyleSheet(''' QPushButton { background-color: #757575; color: white; border: none; border-radius: 4px; padding: 10px 20px; font-size: 12px; } QPushButton:hover { background-color: #616161; } ''') close_btn.clicked.connect(self.close) btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout) self.setLayout(layout)
# ============================================================ # Google Custom Search API 설정 # # 1. Google Cloud Console (https://console.cloud.google.com/) # - 새 프로젝트 생성 # - "Custom Search API" 검색 후 활성화 # - "사용자 인증 정보" > "API 키 만들기" # # 2. Programmable Search Engine (https://programmablesearchengine.google.com/) # - "새 검색 엔진" 만들기 # - "전체 웹 검색" 선택 # - 생성 후 "검색 엔진 ID" (cx) 복사 # ============================================================
API_KEY = '' # 여기에 API 키 입력 또는 앱에서 설정 SEARCH_ENGINE_ID = '' # 여기에 검색 엔진 ID 입력 또는 앱에서 설정 DAILY_LIMIT = 100 # 하루 무료 검색 제한
class UsageTracker: """하루 API 사용량을 로컬 파일에 저장하고 관리하는 클래스"""
def __init__(self): # 사용량 파일 경로 (앱과 같은 폴더에 저장) self.usage_file = Path(__file__).parent / 'usage_data.json' self.load_usage()
def load_usage(self): """저장된 사용량 데이터 로드""" today = datetime.now().strftime('%Y-%m-%d')
if self.usage_file.exists(): try: with open(self.usage_file, 'r', encoding='utf-8') as f: data = json.load(f) # 오늘 날짜가 아니면 초기화 if data.get('date') != today: self.data = {'date': today, 'count': 0} self.save_usage() else: self.data = data except (json.JSONDecodeError, IOError): self.data = {'date': today, 'count': 0} self.save_usage() else: self.data = {'date': today, 'count': 0} self.save_usage()
def save_usage(self): """사용량 데이터 저장""" try: with open(self.usage_file, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2) except IOError as e: print(f"사용량 저장 실패: {e}")
def increment(self): """사용량 1 증가""" today = datetime.now().strftime('%Y-%m-%d')
# 날짜가 바뀌었으면 초기화 if self.data.get('date') != today: self.data = {'date': today, 'count': 0}
self.data['count'] += 1 self.save_usage() return self.data['count']
def get_count(self): """현재 사용량 반환""" today = datetime.now().strftime('%Y-%m-%d')
# 날짜가 바뀌었으면 초기화 if self.data.get('date') != today: self.data = {'date': today, 'count': 0} self.save_usage()
return self.data['count']
def is_limit_exceeded(self): """제한 초과 여부 확인""" return self.get_count() >= DAILY_LIMIT
class SearchThread(QThread): """검색을 백그라운드에서 수행하는 스레드""" finished = pyqtSignal(list) error = pyqtSignal(str)
def __init__(self, keyword, api_key, cx): super().__init__() self.keyword = keyword self.api_key = api_key self.cx = cx
def run(self): try: results = self.google_search(self.keyword) self.finished.emit(results) except Exception as e: self.error.emit(str(e))
def google_search(self, keyword, num_results=10): """Google Custom Search API를 사용하여 검색""" url = 'https://www.googleapis.com/customsearch/v1'
params = { 'key': self.api_key, 'cx': self.cx, 'q': keyword, 'num': min(num_results, 10), # API는 최대 10개 'lr': 'lang_ko', # 한국어 결과 우선 }
response = requests.get(url, params=params, timeout=10)
if response.status_code == 403: raise Exception('API 키가 유효하지 않거나 할당량을 초과했습니다.') elif response.status_code == 400: raise Exception('검색 엔진 ID(CX)가 유효하지 않습니다.')
response.raise_for_status() data = response.json()
results = [] for item in data.get('items', []): results.append({ 'title': item.get('title', ''), 'link': item.get('link', ''), 'description': item.get('snippet', '') })
return results
class GoogleSearchApp(QMainWindow): def __init__(self): super().__init__() self.search_results = [] self.settings = QSettings('GoogleSearchApp', 'Settings') self.usage_tracker = UsageTracker() self.init_ui() self.load_settings() self.update_usage_display()
def init_ui(self): self.setWindowTitle('Google 검색') self.resize(1024, 768)
# 중앙 위젯 central_widget = QWidget() self.setCentralWidget(central_widget)
# 메인 레이아웃 layout = QVBoxLayout() layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15)
# 제목 라벨 title_label = QLabel('Google 검색') title_label.setFont(QFont('맑은 고딕', 16, QFont.Bold)) title_label.setAlignment(Qt.AlignCenter) layout.addWidget(title_label)
# API 설정 그룹 api_group = QGroupBox('API 설정') api_group.setFont(QFont('맑은 고딕', 10)) api_layout = QFormLayout()
self.api_key_input = QLineEdit() self.api_key_input.setPlaceholderText('Google API 키 입력') self.api_key_input.setEchoMode(QLineEdit.Password) api_layout.addRow('API 키:', self.api_key_input)
self.cx_input = QLineEdit() self.cx_input.setPlaceholderText('검색 엔진 ID (cx) 입력') self.cx_input.setEchoMode(QLineEdit.Password) api_layout.addRow('검색 엔진 ID:', self.cx_input)
# 버튼 레이아웃 btn_layout = QHBoxLayout()
# API 키 생성 가이드 버튼 guide_btn = QPushButton('API 키 생성 가이드') guide_btn.setStyleSheet(''' QPushButton { background-color: #fbbc04; color: #333; border: none; border-radius: 4px; padding: 8px 15px; } QPushButton:hover { background-color: #f9a825; } ''') guide_btn.clicked.connect(self.show_api_guide) btn_layout.addWidget(guide_btn)
# 설정 저장 버튼 save_btn = QPushButton('설정 저장') save_btn.setStyleSheet(''' QPushButton { background-color: #34a853; color: white; border: none; border-radius: 4px; padding: 8px 15px; } QPushButton:hover { background-color: #2d8e47; } ''') save_btn.clicked.connect(self.save_settings) btn_layout.addWidget(save_btn)
btn_layout.addStretch() api_layout.addRow('', btn_layout)
api_group.setLayout(api_layout) layout.addWidget(api_group)
# 사용량 표시 프레임 usage_frame = QFrame() usage_frame.setStyleSheet(''' QFrame { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; } ''') usage_layout = QHBoxLayout(usage_frame) usage_layout.setContentsMargins(15, 8, 15, 8)
# 오늘 사용량 라벨 usage_icon = QLabel('📊') usage_icon.setFont(QFont('Segoe UI Emoji', 14)) usage_layout.addWidget(usage_icon)
self.usage_label = QLabel('오늘 사용량: 0 / 100') self.usage_label.setFont(QFont('맑은 고딕', 11, QFont.Bold)) usage_layout.addWidget(self.usage_label)
usage_layout.addStretch()
# 남은 횟수 라벨 self.remaining_label = QLabel('남은 횟수: 100회') self.remaining_label.setFont(QFont('맑은 고딕', 10)) self.remaining_label.setStyleSheet('color: #666;') usage_layout.addWidget(self.remaining_label)
layout.addWidget(usage_frame)
# 검색 입력 레이아웃 search_layout = QHBoxLayout()
# 검색 입력창 (콤보박스로 변경 - 검색 기록 저장) self.search_input = QComboBox() self.search_input.setEditable(True) self.search_input.setFont(QFont('맑은 고딕', 11)) self.search_input.setMinimumHeight(40) self.search_input.lineEdit().setPlaceholderText('검색어를 입력하세요...') self.search_input.lineEdit().returnPressed.connect(self.search) self.search_input.setInsertPolicy(QComboBox.NoInsert) search_layout.addWidget(self.search_input)
# 검색 버튼 self.search_button = QPushButton('검색') self.search_button.setFont(QFont('맑은 고딕', 11)) self.search_button.setMinimumHeight(40) self.search_button.setMinimumWidth(80) self.search_button.clicked.connect(self.search) self.search_button.setStyleSheet(""" QPushButton { background-color: #4285f4; color: white; border: none; border-radius: 4px; } QPushButton:hover { background-color: #3367d6; } QPushButton:pressed { background-color: #2a56c6; } QPushButton:disabled { background-color: #cccccc; } """) search_layout.addWidget(self.search_button)
# 검색어 삭제 버튼 clear_history_btn = QPushButton('기록 삭제') clear_history_btn.setFont(QFont('맑은 고딕', 10)) clear_history_btn.setMinimumHeight(40) clear_history_btn.clicked.connect(self.clear_search_history) clear_history_btn.setStyleSheet(""" QPushButton { background-color: #ea4335; color: white; border: none; border-radius: 4px; padding: 0 15px; } QPushButton:hover { background-color: #d33828; } """) search_layout.addWidget(clear_history_btn)
layout.addLayout(search_layout)
# 상태 라벨 self.status_label = QLabel('') self.status_label.setFont(QFont('맑은 고딕', 10)) self.status_label.setStyleSheet('color: #666666;') layout.addWidget(self.status_label)
# 검색 결과 리스트 self.result_list = QListWidget() self.result_list.setFont(QFont('맑은 고딕', 10)) self.result_list.setSpacing(5) self.result_list.itemDoubleClicked.connect(self.open_link) self.result_list.setStyleSheet(""" QListWidget { border: 1px solid #ddd; border-radius: 4px; } QListWidget::item { padding: 10px; border-bottom: 1px solid #eee; } QListWidget::item:selected { background-color: #e8f0fe; color: black; } QListWidget::item:hover { background-color: #f5f5f5; } """) layout.addWidget(self.result_list)
# 안내 라벨 hint_label = QLabel('* 더블클릭: 브라우저에서 열기 | 무료: 하루 100회 검색') hint_label.setFont(QFont('맑은 고딕', 9)) hint_label.setStyleSheet('color: #888888;') layout.addWidget(hint_label)
central_widget.setLayout(layout)
# 스타일 적용 self.setStyleSheet(""" QMainWindow { background-color: #ffffff; } QLineEdit { border: 2px solid #ddd; border-radius: 4px; padding: 5px 10px; } QLineEdit:focus { border-color: #4285f4; } QGroupBox { border: 1px solid #ddd; border-radius: 4px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } """)
def load_settings(self): """저장된 설정 불러오기""" api_key = self.settings.value('api_key', API_KEY) cx = self.settings.value('cx', SEARCH_ENGINE_ID) self.api_key_input.setText(api_key) self.cx_input.setText(cx)
# 검색 기록 불러오기 search_history = self.settings.value('search_history', []) if search_history: self.search_input.addItems(search_history)
def save_settings(self): """설정 저장""" self.settings.setValue('api_key', self.api_key_input.text().strip()) self.settings.setValue('cx', self.cx_input.text().strip()) QMessageBox.information(self, '알림', '설정이 저장되었습니다.')
def show_api_guide(self): """API 키 생성 가이드 다이얼로그 표시""" dialog = ApiGuideDialog(self) dialog.exec_()
def add_to_search_history(self, keyword): """검색어를 기록에 추가""" # 이미 있는 검색어면 제거 후 맨 앞에 추가 index = self.search_input.findText(keyword) if index >= 0: self.search_input.removeItem(index) self.search_input.insertItem(0, keyword) self.search_input.setCurrentIndex(0)
# 최대 20개까지만 저장 while self.search_input.count() > 20: self.search_input.removeItem(self.search_input.count() - 1)
# 설정에 저장 history = [self.search_input.itemText(i) for i in range(self.search_input.count())] self.settings.setValue('search_history', history)
def clear_search_history(self): """검색 기록 삭제""" reply = QMessageBox.question( self, '확인', '검색 기록을 모두 삭제하시겠습니까?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.search_input.clear() self.settings.setValue('search_history', []) QMessageBox.information(self, '알림', '검색 기록이 삭제되었습니다.')
def update_usage_display(self): """사용량 표시 업데이트""" count = self.usage_tracker.get_count() remaining = DAILY_LIMIT - count
self.usage_label.setText(f'오늘 사용량: {count} / {DAILY_LIMIT}') self.remaining_label.setText(f'남은 횟수: {remaining}회')
# 사용량에 따라 색상 변경 if count >= DAILY_LIMIT: # 제한 초과 - 빨간색 self.usage_label.setStyleSheet('color: #dc3545; font-weight: bold;') self.remaining_label.setStyleSheet('color: #dc3545;') elif count >= 80: # 80% 이상 - 주황색 경고 self.usage_label.setStyleSheet('color: #fd7e14; font-weight: bold;') self.remaining_label.setStyleSheet('color: #fd7e14;') elif count >= 50: # 50% 이상 - 노란색 self.usage_label.setStyleSheet('color: #ffc107; font-weight: bold;') self.remaining_label.setStyleSheet('color: #856404;') else: # 정상 - 초록색 self.usage_label.setStyleSheet('color: #28a745; font-weight: bold;') self.remaining_label.setStyleSheet('color: #666;')
def search(self): keyword = self.search_input.currentText().strip() api_key = self.api_key_input.text().strip() cx = self.cx_input.text().strip()
if not api_key: QMessageBox.warning(self, '알림', 'API 키를 입력해주세요.') return if not cx: QMessageBox.warning(self, '알림', '검색 엔진 ID를 입력해주세요.') return if not keyword: QMessageBox.warning(self, '알림', '검색어를 입력해주세요.') return
# 사용량 제한 체크 current_count = self.usage_tracker.get_count() if current_count >= DAILY_LIMIT: QMessageBox.critical( self, '⚠️ 사용량 초과', f'오늘의 무료 검색 한도({DAILY_LIMIT}회)를 초과했습니다.\n\n' '내일 자정(00:00)이 지나면 사용량이 초기화됩니다.\n' '추가 검색이 필요하시면 Google Cloud에서 유료 플랜을 확인해주세요.' ) return
# 검색어를 기록에 추가 self.add_to_search_history(keyword)
# UI 비활성화 self.search_button.setEnabled(False) self.search_input.setEnabled(False) self.status_label.setText('검색 중...') self.result_list.clear()
# 백그라운드 스레드에서 검색 수행 self.search_thread = SearchThread(keyword, api_key, cx) self.search_thread.finished.connect(self.on_search_finished) self.search_thread.error.connect(self.on_search_error) self.search_thread.start()
def on_search_finished(self, results): """검색 완료 시 호출""" self.search_results = results self.result_list.clear()
# 사용량 증가 및 표시 업데이트 new_count = self.usage_tracker.increment() self.update_usage_display()
# 90회 이상 사용 시 경고 메시지 if new_count == 90: QMessageBox.warning( self, '⚠️ 사용량 경고', f'오늘 검색 횟수가 {new_count}회에 도달했습니다.\n' f'무료 한도({DAILY_LIMIT}회)까지 {DAILY_LIMIT - new_count}회 남았습니다.' )
if not results: self.status_label.setText('검색 결과가 없습니다.') else: self.status_label.setText(f'{len(results)}개의 검색 결과')
for i, result in enumerate(results, 1): # 리스트 아이템 생성 item_text = f"{i}. {result['title']}\n {result['link']}" if result['description']: desc = result['description'][:150] + '...' if len(result['description']) > 150 else result['description'] item_text += f"\n {desc}"
item = QListWidgetItem(item_text) item.setData(Qt.UserRole, result['link']) self.result_list.addItem(item)
# UI 활성화 self.search_button.setEnabled(True) self.search_input.setEnabled(True)
def on_search_error(self, error_msg): """검색 에러 시 호출""" self.status_label.setText('') QMessageBox.critical(self, '오류', f'검색 중 오류가 발생했습니다:\n{error_msg}')
# UI 활성화 self.search_button.setEnabled(True) self.search_input.setEnabled(True)
def open_link(self, item): """링크를 브라우저에서 열기""" link = item.data(Qt.UserRole) if link: QDesktopServices.openUrl(QUrl(link))
def get_results(self): """검색 결과를 반환 (외부에서 활용 가능)""" return self.search_results
def main(): app = QApplication(sys.argv) window = GoogleSearchApp() window.show() sys.exit(app.exec_())
if __name__ == '__main__': main() |