...
This commit is contained in:
parent
ee77982bc7
commit
13533913fb
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -74,31 +74,42 @@ class AppManager:
|
|||
on_change=self.on_tab_change
|
||||
)
|
||||
|
||||
# 로그 영역: 스크롤 가능한 컨테이너에 Text 컨트롤 배치
|
||||
log_text = ft.Text("", size=12, color="black")
|
||||
# 로그 영역: Container 내부에 ListView를 사용하여 스크롤 가능하도록 구현
|
||||
log_list = ft.ListView(
|
||||
controls=[],
|
||||
expand=True,
|
||||
auto_scroll=True,
|
||||
spacing=5,
|
||||
)
|
||||
log_container = ft.Container(
|
||||
content=log_text,
|
||||
content=log_list,
|
||||
height=150,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
border=ft.border.all(1, "lightgray"),
|
||||
padding=10,
|
||||
bgcolor="white",
|
||||
)
|
||||
|
||||
main_layout = ft.Column([tabs, log_container], expand=True, spacing=10)
|
||||
# 전체 레이아웃: 탭 영역을 확장하는 Container와 로그 영역을 하단에 배치
|
||||
main_layout = ft.Column(
|
||||
[
|
||||
ft.Container(content=tabs, expand=True), # 탭 영역이 남은 공간을 채움
|
||||
log_container
|
||||
],
|
||||
spacing=10,
|
||||
expand=True
|
||||
)
|
||||
|
||||
# 창 크기 및 중앙 배치 (1400×800)
|
||||
self.page.window.width = 1400
|
||||
self.page.window.height = 800
|
||||
self.page.window.width = 1000
|
||||
self.page.window.height = 900
|
||||
self.page.window.center()
|
||||
|
||||
self.page.add(main_layout)
|
||||
self.page.controls.append(main_layout)
|
||||
self.page.update()
|
||||
|
||||
# Logger의 GUI 콜백 설정하여 로그가 log_text에 출력되도록 함
|
||||
# Logger의 GUI 콜백: log_list에 새 로그 메시지를 Text 컨트롤로 추가
|
||||
def gui_log_callback(message: str):
|
||||
log_text.value += message + "\n"
|
||||
log_list.controls.append(ft.Text(message, size=12, color="black"))
|
||||
self.page.update()
|
||||
|
||||
self.logger.gui_callback = gui_log_callback
|
||||
|
||||
def on_tab_change(self, e: ft.ControlEvent):
|
||||
|
|
|
|||
|
|
@ -124,3 +124,13 @@ class LoginPage:
|
|||
for ctrl in self.controls:
|
||||
self.page.add(ctrl)
|
||||
self.page.update()
|
||||
|
||||
# 저장된 사용자 정보 불러오기
|
||||
user_info = self.settings_manager.load_user_info()
|
||||
if user_info.get("email"):
|
||||
self.username_field.value = user_info.get("email", "")
|
||||
if user_info.get("password"):
|
||||
self.password_field.value = user_info.get("password", "")
|
||||
# 포커스를 사용자 이름 필드에 설정
|
||||
self.username_field.focus()
|
||||
self.page.update()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import re
|
||||
import json
|
||||
import requests
|
||||
|
||||
def fetch_sold_products(market_url: str, logger=None) -> dict:
|
||||
"""
|
||||
주어진 마켓 URL에서 베스트 상품 페이지(예: https://smartstore.naver.com/modeuda/best?cp=1)에 접속하여
|
||||
__PRELOADED_STATE__ 항목을 추출하고 JSON 객체로 반환합니다.
|
||||
|
||||
Args:
|
||||
market_url (str): 예를 들어 "https://smartstore.naver.com/modeuda"
|
||||
logger: 로거 객체가 제공되면 디버그/오류 메시지를 출력합니다.
|
||||
|
||||
Returns:
|
||||
dict: 추출된 __PRELOADED_STATE__ JSON 데이터 (추출 실패 시 빈 dict 반환)
|
||||
"""
|
||||
if logger:
|
||||
logger.debug("Entering fetch_sold_products()")
|
||||
try:
|
||||
# 베스트 상품 페이지 URL 생성
|
||||
best_url = market_url.rstrip('/') + "/best?cp=1"
|
||||
if logger:
|
||||
logger.debug(f"Fetching URL: {best_url}")
|
||||
response = requests.get(best_url)
|
||||
if response.status_code != 200:
|
||||
if logger:
|
||||
logger.error(f"HTTP error {response.status_code} for URL: {best_url}")
|
||||
return {}
|
||||
html = response.text
|
||||
# __PRELOADED_STATE__ 항목을 정규식으로 추출 (비정형 데이터이므로 non-greedy match 사용)
|
||||
pattern = r'window\.__PRELOADED_STATE__=({.*?});'
|
||||
match = re.search(pattern, html, re.DOTALL)
|
||||
if not match:
|
||||
if logger:
|
||||
logger.error("Could not find __PRELOADED_STATE__ in the page")
|
||||
return {}
|
||||
state_json_str = match.group(1)
|
||||
state = json.loads(state_json_str)
|
||||
if logger:
|
||||
logger.debug("Fetched __PRELOADED_STATE__ successfully")
|
||||
return state
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Error in fetch_sold_products: {e}", exc_info=True)
|
||||
return {}
|
||||
|
|
@ -1,52 +1,55 @@
|
|||
# modules/market_page.py
|
||||
import flet as ft
|
||||
import logging
|
||||
from modules import db_manager
|
||||
|
||||
class MarketPage:
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger, db_manager):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.db_manager = db_manager
|
||||
self.market_list = []
|
||||
self.on_products_fetched_callback = None # 외부에서 설정 (ProductPage 업데이트용)
|
||||
self.logger.debug("MarketPage initialized")
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
self.logger.debug("Building MarketPage content")
|
||||
# 버튼들
|
||||
self.header = ft.Text("마켓 페이지", style=ft.TextStyle(size=24, weight="bold"), color="black")
|
||||
|
||||
# 버튼들: 마켓목록 가져오기, 팔린상품 가져오기, 마켓추가하기
|
||||
self.load_market_btn = ft.ElevatedButton("마켓목록 가져오기", on_click=self.load_market_list)
|
||||
self.load_sold_products_btn = ft.ElevatedButton("팔린상품 가져오기", on_click=self.load_sold_products)
|
||||
self.sold_products_btn = ft.ElevatedButton("팔린상품 가져오기", on_click=self.load_sold_products)
|
||||
self.add_market_btn = ft.ElevatedButton("마켓추가하기", on_click=self.add_market)
|
||||
# 마켓 목록 테이블
|
||||
|
||||
# 마켓 목록 테이블: 각 행에 마켓이름, 마켓 URL, 메모 정보를 표시
|
||||
self.market_table = ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("마켓이름")),
|
||||
ft.DataColumn(ft.Text("마켓 URL")),
|
||||
ft.DataColumn(ft.Text("메모"))
|
||||
ft.DataColumn(ft.Text("메모")),
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="market_table"
|
||||
)
|
||||
|
||||
self.content = ft.Column(
|
||||
[
|
||||
ft.Row([self.load_market_btn, self.load_sold_products_btn, self.add_market_btn]),
|
||||
ft.Row([self.load_market_btn, self.sold_products_btn, self.add_market_btn], spacing=10),
|
||||
self.market_table
|
||||
],
|
||||
spacing=10,
|
||||
)
|
||||
self.load_market_list(None)
|
||||
# self.load_market_list(None) # 초기 로드
|
||||
|
||||
def load_market_list(self, e):
|
||||
self.logger.debug("MarketPage.load_market_list() 호출됨")
|
||||
self.market_list = self.db_manager.get_markets()
|
||||
# DBManager의 get_markets() 메서드를 호출하여 마켓 목록을 가져옵니다.
|
||||
markets = self.db_manager.get_markets()
|
||||
self.logger.debug(f"Retrieved markets: {markets}")
|
||||
rows = []
|
||||
for m in self.market_list:
|
||||
for m in markets:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(m.get("name", ""))),
|
||||
ft.DataCell(ft.Text(m.get("url", ""))),
|
||||
ft.DataCell(ft.Text(m.get("memo", "")))
|
||||
ft.DataCell(ft.Text(m.get("market_name", ""))),
|
||||
ft.DataCell(ft.Text(m.get("market_url", ""))),
|
||||
ft.DataCell(ft.Text(m.get("market_memo", ""))),
|
||||
])
|
||||
rows.append(row)
|
||||
self.market_table.rows = rows
|
||||
|
|
@ -54,18 +57,17 @@ class MarketPage:
|
|||
|
||||
def load_sold_products(self, e):
|
||||
self.logger.debug("MarketPage.load_sold_products() 호출됨")
|
||||
# DBManager를 이용해 각 마켓의 팔린상품 목록을 가져옴
|
||||
sold_products = self.db_manager.get_sold_products(self.market_list)
|
||||
# 각 마켓을 순회하며 팔린상품 목록을 가져오는 기능을 DBManager에서 구현했다고 가정합니다.
|
||||
sold_products = self.db_manager.get_sold_products(self.db_manager.get_markets())
|
||||
self.logger.debug(f"Sold products fetched: {sold_products}")
|
||||
if self.on_products_fetched_callback:
|
||||
# 예를 들어, 상품 페이지에 전달하는 콜백을 호출할 수 있습니다.
|
||||
if hasattr(self, "on_products_fetched_callback") and self.on_products_fetched_callback:
|
||||
self.on_products_fetched_callback(sold_products)
|
||||
else:
|
||||
self.logger.debug("on_products_fetched_callback 미설정")
|
||||
self.page.update()
|
||||
|
||||
def add_market(self, e):
|
||||
self.logger.debug("MarketPage.add_market() 호출됨")
|
||||
# 마켓 추가 기능은 미구현
|
||||
# 마켓 추가 기능은 아직 미구현이므로 SnackBar 메시지 표시
|
||||
snack = ft.SnackBar(ft.Text("마켓 추가 기능은 미구현입니다."))
|
||||
snack.open = True
|
||||
self.page.open(snack)
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ description = "Foreign Function Interface for Python calling C code."
|
|||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""
|
||||
markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" or os_name == \"nt\" and implementation_name != \"pypy\""
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
|
|
@ -1290,6 +1290,21 @@ files = [
|
|||
[package.dependencies]
|
||||
et-xmlfile = "*"
|
||||
|
||||
[[package]]
|
||||
name = "outcome"
|
||||
version = "1.3.0.post0"
|
||||
description = "Capture the outcome of Python function calls."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"},
|
||||
{file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
|
|
@ -1561,7 +1576,7 @@ description = "C parser in Python"
|
|||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""
|
||||
markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" or os_name == \"nt\" and implementation_name != \"pypy\""
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
|
|
@ -1719,6 +1734,19 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
|
|||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pysocks"
|
||||
version = "1.7.1"
|
||||
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
|
||||
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
||||
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.5"
|
||||
|
|
@ -1897,6 +1925,43 @@ files = [
|
|||
cryptography = ">=2.0"
|
||||
jeepney = ">=0.6"
|
||||
|
||||
[[package]]
|
||||
name = "selenium"
|
||||
version = "4.30.0"
|
||||
description = "Official Python bindings for Selenium WebDriver"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "selenium-4.30.0-py3-none-any.whl", hash = "sha256:90bcd3be86a1762100a093b33e5e4530b328226da94208caadb15ce13243dffd"},
|
||||
{file = "selenium-4.30.0.tar.gz", hash = "sha256:16ab890fc7cb21a01e1b1e9a0fbaa9445fe30837eabc66e90b3bacf12138126a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2021.10.8"
|
||||
trio = ">=0.17,<1.0"
|
||||
trio-websocket = ">=0.9,<1.0"
|
||||
typing_extensions = ">=4.9,<5.0"
|
||||
urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
|
||||
websocket-client = ">=1.8,<2.0"
|
||||
|
||||
[[package]]
|
||||
name = "selenium-stealth"
|
||||
version = "1.0.6"
|
||||
description = "Trying to make python selenium more stealthy."
|
||||
optional = false
|
||||
python-versions = ">=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "selenium_stealth-1.0.6-py3-none-any.whl", hash = "sha256:b62da5452aa4a84f29a4dfb21a9696aff20788a7c570dd0b81bc04a940848b97"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
selenium = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
|
|
@ -1921,6 +1986,18 @@ files = [
|
|||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
|
||||
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storage3"
|
||||
version = "0.11.3"
|
||||
|
|
@ -1990,6 +2067,43 @@ files = [
|
|||
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
|
||||
strenum = ">=0.4.15,<0.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "trio"
|
||||
version = "0.29.0"
|
||||
description = "A friendly Python library for async concurrency and I/O"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66"},
|
||||
{file = "trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=23.2.0"
|
||||
cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""}
|
||||
idna = "*"
|
||||
outcome = "*"
|
||||
sniffio = ">=1.3.0"
|
||||
sortedcontainers = "*"
|
||||
|
||||
[[package]]
|
||||
name = "trio-websocket"
|
||||
version = "0.12.2"
|
||||
description = "WebSocket library for Trio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"},
|
||||
{file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
outcome = ">=1.2.0"
|
||||
trio = ">=0.11"
|
||||
wsproto = ">=0.14"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.0"
|
||||
|
|
@ -2014,6 +2128,22 @@ files = [
|
|||
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "undetected-chromedriver"
|
||||
version = "3.5.5"
|
||||
description = "('Selenium.webdriver.Chrome replacement with compatiblity for Brave, and other Chromium based browsers.', 'Not triggered by CloudFlare/Imperva/hCaptcha and such.', 'NOTE: results may vary due to many factors. No guarantees are given, except for ongoing efforts in understanding detection algorithms.')"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "undetected-chromedriver-3.5.5.tar.gz", hash = "sha256:9f945e1435005247abe17de316bcfda85b284a4177fd5f25167c78ced33b65ec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
requests = "*"
|
||||
selenium = ">=4.9.0"
|
||||
websockets = "*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
|
|
@ -2026,12 +2156,32 @@ files = [
|
|||
{file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""}
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.8.0"
|
||||
description = "WebSocket client for Python with low level API options"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"},
|
||||
{file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"]
|
||||
optional = ["python-socks", "wsaccel"]
|
||||
test = ["websockets"]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "14.2"
|
||||
|
|
@ -2111,6 +2261,21 @@ files = [
|
|||
{file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wsproto"
|
||||
version = "1.2.0"
|
||||
description = "WebSockets state-machine based protocol implementation"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"},
|
||||
{file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
h11 = ">=0.9.0,<1"
|
||||
|
||||
[[package]]
|
||||
name = "xlwings"
|
||||
version = "0.33.11"
|
||||
|
|
@ -2294,4 +2459,4 @@ type = ["pytest-mypy"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<4.0"
|
||||
content-hash = "a72b2533fbfa496663693cb7f0429d8c6f27e4f552e38580d14fb86eed879d33"
|
||||
content-hash = "6452961b22cf2443f40017ec928a472efc29f0759ee931106368e1137c552511"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ dependencies = [
|
|||
"xlwings (>=0.33.11,<0.34.0)",
|
||||
"requests (>=2.32.3,<3.0.0)",
|
||||
"supabase (>=2.15.0,<3.0.0)",
|
||||
"keyring (>=25.6.0,<26.0.0)"
|
||||
"keyring (>=25.6.0,<26.0.0)",
|
||||
"undetected-chromedriver (>=3.5.5,<4.0.0)",
|
||||
"selenium-stealth (>=1.0.6,<2.0.0)"
|
||||
]
|
||||
window_title = "나도 좀 팔자 - [나팔]"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,18 @@
|
|||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from modules.market_actions import fetch_sold_products
|
||||
from modules.logger import get_logger
|
||||
import json
|
||||
|
||||
def test_fetch_sold_products():
|
||||
logger = get_logger()
|
||||
# 테스트용 샘플 마켓 URL (실제 운영중인 URL로 변경 가능)
|
||||
market_url = "https://smartstore.naver.com/modeuda"
|
||||
state = fetch_sold_products(market_url, logger)
|
||||
print("Fetched __PRELOADED_STATE__:")
|
||||
print(json.dumps(state, indent=4, ensure_ascii=False))
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_fetch_sold_products()
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import undetected_chromedriver as uc
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium_stealth import stealth
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
|
||||
def fetch_preloaded_state(market_url: str, logger=None) -> dict:
|
||||
best_url = market_url.rstrip('/') + "/best?cp=1"
|
||||
if logger:
|
||||
logger.debug(f"Fetching best URL: {best_url}")
|
||||
|
||||
options = uc.ChromeOptions()
|
||||
options.headless = True
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_argument('--disable-popup-blocking')
|
||||
options.add_argument('--remote-debugging-port=9222')
|
||||
|
||||
try:
|
||||
# enable_cdp_events와 incognito 모드를 활성화합니다.
|
||||
driver = uc.Chrome(options=options, enable_cdp_events=True, incognito=True)
|
||||
|
||||
# selenium_stealth 설정
|
||||
stealth(driver,
|
||||
vendor="Google Inc. ",
|
||||
platform="Win32",
|
||||
webgl_vendor="intel Inc. ",
|
||||
renderer="Intel Iris OpenGL Engine",
|
||||
fix_hairline=True,
|
||||
)
|
||||
|
||||
# 암묵적 대기 설정: 2초
|
||||
driver.implicitly_wait(2)
|
||||
|
||||
# 추가적인 JavaScript 오버라이드 적용
|
||||
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
||||
"source": """
|
||||
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
||||
window.navigator.chrome = { runtime: {} };
|
||||
Object.defineProperty(navigator, 'languages', {get: () => ['ko-KR', 'en-US', 'en']});
|
||||
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
|
||||
"""
|
||||
})
|
||||
|
||||
|
||||
driver.get(best_url)
|
||||
|
||||
# __PRELOADED_STATE__가 로드될 때까지 최대 30초 대기
|
||||
# wait = WebDriverWait(driver, 30)
|
||||
# wait.until(lambda d: d.execute_script("return typeof window.__PRELOADED_STATE__ !== 'undefined';"))
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
# 디버깅: 현재 URL과 HTML의 일부 출력
|
||||
current_url = driver.current_url
|
||||
html = driver.page_source
|
||||
if logger:
|
||||
logger.debug(f"Current URL: {current_url}")
|
||||
logger.debug("HTML snippet:")
|
||||
# logger.debug(html[:500]) # 처음 500자만 출력
|
||||
logger.debug(html)
|
||||
|
||||
# __PRELOADED_STATE__ 추출
|
||||
pattern = r'window\.__PRELOADED_STATE__=({.*?});'
|
||||
match = re.search(pattern, html, re.DOTALL)
|
||||
if not match:
|
||||
if logger:
|
||||
logger.error("Could not find __PRELOADED_STATE__ in the page")
|
||||
driver.quit()
|
||||
return {}
|
||||
state_json_str = match.group(1)
|
||||
state = json.loads(state_json_str)
|
||||
if logger:
|
||||
logger.debug("Fetched __PRELOADED_STATE__ successfully")
|
||||
driver.quit()
|
||||
return state
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Error fetching sold products: {e}", exc_info=True)
|
||||
try:
|
||||
driver.quit()
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
# 테스트 실행 예제
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
test_logger = logging.getLogger("TestLogger")
|
||||
|
||||
sample_market_url = "https://smartstore.naver.com/modeuda"
|
||||
state = fetch_preloaded_state(sample_market_url, test_logger)
|
||||
|
||||
import pprint
|
||||
pprint.pprint(state)
|
||||
Loading…
Reference in New Issue