first commit
This commit is contained in:
parent
8508469625
commit
3b86483435
|
|
@ -1,179 +1,157 @@
|
||||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QLabel, QComboBox, QProgressBar
|
||||||
QLineEdit, QToolBar, QMenu, QTabWidget)
|
from PySide6.QtCore import Qt, Signal, Slot, QUrl
|
||||||
from PySide6.QtCore import Qt, QUrl
|
|
||||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PySide6.QtGui import QIcon, QAction
|
import os
|
||||||
|
|
||||||
|
# Fluent 위젯 추가
|
||||||
|
from qfluentwidgets import (LineEdit, PushButton, ComboBox, ProgressBar, SearchLineEdit,
|
||||||
|
ToolButton, FluentIcon, PrimaryToolButton, TransparentToolButton,
|
||||||
|
InfoBar, InfoBarPosition, CardWidget, ExpandLayout, TitleLabel,
|
||||||
|
SmoothScrollArea, IconWidget, setTheme, Theme)
|
||||||
|
|
||||||
class BrowserWidget(QWidget):
|
class BrowserWidget(QWidget):
|
||||||
|
# 시그널 정의
|
||||||
|
status_changed = Signal(str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
layout = QVBoxLayout(self)
|
# 메인 레이아웃
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
main_layout.setSpacing(0)
|
||||||
|
|
||||||
# 툴바 생성
|
# 브라우저 주소 및 컨트롤 카드
|
||||||
self.toolbar = QToolBar()
|
control_card = CardWidget(self)
|
||||||
self.toolbar.setMovable(False)
|
control_layout = QVBoxLayout(control_card)
|
||||||
|
|
||||||
# 뒤로가기 버튼
|
# 주소창 및 버튼 레이아웃
|
||||||
self.back_button = QPushButton()
|
address_layout = QHBoxLayout()
|
||||||
self.back_button.setIcon(QIcon("modules/assets/icons/back.png"))
|
|
||||||
self.toolbar.addWidget(self.back_button)
|
|
||||||
|
|
||||||
# 앞으로가기 버튼
|
# 네비게이션 버튼
|
||||||
self.forward_button = QPushButton()
|
self.back_button = TransparentToolButton(FluentIcon.CHEVRON_LEFT, self)
|
||||||
self.forward_button.setIcon(QIcon("modules/assets/icons/forward.png"))
|
self.forward_button = TransparentToolButton(FluentIcon.CHEVRON_RIGHT, self)
|
||||||
self.toolbar.addWidget(self.forward_button)
|
self.refresh_button = TransparentToolButton(FluentIcon.REFRESH, self)
|
||||||
|
self.home_button = TransparentToolButton(FluentIcon.HOME, self)
|
||||||
|
|
||||||
# 새로고침 버튼
|
# 주소 입력창
|
||||||
self.reload_button = QPushButton()
|
self.address_bar = SearchLineEdit(self)
|
||||||
self.reload_button.setIcon(QIcon("modules/assets/icons/reload.png"))
|
self.address_bar.setPlaceholderText("웹 주소 입력")
|
||||||
self.toolbar.addWidget(self.reload_button)
|
self.address_bar.returnPressed.connect(self.navigate_to_url)
|
||||||
|
|
||||||
# 홈 버튼
|
# 즐겨찾기 버튼 및 검색 버튼
|
||||||
self.home_button = QPushButton()
|
self.bookmark_button = TransparentToolButton(FluentIcon.BOOKMARK, self)
|
||||||
self.home_button.setIcon(QIcon("modules/assets/icons/home.png"))
|
self.search_button = PrimaryToolButton(FluentIcon.SEARCH, self)
|
||||||
self.toolbar.addWidget(self.home_button)
|
self.search_button.clicked.connect(self.navigate_to_url)
|
||||||
|
|
||||||
# 주소창
|
# 주소창 레이아웃에 위젯 추가
|
||||||
self.url_bar = QLineEdit()
|
address_layout.addWidget(self.back_button)
|
||||||
self.url_bar.setPlaceholderText("주소를 입력하거나 검색어를 입력하세요")
|
address_layout.addWidget(self.forward_button)
|
||||||
self.toolbar.addWidget(self.url_bar)
|
address_layout.addWidget(self.refresh_button)
|
||||||
|
address_layout.addWidget(self.home_button)
|
||||||
|
address_layout.addWidget(self.address_bar)
|
||||||
|
address_layout.addWidget(self.bookmark_button)
|
||||||
|
address_layout.addWidget(self.search_button)
|
||||||
|
|
||||||
# 새 탭 버튼
|
# 컨트롤 레이아웃에 주소창 레이아웃 추가
|
||||||
self.new_tab_button = QPushButton("+")
|
control_layout.addLayout(address_layout)
|
||||||
self.new_tab_button.clicked.connect(self.add_new_tab)
|
|
||||||
self.toolbar.addWidget(self.new_tab_button)
|
|
||||||
|
|
||||||
layout.addWidget(self.toolbar)
|
# 상태 레이아웃
|
||||||
|
status_layout = QHBoxLayout()
|
||||||
|
|
||||||
# 탭 위젯 생성
|
# 상태 라벨
|
||||||
self.tab_widget = QTabWidget()
|
self.status_label = QLabel("준비됨")
|
||||||
self.tab_widget.setTabsClosable(True)
|
|
||||||
self.tab_widget.tabCloseRequested.connect(self.close_tab)
|
|
||||||
layout.addWidget(self.tab_widget)
|
|
||||||
|
|
||||||
# 첫 번째 탭 추가
|
# 로딩 진행바
|
||||||
self.add_new_tab()
|
self.progress_bar = ProgressBar(self)
|
||||||
|
self.progress_bar.setRange(0, 100)
|
||||||
|
self.progress_bar.setValue(100)
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
|
||||||
# 북마크 툴바
|
# 상태 레이아웃에 위젯 추가
|
||||||
self.bookmark_toolbar = QToolBar()
|
status_layout.addWidget(self.status_label)
|
||||||
self.bookmark_toolbar.setMovable(False)
|
status_layout.addWidget(self.progress_bar)
|
||||||
layout.addWidget(self.bookmark_toolbar)
|
|
||||||
|
|
||||||
# 이벤트 연결
|
# 컨트롤 레이아웃에 상태 레이아웃 추가
|
||||||
self.url_bar.returnPressed.connect(self.navigate_to_url)
|
control_layout.addLayout(status_layout)
|
||||||
self.back_button.clicked.connect(self.current_web_view().back)
|
|
||||||
self.forward_button.clicked.connect(self.current_web_view().forward)
|
|
||||||
self.reload_button.clicked.connect(self.current_web_view().reload)
|
|
||||||
self.home_button.clicked.connect(self.navigate_home)
|
|
||||||
|
|
||||||
# 탭 변경 시 주소창 업데이트
|
# 웹뷰 생성
|
||||||
self.tab_widget.currentChanged.connect(self.update_url_bar)
|
self.web_view = QWebEngineView()
|
||||||
|
self.web_view.loadStarted.connect(self.load_started)
|
||||||
|
self.web_view.loadProgress.connect(self.load_progress)
|
||||||
|
self.web_view.loadFinished.connect(self.load_finished)
|
||||||
|
self.web_view.urlChanged.connect(self.url_changed)
|
||||||
|
|
||||||
def current_web_view(self):
|
# 기본 홈페이지 로드
|
||||||
return self.tab_widget.currentWidget()
|
self.load_home_page()
|
||||||
|
|
||||||
def add_new_tab(self, url="https://www.google.com"):
|
|
||||||
web_view = QWebEngineView()
|
|
||||||
web_view.setUrl(QUrl(url))
|
|
||||||
|
|
||||||
# URL 변경 시 주소창 업데이트
|
# 네비게이션 버튼 연결
|
||||||
web_view.urlChanged.connect(self.update_url_bar)
|
self.back_button.clicked.connect(self.web_view.back)
|
||||||
|
self.forward_button.clicked.connect(self.web_view.forward)
|
||||||
|
self.refresh_button.clicked.connect(self.web_view.reload)
|
||||||
|
self.home_button.clicked.connect(self.load_home_page)
|
||||||
|
|
||||||
# 링크 클릭 시 새 탭에서 열기
|
# 메인 레이아웃에 위젯 추가
|
||||||
web_view.page().newWindowRequested.connect(self.handle_new_window_request)
|
main_layout.addWidget(control_card)
|
||||||
|
main_layout.addWidget(self.web_view)
|
||||||
|
|
||||||
# 컨텍스트 메뉴 설정
|
|
||||||
web_view.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
||||||
web_view.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos, web_view))
|
|
||||||
|
|
||||||
index = self.tab_widget.addTab(web_view, "새 탭")
|
|
||||||
self.tab_widget.setCurrentIndex(index)
|
|
||||||
|
|
||||||
# 탭 제목 업데이트
|
|
||||||
web_view.titleChanged.connect(lambda title: self.tab_widget.setTabText(index, title[:20] if len(title) > 20 else title))
|
|
||||||
|
|
||||||
def close_tab(self, index):
|
|
||||||
if self.tab_widget.count() > 1:
|
|
||||||
self.tab_widget.removeTab(index)
|
|
||||||
else:
|
|
||||||
self.current_web_view().setUrl(QUrl("https://www.google.com"))
|
|
||||||
|
|
||||||
def handle_new_window_request(self, request):
|
|
||||||
url = request.requestedUrl().toString()
|
|
||||||
new_web_view = QWebEngineView()
|
|
||||||
new_web_view.setUrl(QUrl(url))
|
|
||||||
index = self.tab_widget.addTab(new_web_view, "새 탭")
|
|
||||||
self.tab_widget.setCurrentIndex(index)
|
|
||||||
|
|
||||||
# URL 변경 시 주소창 업데이트
|
|
||||||
new_web_view.urlChanged.connect(self.update_url_bar)
|
|
||||||
|
|
||||||
# 컨텍스트 메뉴 설정
|
|
||||||
new_web_view.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
||||||
new_web_view.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos, new_web_view))
|
|
||||||
|
|
||||||
# 탭 제목 업데이트
|
|
||||||
new_web_view.titleChanged.connect(lambda title: self.tab_widget.setTabText(index, title[:20] if len(title) > 20 else title))
|
|
||||||
|
|
||||||
request.accept()
|
|
||||||
|
|
||||||
def navigate_to_url(self):
|
def navigate_to_url(self):
|
||||||
url = self.url_bar.text()
|
url_text = self.address_bar.text().strip()
|
||||||
if not url.startswith(('http://', 'https://')):
|
if not url_text:
|
||||||
url = 'https://www.google.com/search?q=' + url
|
return
|
||||||
self.current_web_view().setUrl(QUrl(url))
|
|
||||||
|
if not url_text.startswith(('http://', 'https://')):
|
||||||
def navigate_home(self):
|
# 검색어로 간주하고 검색 페이지로 이동
|
||||||
self.current_web_view().setUrl(QUrl("https://www.google.com"))
|
if ' ' in url_text:
|
||||||
|
self.search_term(url_text)
|
||||||
def update_url_bar(self):
|
return
|
||||||
current_url = self.current_web_view().url().toString()
|
# http:// 추가
|
||||||
self.url_bar.setText(current_url)
|
url_text = 'http://' + url_text
|
||||||
|
|
||||||
def show_context_menu(self, position, web_view):
|
self.web_view.setUrl(QUrl(url_text))
|
||||||
context_menu = QMenu(self)
|
self.status_changed.emit(f"이동: {url_text}")
|
||||||
|
|
||||||
# 기본 메뉴 항목
|
def search_term(self, term):
|
||||||
back_action = QAction("뒤로 가기", self)
|
# 네이버 검색 사용
|
||||||
forward_action = QAction("앞으로 가기", self)
|
search_url = f"https://search.naver.com/search.naver?query={term}"
|
||||||
reload_action = QAction("새로고침", self)
|
self.web_view.setUrl(QUrl(search_url))
|
||||||
|
self.status_changed.emit(f"검색: {term}")
|
||||||
|
|
||||||
# 커스텀 메뉴 항목
|
def load_started(self):
|
||||||
search_action = QAction("지재권 검색", self)
|
self.progress_bar.setVisible(True)
|
||||||
add_forbidden_action = QAction("금지어 추가", self)
|
self.progress_bar.setValue(0)
|
||||||
naver_search_action = QAction("네이버 검색", self)
|
self.status_label.setText("로딩 중...")
|
||||||
|
self.status_changed.emit("페이지 로딩 시작")
|
||||||
|
|
||||||
context_menu.addAction(back_action)
|
def load_progress(self, progress):
|
||||||
context_menu.addAction(forward_action)
|
self.progress_bar.setValue(progress)
|
||||||
context_menu.addAction(reload_action)
|
|
||||||
context_menu.addSeparator()
|
|
||||||
context_menu.addAction(search_action)
|
|
||||||
context_menu.addAction(add_forbidden_action)
|
|
||||||
context_menu.addAction(naver_search_action)
|
|
||||||
|
|
||||||
# 메뉴 표시
|
def load_finished(self, success):
|
||||||
action = context_menu.exec_(web_view.mapToGlobal(position))
|
self.progress_bar.setVisible(False)
|
||||||
|
if success:
|
||||||
|
self.status_label.setText("로딩 완료")
|
||||||
|
self.status_changed.emit("페이지 로딩 완료")
|
||||||
|
else:
|
||||||
|
self.status_label.setText("로딩 실패")
|
||||||
|
self.status_changed.emit("페이지 로딩 실패")
|
||||||
|
self.show_error_message("페이지를 로드할 수 없습니다")
|
||||||
|
|
||||||
|
def url_changed(self, url):
|
||||||
|
self.address_bar.setText(url.toString())
|
||||||
|
|
||||||
# 액션 처리
|
def load_home_page(self):
|
||||||
if action == back_action:
|
# 기본 홈페이지 설정 (여기서는 네이버)
|
||||||
web_view.back()
|
self.web_view.setUrl(QUrl("https://www.naver.com"))
|
||||||
elif action == forward_action:
|
self.status_changed.emit("홈페이지 로드")
|
||||||
web_view.forward()
|
|
||||||
elif action == reload_action:
|
def show_error_message(self, message):
|
||||||
web_view.reload()
|
InfoBar.error(
|
||||||
elif action == search_action:
|
title="오류",
|
||||||
selected_text = web_view.selectedText()
|
content=message,
|
||||||
if selected_text:
|
parent=self,
|
||||||
print(f"지재권 검색을 클릭했습니다: {selected_text}")
|
position=InfoBarPosition.TOP,
|
||||||
# TODO: 실제 지재권 검색 기능 구현
|
duration=3000
|
||||||
elif action == add_forbidden_action:
|
)
|
||||||
# TODO: 금지어 추가 기능 구현
|
|
||||||
pass
|
|
||||||
elif action == naver_search_action:
|
|
||||||
selected_text = web_view.selectedText()
|
|
||||||
if selected_text:
|
|
||||||
naver_url = f"https://search.naver.com/search.naver?query={selected_text}"
|
|
||||||
self.add_new_tab(naver_url)
|
|
||||||
|
|
@ -10,14 +10,23 @@ from modules.tools.category_manager import CategoryManager
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
# Fluent Widgets 추가
|
||||||
|
from qfluentwidgets import (FluentWindow, NavigationItemPosition, FluentIcon,
|
||||||
|
MessageBox, setTheme, Theme, FluentStyleSheet, InfoBar,
|
||||||
|
InfoBarPosition, setFont)
|
||||||
|
from qfluentwidgets import FluentTranslator
|
||||||
|
|
||||||
|
class MainWindow(FluentWindow):
|
||||||
# 시그널 정의
|
# 시그널 정의
|
||||||
status_changed = Signal(str)
|
status_changed = Signal(str)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("MyCar Browser")
|
self.setWindowTitle("MyCar Browser")
|
||||||
self.setMinimumSize(1200, 800)
|
self.resize(1200, 800)
|
||||||
|
|
||||||
|
# Fluent 디자인 테마 설정
|
||||||
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
# 로깅 설정
|
# 로깅 설정
|
||||||
self.setup_logger()
|
self.setup_logger()
|
||||||
|
|
@ -66,20 +75,10 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def initUI(self):
|
def initUI(self):
|
||||||
"""UI 관련 코드 초기화"""
|
"""UI 관련 코드 초기화"""
|
||||||
# 메인 위젯 설정
|
# 브라우저 위젯 생성
|
||||||
self.central_widget = QWidget()
|
|
||||||
self.setCentralWidget(self.central_widget)
|
|
||||||
|
|
||||||
# 레이아웃 설정
|
|
||||||
self.main_layout = QVBoxLayout(self.central_widget)
|
|
||||||
|
|
||||||
# 메뉴바 설정
|
|
||||||
self.menu_bar = MenuBar(self)
|
|
||||||
self.setMenuBar(self.menu_bar)
|
|
||||||
|
|
||||||
# 스플리터 설정 (브라우저와 도구 영역)
|
|
||||||
self.splitter = QSplitter(Qt.Horizontal)
|
|
||||||
self.browser_widget = BrowserWidget()
|
self.browser_widget = BrowserWidget()
|
||||||
|
|
||||||
|
# 도구 위젯 생성
|
||||||
self.tools_widget = ToolsWidget(
|
self.tools_widget = ToolsWidget(
|
||||||
parent=self,
|
parent=self,
|
||||||
thread_pool=self.thread_pool,
|
thread_pool=self.thread_pool,
|
||||||
|
|
@ -88,29 +87,38 @@ class MainWindow(QMainWindow):
|
||||||
category_manager=self.category_manager
|
category_manager=self.category_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
self.splitter.addWidget(self.browser_widget)
|
|
||||||
self.splitter.addWidget(self.tools_widget)
|
|
||||||
self.splitter.setStretchFactor(0, 7) # 브라우저 영역 70%
|
|
||||||
self.splitter.setStretchFactor(1, 3) # 도구 영역 30%
|
|
||||||
|
|
||||||
self.main_layout.addWidget(self.splitter)
|
|
||||||
|
|
||||||
# 상태바 설정
|
# 상태바 설정
|
||||||
self.status_bar = StatusBar()
|
self.status_bar = StatusBar()
|
||||||
self.setStatusBar(self.status_bar)
|
self.setStatusBar(self.status_bar)
|
||||||
|
|
||||||
# 스타일 설정
|
# Fluent 스타일의 내비게이션 추가
|
||||||
self.setStyleSheet("""
|
self.addSubInterface(self.browser_widget, FluentIcon.GLOBE, "브라우저")
|
||||||
QMainWindow {
|
self.addSubInterface(self.tools_widget, FluentIcon.SETTING, "도구")
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
# 추가 도구 메뉴 추가
|
||||||
QSplitter::handle {
|
self.navigationInterface().addSeparator()
|
||||||
background-color: #cccccc;
|
|
||||||
}
|
# 로그 메뉴 추가
|
||||||
QSplitter::handle:horizontal {
|
self.navigationInterface().addItem(
|
||||||
width: 2px;
|
routeKey='logs',
|
||||||
}
|
icon=FluentIcon.SCROLL,
|
||||||
""")
|
text='로그',
|
||||||
|
position=NavigationItemPosition.BOTTOM
|
||||||
|
)
|
||||||
|
|
||||||
|
# 설정 메뉴 추가
|
||||||
|
self.navigationInterface().addItem(
|
||||||
|
routeKey='settings',
|
||||||
|
icon=FluentIcon.SETTING,
|
||||||
|
text='설정',
|
||||||
|
position=NavigationItemPosition.BOTTOM
|
||||||
|
)
|
||||||
|
|
||||||
|
# 기본 페이지 설정
|
||||||
|
self.navigationInterface().setCurrentItem("브라우저")
|
||||||
|
|
||||||
|
# 로고 이미지 설정
|
||||||
|
# self.setWindowIcon(FluentIcon.CAR)
|
||||||
|
|
||||||
self.logger.info("UI 초기화 완료")
|
self.logger.info("UI 초기화 완료")
|
||||||
|
|
||||||
|
|
@ -129,4 +137,23 @@ class MainWindow(QMainWindow):
|
||||||
def update_status(self, message):
|
def update_status(self, message):
|
||||||
"""상태바 메시지 업데이트"""
|
"""상태바 메시지 업데이트"""
|
||||||
self.status_changed.emit(message)
|
self.status_changed.emit(message)
|
||||||
self.logger.info(f"상태 메시지: {message}")
|
self.logger.info(f"상태 메시지: {message}")
|
||||||
|
|
||||||
|
# Fluent 스타일 알림 표시
|
||||||
|
InfoBar.success(
|
||||||
|
title='알림',
|
||||||
|
content=message,
|
||||||
|
duration=2000,
|
||||||
|
position=InfoBarPosition.BOTTOM_RIGHT,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
|
def showMessageBox(self, title, content):
|
||||||
|
"""Fluent 스타일 메시지 박스 표시"""
|
||||||
|
w = MessageBox(title, content, self)
|
||||||
|
w.show()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""앱 종료 시 이벤트 처리"""
|
||||||
|
self.logger.info("애플리케이션 종료")
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from PySide6.QtWidgets import QStatusBar, QLabel
|
from PySide6.QtWidgets import QStatusBar, QLabel, QHBoxLayout
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
from qfluentwidgets import StateToolTip, ProgressBar, PushButton, FluentIcon
|
||||||
|
|
||||||
class StatusBar(QStatusBar):
|
class StatusBar(QStatusBar):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
|
|
@ -9,28 +10,53 @@ class StatusBar(QStatusBar):
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
# 상태 메시지 레이블
|
# 상태 메시지 레이블
|
||||||
self.status_label = QLabel()
|
self.status_label = QLabel()
|
||||||
|
self.status_label.setStyleSheet("color: #555555; padding: 2px;")
|
||||||
self.addWidget(self.status_label)
|
self.addWidget(self.status_label)
|
||||||
|
|
||||||
# 로그인 상태 레이블
|
# 로그인 상태 레이블
|
||||||
self.login_status_label = QLabel("로그인되지 않음")
|
self.login_status_label = QLabel("로그인되지 않음")
|
||||||
|
self.login_status_label.setStyleSheet("color: #777777; padding: 2px;")
|
||||||
self.addPermanentWidget(self.login_status_label)
|
self.addPermanentWidget(self.login_status_label)
|
||||||
|
|
||||||
# 스타일 설정
|
# 전체 상태바 스타일 설정
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QStatusBar {
|
QStatusBar {
|
||||||
background-color: #f0f0f0;
|
background-color: #f5f5f5;
|
||||||
border-top: 1px solid #cccccc;
|
border-top: 1px solid #e0e0e0;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
QLabel {
|
QLabel {
|
||||||
padding: 2px;
|
font-size: 12px;
|
||||||
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def set_status_message(self, message):
|
def set_status_message(self, message):
|
||||||
|
"""상태 메시지 설정"""
|
||||||
self.status_label.setText(message)
|
self.status_label.setText(message)
|
||||||
|
|
||||||
def set_login_status(self, is_logged_in):
|
def set_login_status(self, is_logged_in):
|
||||||
|
"""로그인 상태 설정"""
|
||||||
if is_logged_in:
|
if is_logged_in:
|
||||||
self.login_status_label.setText("로그인됨")
|
self.login_status_label.setText("로그인됨")
|
||||||
|
self.login_status_label.setStyleSheet("color: #4caf50; padding: 2px; font-weight: bold;")
|
||||||
else:
|
else:
|
||||||
self.login_status_label.setText("로그인되지 않음")
|
self.login_status_label.setText("로그인되지 않음")
|
||||||
|
self.login_status_label.setStyleSheet("color: #777777; padding: 2px;")
|
||||||
|
|
||||||
|
def show_progress(self, title, content):
|
||||||
|
"""진행 상태 툴팁 표시 (오버레이 형태)"""
|
||||||
|
state_tooltip = StateToolTip(title, content, self.parent())
|
||||||
|
state_tooltip.show()
|
||||||
|
return state_tooltip
|
||||||
|
|
||||||
|
def update_progress(self, state_tooltip, progress, content=None):
|
||||||
|
"""진행 상태 업데이트"""
|
||||||
|
if progress >= 100:
|
||||||
|
state_tooltip.setContent(content or "완료되었습니다")
|
||||||
|
state_tooltip.setState(True)
|
||||||
|
else:
|
||||||
|
if content:
|
||||||
|
state_tooltip.setContent(content)
|
||||||
|
state_tooltip.setProgress(progress)
|
||||||
|
|
@ -3,7 +3,13 @@ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QTabWidget, QPushButton,
|
||||||
from PySide6.QtCore import Qt, Signal, Slot
|
from PySide6.QtCore import Qt, Signal, Slot
|
||||||
from modules.xlsFilter.xls_filter_dialog import XlsFilterDialog
|
from modules.xlsFilter.xls_filter_dialog import XlsFilterDialog
|
||||||
|
|
||||||
class ToolsWidget(QWidget):
|
# Fluent Widgets 추가
|
||||||
|
from qfluentwidgets import (ScrollArea, ExpandLayout, SimpleCardWidget, SettingCardGroup,
|
||||||
|
SwitchSettingCard, ComboBoxSettingCard, ToolButton, FluentIcon,
|
||||||
|
PrimaryPushButton, InfoBar, InfoBarPosition, SpinBox, LineEdit,
|
||||||
|
TextEdit, ListWidget, PushButton, TabWidget, SmoothScrollArea)
|
||||||
|
|
||||||
|
class ToolsWidget(ScrollArea):
|
||||||
# 시그널 정의
|
# 시그널 정의
|
||||||
status_changed = Signal(str)
|
status_changed = Signal(str)
|
||||||
|
|
||||||
|
|
@ -24,10 +30,15 @@ class ToolsWidget(QWidget):
|
||||||
self.logger.info("도구 위젯 초기화 완료")
|
self.logger.info("도구 위젯 초기화 완료")
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
layout = QVBoxLayout(self)
|
# 스크롤 영역 설정
|
||||||
|
self.setWidgetResizable(True)
|
||||||
|
|
||||||
|
# 메인 위젯 및 레이아웃
|
||||||
|
self.main_widget = QWidget()
|
||||||
|
self.expand_layout = ExpandLayout(self.main_widget)
|
||||||
|
|
||||||
# 탭 위젯 생성
|
# 탭 위젯 생성
|
||||||
self.tab_widget = QTabWidget()
|
self.tab_widget = TabWidget(self.main_widget)
|
||||||
|
|
||||||
# 배송비 계산기 탭
|
# 배송비 계산기 탭
|
||||||
self.shipping_calc_tab = QWidget()
|
self.shipping_calc_tab = QWidget()
|
||||||
|
|
@ -54,126 +65,236 @@ class ToolsWidget(QWidget):
|
||||||
self.setup_xls_filter_tab()
|
self.setup_xls_filter_tab()
|
||||||
self.tab_widget.addTab(self.xls_filter_tab, "엑셀 필터링")
|
self.tab_widget.addTab(self.xls_filter_tab, "엑셀 필터링")
|
||||||
|
|
||||||
layout.addWidget(self.tab_widget)
|
# 카드 그룹 추가
|
||||||
|
card_group = SettingCardGroup("도구 모음", self.main_widget)
|
||||||
|
card_text = "다양한 도구를 탭을 통해 사용할 수 있습니다."
|
||||||
|
|
||||||
|
# 도구 카드 추가
|
||||||
|
self.tools_card = SimpleCardWidget(FluentIcon.SETTING, "도구 모음", card_text, self.main_widget)
|
||||||
|
self.tools_card.setFixedHeight(120)
|
||||||
|
self.tools_card.setFixedWidth(self.width())
|
||||||
|
|
||||||
|
# 레이아웃에 위젯 추가
|
||||||
|
self.expand_layout.addWidget(self.tools_card)
|
||||||
|
self.expand_layout.addWidget(card_group)
|
||||||
|
self.expand_layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
|
# 위젯 설정
|
||||||
|
self.setWidget(self.main_widget)
|
||||||
|
|
||||||
def setup_shipping_calc_tab(self):
|
def setup_shipping_calc_tab(self):
|
||||||
layout = QVBoxLayout(self.shipping_calc_tab)
|
layout = QVBoxLayout(self.shipping_calc_tab)
|
||||||
|
|
||||||
|
# 설정 카드 그룹
|
||||||
|
card_group = SettingCardGroup("배송비 계산", self.shipping_calc_tab)
|
||||||
|
|
||||||
# 무게 입력
|
# 무게 입력
|
||||||
weight_label = QLabel("무게 (kg):")
|
self.weight_input = SpinBox(self.shipping_calc_tab)
|
||||||
self.weight_input = QLineEdit()
|
self.weight_input.setRange(0, 100)
|
||||||
layout.addWidget(weight_label)
|
self.weight_input.setSuffix(" kg")
|
||||||
layout.addWidget(self.weight_input)
|
self.weight_input.setToolTip("배송 상품의 무게를 입력하세요")
|
||||||
|
weight_card = ComboBoxSettingCard(
|
||||||
|
FluentIcon.WEIGHT,
|
||||||
|
"무게",
|
||||||
|
"배송 상품의 무게를 입력하세요",
|
||||||
|
customWidget=self.weight_input,
|
||||||
|
parent=card_group
|
||||||
|
)
|
||||||
|
|
||||||
# 지역 선택
|
# 지역 선택
|
||||||
region_label = QLabel("지역:")
|
self.region_input = LineEdit(self.shipping_calc_tab)
|
||||||
self.region_input = QLineEdit()
|
self.region_input.setPlaceholderText("배송 지역")
|
||||||
layout.addWidget(region_label)
|
region_card = ComboBoxSettingCard(
|
||||||
layout.addWidget(self.region_input)
|
FluentIcon.GLOBE,
|
||||||
|
"지역",
|
||||||
|
"배송 지역을 입력하세요",
|
||||||
|
customWidget=self.region_input,
|
||||||
|
parent=card_group
|
||||||
|
)
|
||||||
|
|
||||||
|
# 카드 그룹에 추가
|
||||||
|
card_group.addSettingCard(weight_card)
|
||||||
|
card_group.addSettingCard(region_card)
|
||||||
|
|
||||||
# 계산 버튼
|
# 계산 버튼
|
||||||
calc_button = QPushButton("계산하기")
|
self.calc_button = PrimaryPushButton("계산하기")
|
||||||
calc_button.clicked.connect(self.calculate_shipping)
|
self.calc_button.clicked.connect(self.calculate_shipping)
|
||||||
layout.addWidget(calc_button)
|
|
||||||
|
|
||||||
# 결과 표시
|
# 결과 표시
|
||||||
self.result_label = QLabel("배송비: ")
|
self.result_label = QLabel("배송비: ")
|
||||||
layout.addWidget(self.result_label)
|
|
||||||
|
|
||||||
|
# 레이아웃에 위젯 추가
|
||||||
|
layout.addWidget(card_group)
|
||||||
|
layout.addWidget(self.calc_button)
|
||||||
|
layout.addWidget(self.result_label)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
def setup_forbidden_words_tab(self):
|
def setup_forbidden_words_tab(self):
|
||||||
layout = QVBoxLayout(self.forbidden_words_tab)
|
layout = QVBoxLayout(self.forbidden_words_tab)
|
||||||
|
|
||||||
|
# 설정 카드 그룹
|
||||||
|
card_group = SettingCardGroup("금지어 관리", self.forbidden_words_tab)
|
||||||
|
|
||||||
# 금지어 입력
|
# 금지어 입력
|
||||||
self.word_input = QLineEdit()
|
self.word_input = LineEdit(self.forbidden_words_tab)
|
||||||
self.word_input.setPlaceholderText("금지어 입력")
|
self.word_input.setPlaceholderText("금지어 입력")
|
||||||
layout.addWidget(self.word_input)
|
word_card = ComboBoxSettingCard(
|
||||||
|
FluentIcon.REMOVE,
|
||||||
|
"금지어",
|
||||||
|
"추가할 금지어를 입력하세요. 콤마 또는 공백으로 구분하여 여러 개 입력 가능합니다.",
|
||||||
|
customWidget=self.word_input,
|
||||||
|
parent=card_group
|
||||||
|
)
|
||||||
|
card_group.addSettingCard(word_card)
|
||||||
|
|
||||||
# 추가 버튼
|
# 추가 버튼
|
||||||
add_button = QPushButton("추가")
|
self.add_button = PrimaryPushButton("추가")
|
||||||
add_button.clicked.connect(self.add_forbidden_word)
|
self.add_button.clicked.connect(self.add_forbidden_word)
|
||||||
layout.addWidget(add_button)
|
|
||||||
|
|
||||||
# 금지어 목록
|
# 금지어 목록
|
||||||
self.word_list = QListWidget()
|
self.word_list = ListWidget(self.forbidden_words_tab)
|
||||||
layout.addWidget(self.word_list)
|
|
||||||
|
|
||||||
# 삭제 버튼
|
# 삭제 버튼
|
||||||
delete_button = QPushButton("선택 삭제")
|
self.delete_button = PushButton("선택 삭제")
|
||||||
delete_button.clicked.connect(self.delete_forbidden_word)
|
self.delete_button.setIcon(FluentIcon.DELETE)
|
||||||
layout.addWidget(delete_button)
|
self.delete_button.clicked.connect(self.delete_forbidden_word)
|
||||||
|
|
||||||
# 금지어 목록 로드
|
# 금지어 목록 로드
|
||||||
if self.forbidden_words:
|
if self.forbidden_words:
|
||||||
self.load_forbidden_words()
|
self.load_forbidden_words()
|
||||||
|
|
||||||
|
# 레이아웃에 위젯 추가
|
||||||
|
layout.addWidget(card_group)
|
||||||
|
layout.addWidget(self.add_button)
|
||||||
|
layout.addWidget(self.word_list)
|
||||||
|
layout.addWidget(self.delete_button)
|
||||||
|
|
||||||
def setup_category_tab(self):
|
def setup_category_tab(self):
|
||||||
layout = QVBoxLayout(self.category_tab)
|
layout = QVBoxLayout(self.category_tab)
|
||||||
|
|
||||||
|
# 설정 카드 그룹
|
||||||
|
card_group = SettingCardGroup("카테고리 관리", self.category_tab)
|
||||||
|
|
||||||
# 카테고리 입력
|
# 카테고리 입력
|
||||||
self.category_input = QLineEdit()
|
self.category_input = LineEdit(self.category_tab)
|
||||||
self.category_input.setPlaceholderText("카테고리 입력")
|
self.category_input.setPlaceholderText("카테고리 입력")
|
||||||
layout.addWidget(self.category_input)
|
category_card = ComboBoxSettingCard(
|
||||||
|
FluentIcon.FOLDER,
|
||||||
|
"카테고리",
|
||||||
|
"추가할 카테고리를 입력하세요",
|
||||||
|
customWidget=self.category_input,
|
||||||
|
parent=card_group
|
||||||
|
)
|
||||||
|
card_group.addSettingCard(category_card)
|
||||||
|
|
||||||
# 추가 버튼
|
# 추가 버튼
|
||||||
add_button = QPushButton("추가")
|
self.add_cat_button = PrimaryPushButton("추가")
|
||||||
add_button.clicked.connect(self.add_category)
|
self.add_cat_button.clicked.connect(self.add_category)
|
||||||
layout.addWidget(add_button)
|
|
||||||
|
|
||||||
# 카테고리 목록
|
# 카테고리 목록
|
||||||
self.category_list = QListWidget()
|
self.category_list = ListWidget(self.category_tab)
|
||||||
layout.addWidget(self.category_list)
|
|
||||||
|
|
||||||
# 삭제 버튼
|
# 삭제 버튼
|
||||||
delete_button = QPushButton("선택 삭제")
|
self.delete_cat_button = PushButton("선택 삭제")
|
||||||
delete_button.clicked.connect(self.delete_category)
|
self.delete_cat_button.setIcon(FluentIcon.DELETE)
|
||||||
layout.addWidget(delete_button)
|
self.delete_cat_button.clicked.connect(self.delete_category)
|
||||||
|
|
||||||
# 카테고리 목록 로드
|
# 카테고리 목록 로드
|
||||||
if self.category_manager:
|
if self.category_manager:
|
||||||
self.load_categories()
|
self.load_categories()
|
||||||
|
|
||||||
|
# 레이아웃에 위젯 추가
|
||||||
|
layout.addWidget(card_group)
|
||||||
|
layout.addWidget(self.add_cat_button)
|
||||||
|
layout.addWidget(self.category_list)
|
||||||
|
layout.addWidget(self.delete_cat_button)
|
||||||
|
|
||||||
def setup_log_tab(self):
|
def setup_log_tab(self):
|
||||||
layout = QVBoxLayout(self.log_tab)
|
layout = QVBoxLayout(self.log_tab)
|
||||||
|
|
||||||
|
# 설정 카드 그룹
|
||||||
|
card_group = SettingCardGroup("로그 관리", self.log_tab)
|
||||||
|
card_group.addSettingCard(SwitchSettingCard(
|
||||||
|
FluentIcon.HISTORY,
|
||||||
|
"자동 저장",
|
||||||
|
"로그를 자동으로 파일에 저장합니다",
|
||||||
|
True,
|
||||||
|
parent=card_group
|
||||||
|
))
|
||||||
|
|
||||||
# 로그 표시 영역
|
# 로그 표시 영역
|
||||||
self.log_text = QTextEdit()
|
self.log_text = TextEdit(self.log_tab)
|
||||||
self.log_text.setReadOnly(True)
|
self.log_text.setReadOnly(True)
|
||||||
layout.addWidget(self.log_text)
|
|
||||||
|
|
||||||
# 로그 정리 버튼
|
# 로그 정리 버튼
|
||||||
clear_button = QPushButton("로그 정리")
|
self.clear_button = PushButton("로그 정리")
|
||||||
clear_button.clicked.connect(self.clear_logs)
|
self.clear_button.setIcon(FluentIcon.CLEAR)
|
||||||
layout.addWidget(clear_button)
|
self.clear_button.clicked.connect(self.clear_logs)
|
||||||
|
|
||||||
|
# 레이아웃에 위젯 추가
|
||||||
|
layout.addWidget(card_group)
|
||||||
|
layout.addWidget(self.log_text)
|
||||||
|
layout.addWidget(self.clear_button)
|
||||||
|
|
||||||
def setup_xls_filter_tab(self):
|
def setup_xls_filter_tab(self):
|
||||||
layout = QVBoxLayout(self.xls_filter_tab)
|
layout = QVBoxLayout(self.xls_filter_tab)
|
||||||
|
|
||||||
# 엑셀 필터링 버튼
|
# 설정 카드 그룹
|
||||||
filter_button = QPushButton("엑셀 필터링 실행")
|
card_group = SettingCardGroup("엑셀 필터링", self.xls_filter_tab)
|
||||||
filter_button.clicked.connect(self.run_xls_filter)
|
card_text = "엑셀 파일에서 상품 정보를 추출하고 금지어/카테고리를 필터링합니다."
|
||||||
layout.addWidget(filter_button)
|
|
||||||
|
|
||||||
|
info_card = SimpleCardWidget(FluentIcon.FILTER, "엑셀 필터링", card_text, self.xls_filter_tab)
|
||||||
|
info_card.setFixedHeight(120)
|
||||||
|
|
||||||
|
# 엑셀 필터링 버튼
|
||||||
|
self.filter_button = PrimaryPushButton("엑셀 필터링 실행")
|
||||||
|
self.filter_button.setIcon(FluentIcon.FILTER)
|
||||||
|
self.filter_button.clicked.connect(self.run_xls_filter)
|
||||||
|
|
||||||
|
# 레이아웃에 위젯 추가
|
||||||
|
layout.addWidget(card_group)
|
||||||
|
layout.addWidget(info_card)
|
||||||
|
layout.addWidget(self.filter_button)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
def calculate_shipping(self):
|
def calculate_shipping(self):
|
||||||
weight = self.weight_input.text()
|
weight = self.weight_input.value()
|
||||||
region = self.region_input.text()
|
region = self.region_input.text()
|
||||||
|
|
||||||
if not weight or not region:
|
if not region:
|
||||||
|
self.show_info_bar("경고", "지역을 입력해주세요", InfoBarPosition.TOP, True)
|
||||||
self.status_changed.emit("무게와 지역을 입력해주세요.")
|
self.status_changed.emit("무게와 지역을 입력해주세요.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
weight = float(weight)
|
|
||||||
# 여기에서 실제 배송비 계산 로직을 구현
|
# 여기에서 실제 배송비 계산 로직을 구현
|
||||||
shipping_cost = weight * 2500 # 예시 계산 로직
|
shipping_cost = weight * 2500 # 예시 계산 로직
|
||||||
self.result_label.setText(f"배송비: {shipping_cost:,} 원")
|
self.result_label.setText(f"배송비: {shipping_cost:,} 원")
|
||||||
self.status_changed.emit(f"배송비 계산 완료: {shipping_cost:,} 원")
|
self.status_changed.emit(f"배송비 계산 완료: {shipping_cost:,} 원")
|
||||||
|
self.show_info_bar("완료", f"배송비 계산 완료: {shipping_cost:,} 원", InfoBarPosition.BOTTOM_RIGHT)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
self.show_info_bar("오류", "무게는 숫자로 입력해주세요.", InfoBarPosition.TOP, True)
|
||||||
self.status_changed.emit("무게는 숫자로 입력해주세요.")
|
self.status_changed.emit("무게는 숫자로 입력해주세요.")
|
||||||
|
|
||||||
|
def show_info_bar(self, title, content, position=InfoBarPosition.TOP, is_error=False):
|
||||||
|
"""정보 바 표시"""
|
||||||
|
if is_error:
|
||||||
|
InfoBar.error(
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
parent=self,
|
||||||
|
position=position,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
InfoBar.success(
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
parent=self,
|
||||||
|
position=position,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
def load_forbidden_words(self):
|
def load_forbidden_words(self):
|
||||||
"""금지어 목록 로드"""
|
"""금지어 목록 로드"""
|
||||||
if self.logger:
|
if self.logger:
|
||||||
|
|
@ -193,6 +314,7 @@ class ToolsWidget(QWidget):
|
||||||
if self.forbidden_words:
|
if self.forbidden_words:
|
||||||
self.forbidden_words.add_word(word)
|
self.forbidden_words.add_word(word)
|
||||||
self.status_changed.emit(f"금지어 '{word}' 추가됨")
|
self.status_changed.emit(f"금지어 '{word}' 추가됨")
|
||||||
|
self.show_info_bar("추가 완료", f"금지어 '{word}' 추가됨", InfoBarPosition.BOTTOM_RIGHT)
|
||||||
self.word_list.addItem(word)
|
self.word_list.addItem(word)
|
||||||
self.word_input.clear()
|
self.word_input.clear()
|
||||||
|
|
||||||
|
|
@ -203,6 +325,7 @@ class ToolsWidget(QWidget):
|
||||||
if self.forbidden_words:
|
if self.forbidden_words:
|
||||||
self.forbidden_words.remove_word(word)
|
self.forbidden_words.remove_word(word)
|
||||||
self.status_changed.emit(f"금지어 '{word}' 삭제됨")
|
self.status_changed.emit(f"금지어 '{word}' 삭제됨")
|
||||||
|
self.show_info_bar("삭제 완료", f"금지어 '{word}' 삭제됨", InfoBarPosition.BOTTOM_RIGHT)
|
||||||
self.word_list.takeItem(self.word_list.row(current_item))
|
self.word_list.takeItem(self.word_list.row(current_item))
|
||||||
|
|
||||||
def load_categories(self):
|
def load_categories(self):
|
||||||
|
|
@ -224,6 +347,7 @@ class ToolsWidget(QWidget):
|
||||||
if self.category_manager:
|
if self.category_manager:
|
||||||
self.category_manager.add_category(category)
|
self.category_manager.add_category(category)
|
||||||
self.status_changed.emit(f"카테고리 '{category}' 추가됨")
|
self.status_changed.emit(f"카테고리 '{category}' 추가됨")
|
||||||
|
self.show_info_bar("추가 완료", f"카테고리 '{category}' 추가됨", InfoBarPosition.BOTTOM_RIGHT)
|
||||||
self.category_list.addItem(category)
|
self.category_list.addItem(category)
|
||||||
self.category_input.clear()
|
self.category_input.clear()
|
||||||
|
|
||||||
|
|
@ -234,11 +358,13 @@ class ToolsWidget(QWidget):
|
||||||
if self.category_manager:
|
if self.category_manager:
|
||||||
self.category_manager.remove_category(category)
|
self.category_manager.remove_category(category)
|
||||||
self.status_changed.emit(f"카테고리 '{category}' 삭제됨")
|
self.status_changed.emit(f"카테고리 '{category}' 삭제됨")
|
||||||
|
self.show_info_bar("삭제 완료", f"카테고리 '{category}' 삭제됨", InfoBarPosition.BOTTOM_RIGHT)
|
||||||
self.category_list.takeItem(self.category_list.row(current_item))
|
self.category_list.takeItem(self.category_list.row(current_item))
|
||||||
|
|
||||||
def clear_logs(self):
|
def clear_logs(self):
|
||||||
self.log_text.clear()
|
self.log_text.clear()
|
||||||
self.status_changed.emit("로그가 정리되었습니다.")
|
self.status_changed.emit("로그가 정리되었습니다.")
|
||||||
|
self.show_info_bar("완료", "로그가 정리되었습니다", InfoBarPosition.BOTTOM_RIGHT)
|
||||||
|
|
||||||
def run_xls_filter(self):
|
def run_xls_filter(self):
|
||||||
if self.logger:
|
if self.logger:
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ selenium = "^4.31.0"
|
||||||
webdriver-manager = "^4.0.2"
|
webdriver-manager = "^4.0.2"
|
||||||
pandas = "^2.2.2"
|
pandas = "^2.2.2"
|
||||||
openpyxl = "^3.1.2"
|
openpyxl = "^3.1.2"
|
||||||
|
PySide6-Fluent-Widgets = "^1.5.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue