This commit is contained in:
9700X_PC 2025-03-30 22:49:30 +09:00
parent ee77982bc7
commit 13533913fb
25 changed files with 596 additions and 37 deletions

209
app.log

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -74,31 +74,42 @@ class AppManager:
on_change=self.on_tab_change on_change=self.on_tab_change
) )
# 로그 영역: 스크롤 가능한 컨테이너에 Text 컨트롤 배치 # 로그 영역: Container 내부에 ListView를 사용하여 스크롤 가능하도록 구현
log_text = ft.Text("", size=12, color="black") log_list = ft.ListView(
controls=[],
expand=True,
auto_scroll=True,
spacing=5,
)
log_container = ft.Container( log_container = ft.Container(
content=log_text, content=log_list,
height=150, height=150,
scroll=ft.ScrollMode.AUTO,
border=ft.border.all(1, "lightgray"), border=ft.border.all(1, "lightgray"),
padding=10, padding=10,
bgcolor="white", 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 = 1000
self.page.window.width = 1400 self.page.window.height = 900
self.page.window.height = 800
self.page.window.center() self.page.window.center()
self.page.controls.append(main_layout)
self.page.add(main_layout)
self.page.update() self.page.update()
# Logger의 GUI 콜백 설정하여 로그가 log_text에 출력되도록 함 # Logger의 GUI 콜백: log_list에 새 로그 메시지를 Text 컨트롤로 추가
def gui_log_callback(message: str): 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.page.update()
self.logger.gui_callback = gui_log_callback self.logger.gui_callback = gui_log_callback
def on_tab_change(self, e: ft.ControlEvent): def on_tab_change(self, e: ft.ControlEvent):

View File

@ -124,3 +124,13 @@ class LoginPage:
for ctrl in self.controls: for ctrl in self.controls:
self.page.add(ctrl) self.page.add(ctrl)
self.page.update() 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()

45
modules/market_actions.py Normal file
View File

@ -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 {}

View File

@ -1,52 +1,55 @@
# modules/market_page.py
import flet as ft import flet as ft
import logging import logging
from modules import db_manager
class MarketPage: class MarketPage:
def __init__(self, page: ft.Page, logger: logging.Logger, db_manager): def __init__(self, page: ft.Page, logger: logging.Logger, db_manager):
self.page = page self.page = page
self.logger = logger self.logger = logger
self.db_manager = db_manager self.db_manager = db_manager
self.market_list = [] self.logger.debug("MarketPage initialized")
self.on_products_fetched_callback = None # 외부에서 설정 (ProductPage 업데이트용)
self.build_content() self.build_content()
def build_content(self): def build_content(self):
self.logger.debug("Building MarketPage content") 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_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) self.add_market_btn = ft.ElevatedButton("마켓추가하기", on_click=self.add_market)
# 마켓 목록 테이블
# 마켓 목록 테이블: 각 행에 마켓이름, 마켓 URL, 메모 정보를 표시
self.market_table = ft.DataTable( self.market_table = ft.DataTable(
columns=[ columns=[
ft.DataColumn(ft.Text("마켓이름")), ft.DataColumn(ft.Text("마켓이름")),
ft.DataColumn(ft.Text("마켓 URL")), ft.DataColumn(ft.Text("마켓 URL")),
ft.DataColumn(ft.Text("메모")) ft.DataColumn(ft.Text("메모")),
], ],
rows=[], rows=[],
expand=True, expand=True,
key="market_table" key="market_table"
) )
self.content = ft.Column( 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 self.market_table
], ],
spacing=10, spacing=10,
) )
self.load_market_list(None) # self.load_market_list(None) # 초기 로드
def load_market_list(self, e): def load_market_list(self, e):
self.logger.debug("MarketPage.load_market_list() 호출됨") 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 = [] rows = []
for m in self.market_list: for m in markets:
row = ft.DataRow(cells=[ row = ft.DataRow(cells=[
ft.DataCell(ft.Text(m.get("name", ""))), ft.DataCell(ft.Text(m.get("market_name", ""))),
ft.DataCell(ft.Text(m.get("url", ""))), ft.DataCell(ft.Text(m.get("market_url", ""))),
ft.DataCell(ft.Text(m.get("memo", ""))) ft.DataCell(ft.Text(m.get("market_memo", ""))),
]) ])
rows.append(row) rows.append(row)
self.market_table.rows = rows self.market_table.rows = rows
@ -54,18 +57,17 @@ class MarketPage:
def load_sold_products(self, e): def load_sold_products(self, e):
self.logger.debug("MarketPage.load_sold_products() 호출됨") self.logger.debug("MarketPage.load_sold_products() 호출됨")
# DBManager를 이용해 각 마켓의 팔린상품 목록을 가져옴 # 각 마켓을 순회하며 팔린상품 목록을 가져오는 기능을 DBManager에서 구현했다고 가정합니다.
sold_products = self.db_manager.get_sold_products(self.market_list) sold_products = self.db_manager.get_sold_products(self.db_manager.get_markets())
self.logger.debug(f"Sold products fetched: {sold_products}") 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) self.on_products_fetched_callback(sold_products)
else:
self.logger.debug("on_products_fetched_callback 미설정")
self.page.update() self.page.update()
def add_market(self, e): def add_market(self, e):
self.logger.debug("MarketPage.add_market() 호출됨") self.logger.debug("MarketPage.add_market() 호출됨")
# 마켓 추가 기능은 미구현 # 마켓 추가 기능은 아직 미구현이므로 SnackBar 메시지 표시
snack = ft.SnackBar(ft.Text("마켓 추가 기능은 미구현입니다.")) snack = ft.SnackBar(ft.Text("마켓 추가 기능은 미구현입니다."))
snack.open = True snack.open = True
self.page.open(snack) self.page.open(snack)

171
poetry.lock generated
View File

@ -245,7 +245,7 @@ description = "Foreign Function Interface for Python calling C code."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] 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 = [ 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_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@ -1290,6 +1290,21 @@ files = [
[package.dependencies] [package.dependencies]
et-xmlfile = "*" 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]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" version = "24.2"
@ -1561,7 +1576,7 @@ description = "C parser in Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] 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 = [ files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, {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"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 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]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.5" version = "8.3.5"
@ -1897,6 +1925,43 @@ files = [
cryptography = ">=2.0" cryptography = ">=2.0"
jeepney = ">=0.6" 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]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"
@ -1921,6 +1986,18 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, {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]] [[package]]
name = "storage3" name = "storage3"
version = "0.11.3" version = "0.11.3"
@ -1990,6 +2067,43 @@ files = [
httpx = {version = ">=0.26,<0.29", extras = ["http2"]} httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
strenum = ">=0.4.15,<0.5.0" 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.0" version = "4.13.0"
@ -2014,6 +2128,22 @@ files = [
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, {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]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.3.0" version = "2.3.0"
@ -2026,12 +2156,32 @@ files = [
{file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, {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] [package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"] h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.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]] [[package]]
name = "websockets" name = "websockets"
version = "14.2" version = "14.2"
@ -2111,6 +2261,21 @@ files = [
{file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, {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]] [[package]]
name = "xlwings" name = "xlwings"
version = "0.33.11" version = "0.33.11"
@ -2294,4 +2459,4 @@ type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.11,<4.0" python-versions = ">=3.11,<4.0"
content-hash = "a72b2533fbfa496663693cb7f0429d8c6f27e4f552e38580d14fb86eed879d33" content-hash = "6452961b22cf2443f40017ec928a472efc29f0759ee931106368e1137c552511"

View File

@ -14,7 +14,9 @@ dependencies = [
"xlwings (>=0.33.11,<0.34.0)", "xlwings (>=0.33.11,<0.34.0)",
"requests (>=2.32.3,<3.0.0)", "requests (>=2.32.3,<3.0.0)",
"supabase (>=2.15.0,<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 = "나도 좀 팔자 - [나팔]" window_title = "나도 좀 팔자 - [나팔]"

Binary file not shown.

View File

@ -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()

97
tests/uc.py Normal file
View File

@ -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)