From 177d8b7daecf4b0208a8198357f911333b8bcfd1 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 16 Oct 2025 14:28:11 +0900 Subject: [PATCH 01/27] =?UTF-8?q?fix:=20=EC=83=81=ED=92=88=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/booth_discord/booth.py b/booth_discord/booth.py index f1551ac..0107244 100644 --- a/booth_discord/booth.py +++ b/booth_discord/booth.py @@ -33,7 +33,7 @@ def get_booth_order_info(item_number, cookie): product_div = soup.find("div", class_="flex desktop:flex-row mobile:flex-col") if not product_div: - raise Exception("상품이 존재하지 않습니다.") + raise Exception("상품이 존재하지 않거나, 구매하지 않은 상품입니다.") order_page = product_div.find("a").get("href") order_parse = parse_url(order_page) From e98f71428189e8d3d2359c82792034812f6f9a45 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 16 Oct 2025 14:35:20 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=AA=85=EB=A0=B9=EC=96=B4=EC=97=90=20FBX?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=A0=90=20=ED=99=95=EC=9D=B8=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth_discord.py | 5 ++++- booth_discord/booth_sqlite.py | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/booth_discord/booth_discord.py b/booth_discord/booth_discord.py index 490c3a6..0ff4d2d 100644 --- a/booth_discord/booth_discord.py +++ b/booth_discord/booth_discord.py @@ -47,12 +47,14 @@ async def booth(interaction: discord.Interaction, cookie: str): @app_commands.describe(item_name="아이템 이름을 입력 해주세요") @app_commands.describe(intent_encoding="아이템 이름의 인코딩 방식을 입력해주세요 (기본값: shift_jis)") @app_commands.describe(summary_this="업데이트 내용 요약 (기본값: True)") + @app_commands.describe(fbx_only="FBX 변경점만 확인 (기본값: False)") async def item_add( interaction: discord.Interaction, item_number: str, item_name: str = None, intent_encoding: str = "shift_jis", - summary_this: bool = True + summary_this: bool = True, + fbx_only: bool = False ): try: await interaction.response.defer(ephemeral=True) @@ -63,6 +65,7 @@ async def item_add( item_name, intent_encoding, summary_this, + fbx_only, ) self.logger.info(f"User {interaction.user.id} is adding item {item_number}") await interaction.followup.send(f"[{item_number}] 등록 완료", ephemeral=True) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index 55ebe12..7a92fb3 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -26,6 +26,7 @@ def __init__(self, db, logger): archive_this BOOLEAN, gift_item BOOLEAN, summary_this BOOLEAN, + fbx_only BOOLEAN, FOREIGN KEY(discord_user_id) REFERENCES booth_accounts(discord_user_id) ) ''') @@ -50,7 +51,7 @@ def add_booth_account(self, session_cookie, discord_user_id): self.conn.commit() return self.cursor.lastrowid - def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this): + def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this, fbx_only): # Moved import to be local to avoid dependency issues in booth_checker from booth import get_booth_order_info @@ -73,8 +74,9 @@ def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, archive_this, gift_item, summary_this + fbx_only ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (booth_order_info[1], booth_item_number, discord_user_id, @@ -84,7 +86,8 @@ def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, True, False, booth_order_info[0], - summary_this)) + summary_this, + fbx_only)) self.conn.commit() self.add_discord_noti_channel(discord_channel_id, booth_order_info[1]) return self.cursor.lastrowid @@ -227,6 +230,7 @@ def get_booth_items(self): items.archive_this, items.gift_item, items.summary_this, + items.fbx_only, accounts.session_cookie, accounts.discord_user_id, channels.discord_channel_id From 4d595ee4d99e80c90a825f9f44adea4bba9cd28a Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 16 Oct 2025 14:39:31 +0900 Subject: [PATCH 03/27] =?UTF-8?q?refactor:=20booth=5Fsqlite.py=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/booth_sqlite.py | 209 +--------------------------------- booth_discord/booth_sqlite.py | 23 ---- 2 files changed, 2 insertions(+), 230 deletions(-) diff --git a/booth_checker/booth_sqlite.py b/booth_checker/booth_sqlite.py index 6c9484b..1685504 100644 --- a/booth_checker/booth_sqlite.py +++ b/booth_checker/booth_sqlite.py @@ -1,194 +1,13 @@ import sqlite3 class BoothSQLite(): - def __init__(self, db, logger=None): - self.logger = logger + def __init__(self, db): self.conn = sqlite3.connect(db) self.conn.execute("PRAGMA journal_mode=WAL;") self.cursor = self.conn.cursor() - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS booth_accounts ( - session_cookie TEXT UNIQUE, - discord_user_id INTEGER PRIMARY KEY - ) - ''') - - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS booth_items ( - booth_order_number TEXT PRIMARY KEY, - booth_item_number TEXT, - discord_user_id INTEGER, - item_name TEXT, - intent_encoding TEXT, - download_number_show BOOLEAN, - changelog_show BOOLEAN, - archive_this BOOLEAN, - gift_item BOOLEAN, - summary_this BOOLEAN, - FOREIGN KEY(discord_user_id) REFERENCES booth_accounts(discord_user_id) - ) - ''') - - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS discord_noti_channels ( - discord_channel_id INTEGER, - booth_order_number TEXT, - UNIQUE(discord_channel_id, booth_order_number), - FOREIGN KEY(booth_order_number) REFERENCES booth_items(booth_order_number) - ) - ''') - def __del__(self): self.conn.close() - - def add_booth_account(self, session_cookie, discord_user_id): - self.cursor.execute(''' - INSERT OR IGNORE INTO booth_accounts (session_cookie, discord_user_id) - VALUES (?, ?) - ''', (session_cookie, discord_user_id)) - self.conn.commit() - return self.cursor.lastrowid - - def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this): - # Moved import to be local to avoid dependency issues in booth_checker - from booth import get_booth_order_info - - booth_account = self.get_booth_account(discord_user_id) - if self.is_item_duplicate(booth_item_number, discord_user_id): - raise Exception("이미 등록된 아이템입니다.") - # 서버에 부스 아이템 파일이 남지않도록 하드코딩 - # download_number_show True, changelog_show True, archive_this False - if booth_account: - booth_order_info = get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) - self.cursor.execute(''' - INSERT OR IGNORE INTO booth_items ( - booth_order_number, - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - download_number_show, - changelog_show, - archive_this, - gift_item, - summary_this - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (booth_order_info[1], - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - True, - True, - False, - booth_order_info[0], - summary_this)) - self.conn.commit() - self.add_discord_noti_channel(discord_channel_id, booth_order_info[1]) - return self.cursor.lastrowid - else: - raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") - - def del_booth_account(self, discord_user_id): - try: - self.cursor.execute(''' - SELECT EXISTS ( - SELECT 1 - FROM booth_items - WHERE discord_user_id = ? - ); - ''', (discord_user_id,)) - result = self.cursor.fetchone() - if result[0] == 1: - raise Exception("BOOTH 아이템이 등록되어 있습니다. 먼저 아이템을 삭제해주세요.") - self.cursor.execute(''' - DELETE FROM booth_accounts WHERE discord_user_id = ? - ''', (discord_user_id,)) - self.conn.commit() - return self.cursor.lastrowid - except Exception as e: - raise Exception(e) - - def del_booth_item(self, discord_user_id, booth_item_number): - booth_account = self.get_booth_account(discord_user_id) - if booth_account: - try: - self.cursor.execute(''' - SELECT booth_order_number FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? - ''', (booth_item_number, discord_user_id)) - result = self.cursor.fetchone() - if not result: - raise Exception(f"Item {booth_item_number} not found for user {discord_user_id}") - - booth_order_number = result[0] - if self.logger: - self.logger.debug(f"booth_order_number: {booth_order_number}") - - self.cursor.execute(''' - DELETE FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? - ''', (booth_item_number, discord_user_id)) - self.conn.commit() - - self.del_discord_noti_channel(booth_order_number) - return self.cursor.lastrowid - except Exception as e: - raise Exception(e) - else: - raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") - - def get_booth_account(self, discord_user_id): - self.cursor.execute(''' - SELECT * FROM booth_accounts - WHERE discord_user_id = ? - ''', (discord_user_id,)) - result = self.cursor.fetchone() - if result: - return result - return None - - def is_item_duplicate(self, booth_item_number, discord_user_id): - self.cursor.execute(''' - SELECT * FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? - ''', (booth_item_number, discord_user_id)) - result = self.cursor.fetchone() - return bool(result) - - def list_booth_items(self, discord_user_id, discord_channel_id): - booth_account = self.get_booth_account(discord_user_id) - if booth_account: - self.cursor.execute(''' - SELECT bi.booth_item_number - FROM booth_items bi - JOIN discord_noti_channels dnc - ON bi.booth_order_number = dnc.booth_order_number - WHERE bi.discord_user_id = ? - AND dnc.discord_channel_id = ?; - ''', (discord_user_id, discord_channel_id)) - return self.cursor.fetchall() - else: - raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") - - def add_discord_noti_channel(self, discord_channel_id, booth_order_number): - self.cursor.execute(''' - INSERT OR IGNORE INTO discord_noti_channels (discord_channel_id, booth_order_number) - VALUES (?, ?) - ''', (discord_channel_id, booth_order_number)) - self.conn.commit() - return self.cursor.lastrowid - - def del_discord_noti_channel(self, booth_order_number): - if self.logger: - self.logger.debug(f"del_discord_noti_channel - booth_order_number : {booth_order_number}") - self.cursor.execute(''' - DELETE FROM discord_noti_channels WHERE booth_order_number = ? - ''', (booth_order_number,)) - self.conn.commit() - return self.cursor.lastrowid def get_booth_items(self): self.cursor.execute(''' @@ -201,6 +20,7 @@ def get_booth_items(self): items.archive_this, items.gift_item, items.summary_this, + items.fbx_only, accounts.session_cookie, accounts.discord_user_id, channels.discord_channel_id @@ -211,28 +31,3 @@ def get_booth_items(self): ON items.booth_order_number = channels.booth_order_number ''') return self.cursor.fetchall() - - def update_discord_noti_channel(self, discord_user_id, discord_channel_id, booth_item_number): - booth_order_number = self.get_booth_order_number(booth_item_number, discord_user_id) - if not booth_order_number: - raise Exception("Item not found") - self.cursor.execute(''' - UPDATE discord_noti_channels - SET discord_channel_id = ? - WHERE booth_order_number = ? - ''', (discord_channel_id, booth_order_number)) - self.conn.commit() - return self.cursor.lastrowid - - def get_booth_order_number(self, booth_item_number, discord_user_id): - self.cursor.execute(''' - SELECT booth_order_number FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? - ''', (booth_item_number, discord_user_id)) - result = self.cursor.fetchone() - return result[0] if result else None - - def get_booth_item_count(self, discord_user_id): - self.cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = ?', (discord_user_id,)) - result = self.cursor.fetchone() - return result[0] if result else 0 diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index 7a92fb3..27d006e 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -218,26 +218,3 @@ def get_booth_item_count(self, discord_user_id): self.cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = ?', (discord_user_id,)) result = self.cursor.fetchone() return result[0] if result else 0 - - def get_booth_items(self): - self.cursor.execute(''' - SELECT items.booth_order_number, - items.booth_item_number, - items.item_name, - items.intent_encoding, - items.download_number_show, - items.changelog_show, - items.archive_this, - items.gift_item, - items.summary_this, - items.fbx_only, - accounts.session_cookie, - accounts.discord_user_id, - channels.discord_channel_id - FROM booth_items items - INNER JOIN booth_accounts accounts - ON items.discord_user_id = accounts.discord_user_id - INNER JOIN discord_noti_channels channels - ON items.booth_order_number = channels.booth_order_number - ''') - return self.cursor.fetchall() From ddaff32bbdea26c36c9fd8588b001b2a95f67dff Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 16 Oct 2025 15:52:49 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat:=20FBX=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EC=9D=84=20=EC=B2=98=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=95=A8=EC=88=98=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 215 ++++++++++++++++++++++++++++++-------- booth_checker/shared.py | 7 +- 2 files changed, 179 insertions(+), 43 deletions(-) diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index 61db797..5a14cb3 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -72,9 +72,10 @@ def prepare_item_data(item): "archive_this": bool(item[6]), "gift_item": bool(item[7]), "summary_this": bool(item[8]), - "booth_cookie": {"_plaza_session_nktz7u": item[9]}, - "discord_user_id": item[10], - "discord_channel_id": item[11], + "fbx_only": bool(item[9]), + "booth_cookie": {"_plaza_session_nktz7u": item[10]}, + "discord_user_id": item[11], + "discord_channel_id": item[12] } def fetch_booth_data(item_data): @@ -99,14 +100,14 @@ def fetch_booth_data(item_data): return download_url_list, product_info_list, download_short_list, thumblist -def load_and_compare_version(order_num, download_short_list): - """Loads version file and checks for changes. Returns (path, data) or None.""" +def load_and_compare_version(order_num, download_short_list, fbx_only): + """Loads version file, ensures structure, and reports whether the download list changed.""" version_file_path = f'./version/json/{order_num}.json' # In dry run, if the file doesn't exist, simulate a new item by creating an empty version_json. if DRY_RUN and not os.path.exists(version_file_path): logger.info('Dry run: version file not found, simulating new item.') - version_json = {'short-list': [], 'name-list': [], 'files': {}} + version_json = {'short-list': [], 'name-list': [], 'files': {}, 'fbx-files': {}} else: # Original logic for non-dry-run or if file exists in dry run. if not os.path.exists(version_file_path): @@ -127,7 +128,16 @@ def _load_json(path): else: # If file is corrupted in dry run, also simulate a new item. logger.info('Dry run: version file corrupted, simulating new item.') - version_json = {'short-list': [], 'name-list': [], 'files': {}} + version_json = {'short-list': [], 'name-list': [], 'files': {}, 'fbx-files': {}} + + if 'fbx-files' not in version_json: + version_json['fbx-files'] = {} + if 'files' not in version_json: + version_json['files'] = {} + if 'name-list' not in version_json: + version_json['name-list'] = [] + if 'short-list' not in version_json: + version_json['short-list'] = [] local_list = version_json.get('short-list', []) @@ -138,15 +148,19 @@ def _load_json(path): ) if not has_changed: - logger.info('nothing has changed.') - return None + if not fbx_only: + logger.info('nothing has changed.') + return None, None, False + logger.info('Download list unchanged; checking FBX contents for differences.') if not download_short_list: logger.error('BOOTH no responding, but change was detected.') - return None + return None, None, False - logger.info('something has changed.') - return version_file_path, version_json + if has_changed: + logger.info('something has changed.') + + return version_file_path, version_json, has_changed def process_files_for_changelog(item_data, download_url_list, local_list): """Downloads new files and archives them if configured.""" @@ -157,7 +171,9 @@ def process_files_for_changelog(item_data, download_url_list, local_list): download_path = f'./download/{filename}' item_name_list.append(filename) - if item_data["changelog_show"] or item_data["archive_this"]: + should_download = item_data["changelog_show"] or item_data["archive_this"] or item_data["fbx_only"] + + if should_download: logger.info(f'downloading {download_number} to {download_path}') booth.download_item(download_number, download_path, item_data["booth_cookie"]) @@ -169,7 +185,14 @@ def process_files_for_changelog(item_data, download_url_list, local_list): return item_name_list def generate_changelog_and_summary(item_data, download_url_list, version_json): - """Generates changelog, summary, and uploads to S3 if configured.""" + """Generates changelog content and returns metadata. + + Returns: + tuple: (changelog_html_path, s3_object_url, summary_result, diff_found, new_fbx_records) + """ + if item_data["fbx_only"]: + return generate_fbx_changelog_and_summary(item_data, download_url_list, version_json) + saved_prehash = {} for local_file in version_json['files'].keys(): element_mark(version_json['files'][local_file], 2, local_file, saved_prehash) @@ -184,9 +207,10 @@ def generate_changelog_and_summary(item_data, download_url_list, version_json): logger.debug(traceback.format_exc()) path_list = generate_path_info(version_json, saved_prehash) - if not path_list: - logger.warning('path_list is empty. Changelog will not be generated.') - return None, None, None + diff_found = bool(path_list) + if not diff_found: + logger.info('No structural changes detected; skipping changelog generation.') + return None, None, None, False, None tree = build_tree(path_list) html_list_items = tree_to_html(tree) @@ -227,7 +251,89 @@ def generate_changelog_and_summary(item_data, download_url_list, version_json): elif s3_uploader and DRY_RUN: logger.info('Dry run: Skipping changelog upload to S3.') - return changelog_html_path, s3_object_url, summary_result + return changelog_html_path, s3_object_url, summary_result, diff_found, None + + +def generate_fbx_changelog_and_summary(item_data, download_url_list, version_json): + """Generates changelog information for FBX-only tracking.""" + previous_fbx = version_json.get('fbx-files', {}) or {} + current_fbx = {} + + for _, filename in download_url_list: + download_path = f'./download/{filename}' + logger.info(f'parsing {filename} structure (FBX only)') + try: + process_file_tree(download_path, filename, None, item_data["encoding"], [], fbx_only=True, fbx_records=current_fbx) + except Exception as e: + logger.error(f'An error occurred while parsing {filename}: {e}') + logger.debug(traceback.format_exc()) + + added = [] + changed = [] + + for name, new_hash in current_fbx.items(): + old_hash = previous_fbx.get(name) + if old_hash is None: + added.append(name) + elif old_hash != new_hash: + changed.append(name) + + deleted = [name for name in previous_fbx.keys() if name not in current_fbx] + + if not added and not changed and not deleted: + logger.info('No FBX hash differences detected; skipping changelog generation.') + return None, None, None, False, current_fbx + + path_list = [] + for name in sorted(added): + path_list.append({'line_str': name, 'status': 1}) + for name in sorted(changed): + path_list.append({'line_str': name, 'status': 3}) + for name in sorted(deleted): + path_list.append({'line_str': name, 'status': 2}) + + tree = build_tree(path_list) + html_list_items = tree_to_html(tree) if item_data["changelog_show"] else '' + summary_data = files_list(tree) + + changelog_html_path = None + s3_object_url = None + summary_result = None + + if item_data["summary_this"] and gemini_api_key and summary_data and not DRY_RUN: + logger.info('Generating summary') + summary_result = f"{summary.chat(summary_data)}" + logger.debug(summary_result) + elif item_data["summary_this"] and gemini_api_key and summary_data and DRY_RUN: + logger.info('Dry run: Skipping summary generation.') + + if item_data["changelog_show"]: + file_loader = FileSystemLoader('./templates') + env = Environment(loader=file_loader) + changelog_html = env.get_template('changelog.html') + + data = { + 'html_list_items': html_list_items + } + output = changelog_html.render(data) + + changelog_filename = uuid.uuid4() + changelog_html_path = f"changelog/{changelog_filename}.html" + + with open(changelog_html_path, 'w', encoding='utf-8') as html_file: + html_file.write(output) + + if s3_uploader and not DRY_RUN: + try: + s3_uploader.upload(changelog_html_path, s3['bucket_name'], changelog_html_path) + logger.info('Changelog uploaded to S3') + s3_object_url = f"https://{s3['bucket_access_url']}/{changelog_html_path}" + except Exception as e: + logger.error(f'Error occurred while uploading changelog to S3: {e}') + elif s3_uploader and DRY_RUN: + logger.info('Dry run: Skipping changelog upload to S3.') + + return changelog_html_path, s3_object_url, summary_result, True, current_fbx def send_discord_notification(item_data, product_info, thumb, local_list_name, item_name_list, changelog_html_path, s3_object_url, summary_result): """Sends update notification to Discord.""" @@ -270,15 +376,23 @@ def send_discord_notification(item_data, product_info, thumb, local_list_name, i else: logger.error(f'send_changelog API 요청 실패: {response.text}') -def update_version_file(version_file_path, version_json, item_name_list, download_short_list): +def update_version_file(version_file_path, version_json, item_name_list, download_short_list, fbx_only=False, new_fbx_records=None): """Cleans up and saves the updated version file.""" if DRY_RUN: logger.info(f'Dry run: Skipping version file update for {version_file_path}.') return - cleanup_version_json(version_json['files']) + if not fbx_only: + cleanup_version_json(version_json['files']) + else: + version_json['files'] = {} + version_json['name-list'] = item_name_list version_json['short-list'] = download_short_list + if new_fbx_records is not None: + version_json['fbx-files'] = new_fbx_records + elif 'fbx-files' not in version_json: + version_json['fbx-files'] = {} with open(version_file_path, 'w') as f: simdjson.dump(version_json, fp=f, indent=4) @@ -300,20 +414,32 @@ def init_update_check(item): # This is the main orchestrator function if item_data["name"] is None: item_data["name"] = product_name - version_info = load_and_compare_version(order_num, download_short_list) # This is the new, correct function - if not version_info: + version_file_path, version_json, download_list_changed = load_and_compare_version(order_num, download_short_list, item_data["fbx_only"]) + if version_file_path is None and version_json is None and not download_list_changed: return - version_file_path, version_json = version_info - local_list = version_json.get('short-list', []) local_list_name = version_json.get('name-list', []) item_name_list = process_files_for_changelog(item_data, download_url_list, local_list) # This is a new helper function changelog_html_path, s3_object_url, summary_result = None, None, None - if item_data["changelog_show"]: - changelog_html_path, s3_object_url, summary_result = generate_changelog_and_summary(item_data, download_url_list, version_json) + diff_found = download_list_changed + new_fbx_records = None + + if item_data["changelog_show"] or item_data["fbx_only"]: + changelog_html_path, s3_object_url, summary_result, calc_diff_found, new_fbx_records = generate_changelog_and_summary( + item_data, download_url_list, version_json + ) + if item_data["fbx_only"]: + diff_found = calc_diff_found + elif item_data["changelog_show"]: + diff_found = calc_diff_found or diff_found + + if item_data["fbx_only"] and not diff_found: + logger.info('FBX contents unchanged. Skipping notification.') + update_version_file(version_file_path, version_json, item_name_list, download_short_list, item_data["fbx_only"], new_fbx_records) + return thumb = thumblist[0] if thumblist else "https://asset.booth.pm/assets/thumbnail_placeholder_f_150x150-73e650fbec3b150090cbda36377f1a3402c01e36fa067d01.png" @@ -322,7 +448,7 @@ def init_update_check(item): # This is the main orchestrator function item_name_list, changelog_html_path, s3_object_url, summary_result ) - update_version_file(version_file_path, version_json, item_name_list, download_short_list) + update_version_file(version_file_path, version_json, item_name_list, download_short_list, item_data["fbx_only"], new_fbx_records) def generate_path_info(root, saved_prehash): path_list = [] @@ -364,7 +490,7 @@ def _generate_path_info_recursive(root, saved_prehash, path_list, current_level= path_list.append(file_info) _generate_path_info_recursive(file_node, saved_prehash, path_list, current_level + 1) -def process_file_tree(input_path, filename, version_json, encoding, current_path): +def process_file_tree(input_path, filename, version_json, encoding, current_path, fbx_only=False, fbx_records=None): current_path.append(filename) pathstr = '/'.join(current_path) @@ -386,25 +512,30 @@ def process_file_tree(input_path, filename, version_json, encoding, current_path end_file_process(0, process_path) return - node = version_json - for part in current_path[:-1]: - node = node.setdefault('files', {}).setdefault(part, {}) # Corrected: Removed extra closing parenthesis - parent_dict = node.setdefault('files', {}) - file_node = parent_dict.get(filename) - - if file_node is None: - parent_dict[filename] = {'hash': filehash, 'mark_as': 1} - else: - if file_node['hash'] == filehash: - file_node['mark_as'] = 0 + if not fbx_only: + node = version_json + for part in current_path[:-1]: + node = node.setdefault('files', {}).setdefault(part, {}) + parent_dict = node.setdefault('files', {}) + file_node = parent_dict.get(filename) + + if file_node is None: + parent_dict[filename] = {'hash': filehash, 'mark_as': 1} else: - file_node['hash'] = filehash - file_node['mark_as'] = 3 + if file_node['hash'] == filehash: + file_node['mark_as'] = 0 + else: + file_node['hash'] = filehash + file_node['mark_as'] = 3 + else: + if not isdir and filename.lower().endswith('.fbx'): + if fbx_records is not None: + fbx_records[filename] = filehash if zip_type > 0 or os.path.isdir(process_path): for new_filename in os.listdir(process_path): new_process_path = os.path.join(process_path, new_filename) - process_file_tree(new_process_path, new_filename, version_json, encoding, current_path) + process_file_tree(new_process_path, new_filename, version_json, encoding, current_path, fbx_only=fbx_only, fbx_records=fbx_records) current_path.pop() end_file_process(zip_type, process_path) diff --git a/booth_checker/shared.py b/booth_checker/shared.py index fb354d6..86cf4d1 100644 --- a/booth_checker/shared.py +++ b/booth_checker/shared.py @@ -3,7 +3,12 @@ def createVersionFile(version_file_path): with open(version_file_path, 'w') as f: - short_list = {'short-list': [], 'files': {}, 'name-list': []} + short_list = { + 'short-list': [], + 'files': {}, + 'name-list': [], + 'fbx-files': {} + } simdjson.dump(short_list, fp=f, indent=4) def createFolder(directory): From b655572ba5c49b8cf8b42bed2ae016842a512731 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Fri, 17 Oct 2025 10:14:46 +0900 Subject: [PATCH 05/27] =?UTF-8?q?fix:=20fbx=5Fonly=EA=B0=80=20true?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EA=B3=84=EC=86=8D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9D=84=20=ED=99=95=EC=9D=B8=ED=95=A0=EB=A0=A4?= =?UTF-8?q?=EA=B3=A0=20=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index 5a14cb3..417012c 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -148,10 +148,8 @@ def _load_json(path): ) if not has_changed: - if not fbx_only: - logger.info('nothing has changed.') - return None, None, False - logger.info('Download list unchanged; checking FBX contents for differences.') + logger.info('nothing has changed.') + return None, None, False if not download_short_list: logger.error('BOOTH no responding, but change was detected.') From 12c3e8a7803784bf6832b77f9af6b9e93b5bfce3 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 16:18:38 +0900 Subject: [PATCH 06/27] =?UTF-8?q?fix:=20fbx=5Frecords=EC=97=90=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=95=B4=EC=8B=9C=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index 417012c..4ba3d0d 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -528,7 +528,7 @@ def process_file_tree(input_path, filename, version_json, encoding, current_path else: if not isdir and filename.lower().endswith('.fbx'): if fbx_records is not None: - fbx_records[filename] = filehash + fbx_records[pathstr] = filehash if zip_type > 0 or os.path.isdir(process_path): for new_filename in os.listdir(process_path): From 682fb4a4e7be64ddc2b83808eaba8929e538c3a5 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 16:59:58 +0900 Subject: [PATCH 07/27] fix: add missing comma in add_booth_item SQL query --- booth_discord/booth_sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index 27d006e..d5d14f3 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -73,7 +73,7 @@ def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, changelog_show, archive_this, gift_item, - summary_this + summary_this, fbx_only ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) From 9ccd1979a089047bfd73274b2a387d02a5a6e025 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 17:11:19 +0900 Subject: [PATCH 08/27] =?UTF-8?q?fix:=20add=5Fbooth=5Faccount=20=EB=B0=8F?= =?UTF-8?q?=20add=5Fbooth=5Fitem=20=ED=95=A8=EC=88=98=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=B0=8F=20=EC=B6=A9=EB=8F=8C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth_sqlite.py | 113 +++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index d5d14f3..447d7ec 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -45,11 +45,27 @@ def __del__(self): def add_booth_account(self, session_cookie, discord_user_id): self.cursor.execute(''' - INSERT OR IGNORE INTO booth_accounts (session_cookie, discord_user_id) - VALUES (?, ?) - ''', (session_cookie, discord_user_id)) + SELECT discord_user_id FROM booth_accounts + WHERE session_cookie = ? + ''', (session_cookie,)) + owner = self.cursor.fetchone() + if owner and owner[0] != discord_user_id: + raise Exception("이미 다른 Discord 계정에 등록된 쿠키입니다.") + + existing_account = self.get_booth_account(discord_user_id) + if existing_account: + self.cursor.execute(''' + UPDATE booth_accounts + SET session_cookie = ? + WHERE discord_user_id = ? + ''', (session_cookie, discord_user_id)) + else: + self.cursor.execute(''' + INSERT INTO booth_accounts (session_cookie, discord_user_id) + VALUES (?, ?) + ''', (session_cookie, discord_user_id)) self.conn.commit() - return self.cursor.lastrowid + return self.get_booth_account(discord_user_id) def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this, fbx_only): # Moved import to be local to avoid dependency issues in booth_checker @@ -62,35 +78,39 @@ def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, # download_number_show True, changelog_show True, archive_this False if booth_account: booth_order_info = get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) - self.cursor.execute(''' - INSERT OR IGNORE INTO booth_items ( - booth_order_number, - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - download_number_show, - changelog_show, - archive_this, - gift_item, - summary_this, - fbx_only - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (booth_order_info[1], - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - True, - True, - False, - booth_order_info[0], - summary_this, - fbx_only)) - self.conn.commit() - self.add_discord_noti_channel(discord_channel_id, booth_order_info[1]) - return self.cursor.lastrowid + try: + self.cursor.execute(''' + INSERT INTO booth_items ( + booth_order_number, + booth_item_number, + discord_user_id, + item_name, + intent_encoding, + download_number_show, + changelog_show, + archive_this, + gift_item, + summary_this, + fbx_only + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (booth_order_info[1], + booth_item_number, + discord_user_id, + item_name, + intent_encoding, + True, + True, + False, + booth_order_info[0], + summary_this, + fbx_only)) + self.add_discord_noti_channel(discord_channel_id, booth_order_info[1]) + self.conn.commit() + except sqlite3.IntegrityError as exc: + self.conn.rollback() + raise Exception("아이템 등록 중 충돌이 발생했습니다.") from exc + return booth_order_info[1] else: raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") @@ -110,7 +130,7 @@ def del_booth_account(self, discord_user_id): DELETE FROM booth_accounts WHERE discord_user_id = ? ''', (discord_user_id,)) self.conn.commit() - return self.cursor.lastrowid + return self.cursor.rowcount except Exception as e: raise Exception(e) @@ -135,12 +155,14 @@ def del_booth_item(self, discord_user_id, booth_item_number): DELETE FROM booth_items WHERE booth_item_number = ? AND discord_user_id = ? ''', (booth_item_number, discord_user_id)) - self.conn.commit() + deleted_items = self.cursor.rowcount # 3. 조회된 booth_order_number를 사용하여 discord_noti_channels 테이블에서도 삭제합니다. - self.del_discord_noti_channel(booth_order_number) - return self.cursor.lastrowid + deleted_channels = self.del_discord_noti_channel(booth_order_number) + self.conn.commit() + return {'items_deleted': deleted_items, 'channels_deleted': deleted_channels} except Exception as e: + self.conn.rollback() raise Exception(e) else: raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") @@ -180,19 +202,24 @@ def list_booth_items(self, discord_user_id, discord_channel_id): def add_discord_noti_channel(self, discord_channel_id, booth_order_number): self.cursor.execute(''' - INSERT OR IGNORE INTO discord_noti_channels (discord_channel_id, booth_order_number) + SELECT 1 FROM discord_noti_channels + WHERE discord_channel_id = ? AND booth_order_number = ? + ''', (discord_channel_id, booth_order_number)) + if self.cursor.fetchone(): + return False + + self.cursor.execute(''' + INSERT INTO discord_noti_channels (discord_channel_id, booth_order_number) VALUES (?, ?) ''', (discord_channel_id, booth_order_number)) - self.conn.commit() - return self.cursor.lastrowid + return True def del_discord_noti_channel(self, booth_order_number): self.logger.debug(f"del_discord_noti_channel - booth_order_number : {booth_order_number}") # 추가된 로그 self.cursor.execute(''' DELETE FROM discord_noti_channels WHERE booth_order_number = ? ''', (booth_order_number,)) - self.conn.commit() - return self.cursor.lastrowid + return self.cursor.rowcount def update_discord_noti_channel(self, discord_user_id, discord_channel_id, booth_item_number): booth_order_number = self.get_booth_order_number(booth_item_number, discord_user_id) @@ -204,7 +231,7 @@ def update_discord_noti_channel(self, discord_user_id, discord_channel_id, booth WHERE booth_order_number = ? ''', (discord_channel_id, booth_order_number)) self.conn.commit() - return self.cursor.lastrowid + return self.cursor.rowcount def get_booth_order_number(self, booth_item_number, discord_user_id): self.cursor.execute(''' From bac87591fd877656c5a810b19a0c18921b31c75f Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 17:13:20 +0900 Subject: [PATCH 09/27] =?UTF-8?q?fix:=20=EC=99=B8=EB=9E=98=20=ED=82=A4=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20PRA?= =?UTF-8?q?GMA=20foreign=5Fkeys=3DON=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth_sqlite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index 447d7ec..a3ffafe 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -5,6 +5,7 @@ def __init__(self, db, logger): self.logger = logger self.conn = sqlite3.connect(db) self.conn.execute("PRAGMA journal_mode=WAL;") + self.conn.execute("PRAGMA foreign_keys=ON;") self.cursor = self.conn.cursor() self.cursor.execute(''' From e5d8fce233057b4eb7f3ed57fdcb221f8af1dabc Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 20:45:23 +0900 Subject: [PATCH 10/27] =?UTF-8?q?fix:=20config.json=EC=97=90=EC=84=9C=20se?= =?UTF-8?q?lenium=5Furl=EC=9D=84=20=EC=9D=BD=EC=96=B4=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index 5365467..40fc68e 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -17,6 +17,7 @@ def main(): with open("config.json") as file: config_json = json.load(file) discord_bot_token = config_json['discord_bot_token'] + selenium_url = config_json['selenium_url'] bot.run(discord_bot_token) if __name__ == "__main__": From cdaddce2ce187ceae1e5a073d22318b11b80daf2 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 21:03:53 +0900 Subject: [PATCH 11/27] =?UTF-8?q?refactor:=20main=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20=EB=B4=87=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/__main__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index 40fc68e..81355ae 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -12,12 +12,18 @@ logger = logging.getLogger(__name__) def main(): - booth_db = booth_sqlite.BoothSQLite('./version/db/booth.db', logger) - bot = booth_discord.DiscordBot(booth_db, logger) + # Load configuration with open("config.json") as file: config_json = json.load(file) + + # Read configuration values discord_bot_token = config_json['discord_bot_token'] - selenium_url = config_json['selenium_url'] + selenium_url = config_json['selenium_url'] + + # Initialize database and bot + booth_db = booth_sqlite.BoothSQLite('./version/db/booth.db', logger) + bot = booth_discord.DiscordBot(booth_db, logger) + selumin = booth.BoothCrawler(selenium_url) bot.run(discord_bot_token) if __name__ == "__main__": From 07e3802858b539d5236f3ff5ad3bfc6441ee3fe7 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 21:04:17 +0900 Subject: [PATCH 12/27] =?UTF-8?q?refactor:=20BoothCrawler=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20get=5Fbooth=5Forder=5Finfo=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth.py | 91 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/booth_discord/booth.py b/booth_discord/booth.py index 0107244..a206aa9 100644 --- a/booth_discord/booth.py +++ b/booth_discord/booth.py @@ -6,53 +6,52 @@ from selenium.webdriver.support import expected_conditions as EC from bs4 import BeautifulSoup -def get_booth_order_info(item_number, cookie): - chrome_options = Options() - chrome_options.add_argument("--headless") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument("--disable-dev-shm-usage") - - SELENIUM_SERVER_URL = "http://chrome:4444/wd/hub" - - driver = webdriver.Remote( - command_executor=SELENIUM_SERVER_URL, - options=chrome_options - ) - - driver.get(f"https://booth.pm/ko/items/{item_number}") - driver.add_cookie({"name": cookie[0], "value": cookie[1]}) - driver.refresh() - - try: - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "flex.desktop\\:flex-row.mobile\\:flex-col")) +class BoothCrawler(): + def __init__(self, selenium_url): + self.selenium_url = selenium_url + + def get_booth_order_info(self, item_number, cookie): + chrome_options = Options() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--disable-dev-shm-usage") + + driver = webdriver.Remote( + command_executor=self.selenium_url, + options=chrome_options ) - html = driver.page_source - soup = BeautifulSoup(html, "html.parser") - - product_div = soup.find("div", class_="flex desktop:flex-row mobile:flex-col") - if not product_div: - raise Exception("상품이 존재하지 않거나, 구매하지 않은 상품입니다.") + driver.get(f"https://booth.pm/ko/items/{item_number}") + driver.add_cookie({"name": cookie[0], "value": cookie[1]}) + driver.refresh() + + try: + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "flex.desktop\\:flex-row.mobile\\:flex-col")) + ) + + html = driver.page_source + soup = BeautifulSoup(html, "html.parser") + + product_div = soup.find("div", class_="flex desktop:flex-row mobile:flex-col") + if not product_div: + raise Exception("상품이 존재하지 않거나, 구매하지 않은 상품입니다.") + + order_page = product_div.find("a").get("href") + order_parse = parse_url(order_page) + return order_parse - order_page = product_div.find("a").get("href") - order_parse = parse_url(order_page) - return order_parse - - finally: - driver.quit() - -def parse_url(url): - # 정규식 정의 - pattern = r"https://(?:accounts\.)?booth\.pm/(orders|gifts)/([\w-]+)" - match = re.match(pattern, url) - - if match: - gift_flag = match.group(1) == "gifts" # gifts이면 True, orders이면 False - order_number = match.group(2) - return gift_flag, order_number - else: - raise ValueError("URL 형식이 잘못되었습니다.") - - + finally: + driver.quit() + def parse_url(url): + # 정규식 정의 + pattern = r"https://(?:accounts\.)?booth\.pm/(orders|gifts)/([\w-]+)" + match = re.match(pattern, url) + + if match: + gift_flag = match.group(1) == "gifts" # gifts이면 True, orders이면 False + order_number = match.group(2) + return gift_flag, order_number + else: + raise ValueError("URL 형식이 잘못되었습니다.") \ No newline at end of file From b44fc335cd1e4148313428d5d804468bd7d16cde Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 21:04:32 +0900 Subject: [PATCH 13/27] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth_sqlite.py | 182 +++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index a3ffafe..de8ce76 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -1,4 +1,5 @@ import sqlite3 +from contextlib import contextmanager, nullcontext class BoothSQLite(): def __init__(self, db, logger): @@ -7,6 +8,7 @@ def __init__(self, db, logger): self.conn.execute("PRAGMA journal_mode=WAL;") self.conn.execute("PRAGMA foreign_keys=ON;") self.cursor = self.conn.cursor() + self._transaction_depth = 0 self.cursor.execute(''' CREATE TABLE IF NOT EXISTS booth_accounts ( @@ -43,6 +45,21 @@ def __init__(self, db, logger): def __del__(self): self.conn.close() + + @contextmanager + def _transaction(self): + is_outermost = self._transaction_depth == 0 + self._transaction_depth += 1 + try: + yield + if is_outermost: + self.conn.commit() + except Exception: + if is_outermost: + self.conn.rollback() + raise + finally: + self._transaction_depth -= 1 def add_booth_account(self, session_cookie, discord_user_id): self.cursor.execute(''' @@ -54,18 +71,18 @@ def add_booth_account(self, session_cookie, discord_user_id): raise Exception("이미 다른 Discord 계정에 등록된 쿠키입니다.") existing_account = self.get_booth_account(discord_user_id) - if existing_account: - self.cursor.execute(''' - UPDATE booth_accounts - SET session_cookie = ? - WHERE discord_user_id = ? - ''', (session_cookie, discord_user_id)) - else: - self.cursor.execute(''' - INSERT INTO booth_accounts (session_cookie, discord_user_id) - VALUES (?, ?) - ''', (session_cookie, discord_user_id)) - self.conn.commit() + with self._transaction(): + if existing_account: + self.cursor.execute(''' + UPDATE booth_accounts + SET session_cookie = ? + WHERE discord_user_id = ? + ''', (session_cookie, discord_user_id)) + else: + self.cursor.execute(''' + INSERT INTO booth_accounts (session_cookie, discord_user_id) + VALUES (?, ?) + ''', (session_cookie, discord_user_id)) return self.get_booth_account(discord_user_id) def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this, fbx_only): @@ -75,41 +92,38 @@ def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, booth_account = self.get_booth_account(discord_user_id) if self.is_item_duplicate(booth_item_number, discord_user_id): raise Exception("이미 등록된 아이템입니다.") - # 서버에 부스 아이템 파일이 남지않도록 하드코딩 - # download_number_show True, changelog_show True, archive_this False if booth_account: booth_order_info = get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) try: - self.cursor.execute(''' - INSERT INTO booth_items ( - booth_order_number, - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - download_number_show, - changelog_show, - archive_this, - gift_item, - summary_this, - fbx_only - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (booth_order_info[1], - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - True, - True, - False, - booth_order_info[0], - summary_this, - fbx_only)) - self.add_discord_noti_channel(discord_channel_id, booth_order_info[1]) - self.conn.commit() + with self._transaction(): + self.cursor.execute(''' + INSERT INTO booth_items ( + booth_order_number, + booth_item_number, + discord_user_id, + item_name, + intent_encoding, + download_number_show, + changelog_show, + archive_this, + gift_item, + summary_this, + fbx_only + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (booth_order_info[1], # booth_order_number + booth_item_number, + discord_user_id, + item_name, + intent_encoding, + True, # download_number_show + True, # changelog_show + False, # archive_this + booth_order_info[0], # gift_item + summary_this, + fbx_only)) + self.add_discord_noti_channel(discord_channel_id, booth_order_info[1], use_transaction=False) except sqlite3.IntegrityError as exc: - self.conn.rollback() raise Exception("아이템 등록 중 충돌이 발생했습니다.") from exc return booth_order_info[1] else: @@ -127,11 +141,12 @@ def del_booth_account(self, discord_user_id): result = self.cursor.fetchone() if result[0] == 1: raise Exception("BOOTH 아이템이 등록되어 있습니다. 먼저 아이템을 삭제해주세요.") - self.cursor.execute(''' - DELETE FROM booth_accounts WHERE discord_user_id = ? - ''', (discord_user_id,)) - self.conn.commit() - return self.cursor.rowcount + with self._transaction(): + self.cursor.execute(''' + DELETE FROM booth_accounts WHERE discord_user_id = ? + ''', (discord_user_id,)) + deleted_accounts = self.cursor.rowcount + return deleted_accounts except Exception as e: raise Exception(e) @@ -152,18 +167,17 @@ def del_booth_item(self, discord_user_id, booth_item_number): self.logger.debug(f"booth_order_number: {booth_order_number}") # 2. booth_items 테이블에서 해당 행을 삭제합니다. - self.cursor.execute(''' - DELETE FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? - ''', (booth_item_number, discord_user_id)) - deleted_items = self.cursor.rowcount - - # 3. 조회된 booth_order_number를 사용하여 discord_noti_channels 테이블에서도 삭제합니다. - deleted_channels = self.del_discord_noti_channel(booth_order_number) - self.conn.commit() + with self._transaction(): + self.cursor.execute(''' + DELETE FROM booth_items + WHERE booth_item_number = ? AND discord_user_id = ? + ''', (booth_item_number, discord_user_id)) + deleted_items = self.cursor.rowcount + + # 3. 조회된 booth_order_number를 사용하여 discord_noti_channels 테이블에서도 삭제합니다. + deleted_channels = self.del_discord_noti_channel(booth_order_number, use_transaction=False) return {'items_deleted': deleted_items, 'channels_deleted': deleted_channels} except Exception as e: - self.conn.rollback() raise Exception(e) else: raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") @@ -201,38 +215,42 @@ def list_booth_items(self, discord_user_id, discord_channel_id): else: raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") - def add_discord_noti_channel(self, discord_channel_id, booth_order_number): - self.cursor.execute(''' - SELECT 1 FROM discord_noti_channels - WHERE discord_channel_id = ? AND booth_order_number = ? - ''', (discord_channel_id, booth_order_number)) - if self.cursor.fetchone(): - return False + def add_discord_noti_channel(self, discord_channel_id, booth_order_number, use_transaction=True): + tx_context = self._transaction() if use_transaction else nullcontext() + with tx_context: + self.cursor.execute(''' + SELECT 1 FROM discord_noti_channels + WHERE discord_channel_id = ? AND booth_order_number = ? + ''', (discord_channel_id, booth_order_number)) + if self.cursor.fetchone(): + return False - self.cursor.execute(''' - INSERT INTO discord_noti_channels (discord_channel_id, booth_order_number) - VALUES (?, ?) - ''', (discord_channel_id, booth_order_number)) - return True + self.cursor.execute(''' + INSERT INTO discord_noti_channels (discord_channel_id, booth_order_number) + VALUES (?, ?) + ''', (discord_channel_id, booth_order_number)) + return True - def del_discord_noti_channel(self, booth_order_number): + def del_discord_noti_channel(self, booth_order_number, use_transaction=True): self.logger.debug(f"del_discord_noti_channel - booth_order_number : {booth_order_number}") # 추가된 로그 - self.cursor.execute(''' - DELETE FROM discord_noti_channels WHERE booth_order_number = ? - ''', (booth_order_number,)) - return self.cursor.rowcount + tx_context = self._transaction() if use_transaction else nullcontext() + with tx_context: + self.cursor.execute(''' + DELETE FROM discord_noti_channels WHERE booth_order_number = ? + ''', (booth_order_number,)) + return self.cursor.rowcount def update_discord_noti_channel(self, discord_user_id, discord_channel_id, booth_item_number): booth_order_number = self.get_booth_order_number(booth_item_number, discord_user_id) if not booth_order_number: raise Exception("Item not found") - self.cursor.execute(''' - UPDATE discord_noti_channels - SET discord_channel_id = ? - WHERE booth_order_number = ? - ''', (discord_channel_id, booth_order_number)) - self.conn.commit() - return self.cursor.rowcount + with self._transaction(): + self.cursor.execute(''' + UPDATE discord_noti_channels + SET discord_channel_id = ? + WHERE booth_order_number = ? + ''', (discord_channel_id, booth_order_number)) + return self.cursor.rowcount def get_booth_order_number(self, booth_item_number, discord_user_id): self.cursor.execute(''' From cdc0d181820bab9ed2867674953ba3c25ab1d903 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 21 Oct 2025 21:32:00 +0900 Subject: [PATCH 14/27] =?UTF-8?q?refactor:=20BoothCrawler=20=EB=B0=8F=20Bo?= =?UTF-8?q?othSQLite=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=9D=98=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/__main__.py | 7 ++++--- booth_discord/booth.py | 4 ++-- booth_discord/booth_sqlite.py | 14 +++++--------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index 81355ae..df5ea59 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -1,6 +1,7 @@ # main.py import json import logging +import booth as booth_module import booth_sqlite import booth_discord @@ -21,10 +22,10 @@ def main(): selenium_url = config_json['selenium_url'] # Initialize database and bot - booth_db = booth_sqlite.BoothSQLite('./version/db/booth.db', logger) + booth_crawler = booth_module.BoothCrawler(selenium_url) + booth_db = booth_sqlite.BoothSQLite('./version/db/booth.db', booth_crawler, logger) bot = booth_discord.DiscordBot(booth_db, logger) - selumin = booth.BoothCrawler(selenium_url) bot.run(discord_bot_token) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/booth_discord/booth.py b/booth_discord/booth.py index a206aa9..fded0a5 100644 --- a/booth_discord/booth.py +++ b/booth_discord/booth.py @@ -38,13 +38,13 @@ def get_booth_order_info(self, item_number, cookie): raise Exception("상품이 존재하지 않거나, 구매하지 않은 상품입니다.") order_page = product_div.find("a").get("href") - order_parse = parse_url(order_page) + order_parse = self.parse_url(order_page) return order_parse finally: driver.quit() - def parse_url(url): + def parse_url(self, url): # 정규식 정의 pattern = r"https://(?:accounts\.)?booth\.pm/(orders|gifts)/([\w-]+)" match = re.match(pattern, url) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index de8ce76..e0a86e8 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -2,8 +2,9 @@ from contextlib import contextmanager, nullcontext class BoothSQLite(): - def __init__(self, db, logger): + def __init__(self, db, booth, logger): self.logger = logger + self.booth = booth self.conn = sqlite3.connect(db) self.conn.execute("PRAGMA journal_mode=WAL;") self.conn.execute("PRAGMA foreign_keys=ON;") @@ -86,14 +87,11 @@ def add_booth_account(self, session_cookie, discord_user_id): return self.get_booth_account(discord_user_id) def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this, fbx_only): - # Moved import to be local to avoid dependency issues in booth_checker - from booth import get_booth_order_info - booth_account = self.get_booth_account(discord_user_id) if self.is_item_duplicate(booth_item_number, discord_user_id): raise Exception("이미 등록된 아이템입니다.") if booth_account: - booth_order_info = get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) + booth_order_info = self.booth.get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) try: with self._transaction(): self.cursor.execute(''' @@ -166,16 +164,14 @@ def del_booth_item(self, discord_user_id, booth_item_number): booth_order_number = result[0] self.logger.debug(f"booth_order_number: {booth_order_number}") - # 2. booth_items 테이블에서 해당 행을 삭제합니다. + # 2. 조회된 booth_order_number를 사용하여 discord_noti_channels 테이블에서 먼저 삭제합니다. with self._transaction(): + deleted_channels = self.del_discord_noti_channel(booth_order_number, use_transaction=False) self.cursor.execute(''' DELETE FROM booth_items WHERE booth_item_number = ? AND discord_user_id = ? ''', (booth_item_number, discord_user_id)) deleted_items = self.cursor.rowcount - - # 3. 조회된 booth_order_number를 사용하여 discord_noti_channels 테이블에서도 삭제합니다. - deleted_channels = self.del_discord_noti_channel(booth_order_number, use_transaction=False) return {'items_deleted': deleted_items, 'channels_deleted': deleted_channels} except Exception as e: raise Exception(e) From 5c1cec7e0a8a8d4fc6104f1b38211450ce44027b Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Wed, 22 Oct 2025 10:08:10 +0900 Subject: [PATCH 15/27] =?UTF-8?q?refactor:=20PostgreSQL=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 5 +- booth_checker/booth_sqlite.py | 44 ++++++- booth_discord/__main__.py | 3 +- booth_discord/booth_sqlite.py | 177 +++++++++++++++----------- config-sample.json | 10 +- docker/booth-checker/requirements.txt | 3 +- docker/booth-discord/requirements.txt | 3 +- 7 files changed, 157 insertions(+), 88 deletions(-) diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index 4ba3d0d..1595614 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -819,7 +819,8 @@ def strftime_now(): createFolder("./download") createFolder("./process") - booth_db = booth_sqlite.BoothSQLite('./version/db/booth.db') + postgres_config = dict(config_json['postgres']) + booth_db = booth_sqlite.BoothSQLite(postgres_config) if not DRY_RUN: # booth_discord 컨테이너 시작 대기 @@ -865,4 +866,4 @@ def strftime_now(): # 갱신 대기 logger.info("BoothChecker cycle finished") logger.info(f"Next check will be at {datetime.now() + timedelta(seconds=refresh_interval)}") - sleep(refresh_interval) \ No newline at end of file + sleep(refresh_interval) diff --git a/booth_checker/booth_sqlite.py b/booth_checker/booth_sqlite.py index 1685504..c376fb2 100644 --- a/booth_checker/booth_sqlite.py +++ b/booth_checker/booth_sqlite.py @@ -1,13 +1,26 @@ -import sqlite3 +import logging +import time +import psycopg -class BoothSQLite(): - def __init__(self, db): - self.conn = sqlite3.connect(db) - self.conn.execute("PRAGMA journal_mode=WAL;") + +logger = logging.getLogger(__name__) + + +class BoothPostgres: + def __init__(self, conn_params, retries=5, delay=2): + self.conn = self._connect_with_retry(conn_params, retries, delay) + self.conn.autocommit = True self.cursor = self.conn.cursor() def __del__(self): - self.conn.close() + try: + self.cursor.close() + except Exception: + pass + try: + self.conn.close() + except Exception: + pass def get_booth_items(self): self.cursor.execute(''' @@ -31,3 +44,22 @@ def get_booth_items(self): ON items.booth_order_number = channels.booth_order_number ''') return self.cursor.fetchall() + + def _connect_with_retry(self, conn_params, retries=5, delay=2): + for attempt in range(1, retries + 1): + try: + return psycopg.connect(**conn_params) + except psycopg.OperationalError as exc: + if attempt == retries: + logger.error("PostgreSQL 연결에 실패했습니다. 설정을 확인해주세요.") + raise + logger.warning( + "PostgreSQL 연결 재시도 %s/%s: %s", + attempt, + retries, + exc, + ) + time.sleep(delay) + + +BoothSQLite = BoothPostgres diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index df5ea59..026b8b0 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -23,7 +23,8 @@ def main(): # Initialize database and bot booth_crawler = booth_module.BoothCrawler(selenium_url) - booth_db = booth_sqlite.BoothSQLite('./version/db/booth.db', booth_crawler, logger) + postgres_config = dict(config_json['postgres']) + booth_db = booth_sqlite.BoothSQLite(postgres_config, booth_crawler, logger) bot = booth_discord.DiscordBot(booth_db, logger) bot.run(discord_bot_token) diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sqlite.py index e0a86e8..1faf340 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sqlite.py @@ -1,71 +1,93 @@ -import sqlite3 +import time +import psycopg +from psycopg import errors as pg_errors from contextlib import contextmanager, nullcontext -class BoothSQLite(): - def __init__(self, db, booth, logger): + +class BoothPostgres: + def __init__(self, conn_params, booth, logger): self.logger = logger self.booth = booth - self.conn = sqlite3.connect(db) - self.conn.execute("PRAGMA journal_mode=WAL;") - self.conn.execute("PRAGMA foreign_keys=ON;") - self.cursor = self.conn.cursor() + self.conn = self._connect_with_retry(conn_params) + self.conn.autocommit = True self._transaction_depth = 0 - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS booth_accounts ( - session_cookie TEXT UNIQUE, - discord_user_id INTEGER PRIMARY KEY - ) - ''') + with self.conn.transaction(): + with self.conn.cursor() as cursor: + cursor.execute(''' + CREATE TABLE IF NOT EXISTS booth_accounts ( + session_cookie TEXT UNIQUE, + discord_user_id BIGINT PRIMARY KEY + ) + ''') - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS booth_items ( - booth_order_number TEXT PRIMARY KEY, - booth_item_number TEXT, - discord_user_id INTEGER, - item_name TEXT, - intent_encoding TEXT, - download_number_show BOOLEAN, - changelog_show BOOLEAN, - archive_this BOOLEAN, - gift_item BOOLEAN, - summary_this BOOLEAN, - fbx_only BOOLEAN, - FOREIGN KEY(discord_user_id) REFERENCES booth_accounts(discord_user_id) - ) - ''') - - self.cursor.execute(''' - CREATE TABLE IF NOT EXISTS discord_noti_channels ( - discord_channel_id INTEGER, - booth_order_number TEXT, - UNIQUE(discord_channel_id, booth_order_number), - FOREIGN KEY(booth_order_number) REFERENCES booth_items(booth_order_number) - ) - ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS booth_items ( + booth_order_number TEXT PRIMARY KEY, + booth_item_number TEXT, + discord_user_id BIGINT, + item_name TEXT, + intent_encoding TEXT, + download_number_show BOOLEAN, + changelog_show BOOLEAN, + archive_this BOOLEAN, + gift_item BOOLEAN, + summary_this BOOLEAN, + fbx_only BOOLEAN, + FOREIGN KEY(discord_user_id) REFERENCES booth_accounts(discord_user_id) + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS discord_noti_channels ( + discord_channel_id BIGINT, + booth_order_number TEXT, + UNIQUE(discord_channel_id, booth_order_number), + FOREIGN KEY(booth_order_number) REFERENCES booth_items(booth_order_number) + ) + ''') + self.cursor = self.conn.cursor() def __del__(self): - self.conn.close() + try: + self.cursor.close() + except Exception: + pass + try: + self.conn.close() + except Exception: + pass + + def _connect_with_retry(self, conn_params, retries=5, delay=2): + for attempt in range(1, retries + 1): + try: + return psycopg.connect(**conn_params) + except psycopg.OperationalError as exc: + if attempt == retries: + self.logger.error("PostgreSQL 연결에 실패했습니다. 설정을 확인해주세요.") + raise + self.logger.warning( + "PostgreSQL 연결 재시도 %s/%s: %s", + attempt, + retries, + exc, + ) + time.sleep(delay) @contextmanager def _transaction(self): - is_outermost = self._transaction_depth == 0 + savepoint = self._transaction_depth > 0 self._transaction_depth += 1 try: - yield - if is_outermost: - self.conn.commit() - except Exception: - if is_outermost: - self.conn.rollback() - raise + with self.conn.transaction(savepoint=savepoint): + yield finally: self._transaction_depth -= 1 def add_booth_account(self, session_cookie, discord_user_id): self.cursor.execute(''' SELECT discord_user_id FROM booth_accounts - WHERE session_cookie = ? + WHERE session_cookie = %s ''', (session_cookie,)) owner = self.cursor.fetchone() if owner and owner[0] != discord_user_id: @@ -76,17 +98,17 @@ def add_booth_account(self, session_cookie, discord_user_id): if existing_account: self.cursor.execute(''' UPDATE booth_accounts - SET session_cookie = ? - WHERE discord_user_id = ? + SET session_cookie = %s + WHERE discord_user_id = %s ''', (session_cookie, discord_user_id)) else: self.cursor.execute(''' INSERT INTO booth_accounts (session_cookie, discord_user_id) - VALUES (?, ?) + VALUES (%s, %s) ''', (session_cookie, discord_user_id)) return self.get_booth_account(discord_user_id) - def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding,summary_this, fbx_only): + def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding, summary_this, fbx_only): booth_account = self.get_booth_account(discord_user_id) if self.is_item_duplicate(booth_item_number, discord_user_id): raise Exception("이미 등록된 아이템입니다.") @@ -108,20 +130,20 @@ def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, summary_this, fbx_only ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ''', (booth_order_info[1], # booth_order_number booth_item_number, discord_user_id, item_name, intent_encoding, True, # download_number_show - True, # changelog_show + True, # changelog_show False, # archive_this booth_order_info[0], # gift_item summary_this, fbx_only)) self.add_discord_noti_channel(discord_channel_id, booth_order_info[1], use_transaction=False) - except sqlite3.IntegrityError as exc: + except pg_errors.IntegrityError as exc: raise Exception("아이템 등록 중 충돌이 발생했습니다.") from exc return booth_order_info[1] else: @@ -131,9 +153,9 @@ def del_booth_account(self, discord_user_id): try: self.cursor.execute(''' SELECT EXISTS ( - SELECT 1 - FROM booth_items - WHERE discord_user_id = ? + SELECT 1 + FROM booth_items + WHERE discord_user_id = %s ); ''', (discord_user_id,)) result = self.cursor.fetchone() @@ -141,7 +163,7 @@ def del_booth_account(self, discord_user_id): raise Exception("BOOTH 아이템이 등록되어 있습니다. 먼저 아이템을 삭제해주세요.") with self._transaction(): self.cursor.execute(''' - DELETE FROM booth_accounts WHERE discord_user_id = ? + DELETE FROM booth_accounts WHERE discord_user_id = %s ''', (discord_user_id,)) deleted_accounts = self.cursor.rowcount return deleted_accounts @@ -154,8 +176,8 @@ def del_booth_item(self, discord_user_id, booth_item_number): try: # 1. 삭제할 행의 booth_order_number를 먼저 조회합니다. self.cursor.execute(''' - SELECT booth_order_number FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? + SELECT booth_order_number FROM booth_items + WHERE booth_item_number = %s AND discord_user_id = %s ''', (booth_item_number, discord_user_id)) result = self.cursor.fetchone() if not result: @@ -168,8 +190,8 @@ def del_booth_item(self, discord_user_id, booth_item_number): with self._transaction(): deleted_channels = self.del_discord_noti_channel(booth_order_number, use_transaction=False) self.cursor.execute(''' - DELETE FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? + DELETE FROM booth_items + WHERE booth_item_number = %s AND discord_user_id = %s ''', (booth_item_number, discord_user_id)) deleted_items = self.cursor.rowcount return {'items_deleted': deleted_items, 'channels_deleted': deleted_channels} @@ -181,7 +203,7 @@ def del_booth_item(self, discord_user_id, booth_item_number): def get_booth_account(self, discord_user_id): self.cursor.execute(''' SELECT * FROM booth_accounts - WHERE discord_user_id = ? + WHERE discord_user_id = %s ''', (discord_user_id,)) result = self.cursor.fetchone() if result: @@ -191,7 +213,7 @@ def get_booth_account(self, discord_user_id): def is_item_duplicate(self, booth_item_number, discord_user_id): self.cursor.execute(''' SELECT * FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? + WHERE booth_item_number = %s AND discord_user_id = %s ''', (booth_item_number, discord_user_id)) result = self.cursor.fetchone() return bool(result) @@ -200,12 +222,12 @@ def list_booth_items(self, discord_user_id, discord_channel_id): booth_account = self.get_booth_account(discord_user_id) if booth_account: self.cursor.execute(''' - SELECT bi.booth_item_number + SELECT bi.booth_item_number FROM booth_items bi - JOIN discord_noti_channels dnc + JOIN discord_noti_channels dnc ON bi.booth_order_number = dnc.booth_order_number - WHERE bi.discord_user_id = ? - AND dnc.discord_channel_id = ?; + WHERE bi.discord_user_id = %s + AND dnc.discord_channel_id = %s; ''', (discord_user_id, discord_channel_id)) return self.cursor.fetchall() else: @@ -216,14 +238,14 @@ def add_discord_noti_channel(self, discord_channel_id, booth_order_number, use_t with tx_context: self.cursor.execute(''' SELECT 1 FROM discord_noti_channels - WHERE discord_channel_id = ? AND booth_order_number = ? + WHERE discord_channel_id = %s AND booth_order_number = %s ''', (discord_channel_id, booth_order_number)) if self.cursor.fetchone(): return False self.cursor.execute(''' INSERT INTO discord_noti_channels (discord_channel_id, booth_order_number) - VALUES (?, ?) + VALUES (%s, %s) ''', (discord_channel_id, booth_order_number)) return True @@ -232,7 +254,7 @@ def del_discord_noti_channel(self, booth_order_number, use_transaction=True): tx_context = self._transaction() if use_transaction else nullcontext() with tx_context: self.cursor.execute(''' - DELETE FROM discord_noti_channels WHERE booth_order_number = ? + DELETE FROM discord_noti_channels WHERE booth_order_number = %s ''', (booth_order_number,)) return self.cursor.rowcount @@ -243,20 +265,23 @@ def update_discord_noti_channel(self, discord_user_id, discord_channel_id, booth with self._transaction(): self.cursor.execute(''' UPDATE discord_noti_channels - SET discord_channel_id = ? - WHERE booth_order_number = ? + SET discord_channel_id = %s + WHERE booth_order_number = %s ''', (discord_channel_id, booth_order_number)) return self.cursor.rowcount def get_booth_order_number(self, booth_item_number, discord_user_id): self.cursor.execute(''' SELECT booth_order_number FROM booth_items - WHERE booth_item_number = ? AND discord_user_id = ? + WHERE booth_item_number = %s AND discord_user_id = %s ''', (booth_item_number, discord_user_id)) result = self.cursor.fetchone() return result[0] if result else None def get_booth_item_count(self, discord_user_id): - self.cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = ?', (discord_user_id,)) + self.cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = %s', (discord_user_id,)) result = self.cursor.fetchone() return result[0] if result else 0 + + +BoothSQLite = BoothPostgres diff --git a/config-sample.json b/config-sample.json index 726839d..b8523f2 100644 --- a/config-sample.json +++ b/config-sample.json @@ -1,6 +1,14 @@ { "dry_run": false, "refresh_interval": 600, + "selenium_url": "http://localhost:4444/wd/hub", + "postgres": { + "host": "postgres", + "port": 5432, + "dbname": "booth", + "user": "booth_user", + "password": "booth_password" + }, "discord_api_url": "http://booth-discord:5000", "discord_bot_token": "", "gemini_api_key": "", @@ -12,4 +20,4 @@ "access_key_id": "", "secret_access_key": "" } -} \ No newline at end of file +} diff --git a/docker/booth-checker/requirements.txt b/docker/booth-checker/requirements.txt index 606c9f4..37fba8c 100644 --- a/docker/booth-checker/requirements.txt +++ b/docker/booth-checker/requirements.txt @@ -6,4 +6,5 @@ unitypackage_extractor pytz boto3 jinja2 -google-genai \ No newline at end of file +google-genai +psycopg[binary] diff --git a/docker/booth-discord/requirements.txt b/docker/booth-discord/requirements.txt index c49e9a7..ffa4dbb 100644 --- a/docker/booth-discord/requirements.txt +++ b/docker/booth-discord/requirements.txt @@ -2,4 +2,5 @@ discord.py pytz quart selenium -beautifulsoup4 \ No newline at end of file +beautifulsoup4 +psycopg[binary] From 56cfd40cf93410c02537f8eba3d3e937cde60ab7 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Wed, 22 Oct 2025 10:31:57 +0900 Subject: [PATCH 16/27] =?UTF-8?q?refactor:=20booth=5Fsqlite.py=20->=20boot?= =?UTF-8?q?h=5Fsql.py=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/{booth_sqlite.py => booth_sql.py} | 5 +---- booth_discord/{booth_sqlite.py => booth_sql.py} | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) rename booth_checker/{booth_sqlite.py => booth_sql.py} (96%) rename booth_discord/{booth_sqlite.py => booth_sql.py} (99%) diff --git a/booth_checker/booth_sqlite.py b/booth_checker/booth_sql.py similarity index 96% rename from booth_checker/booth_sqlite.py rename to booth_checker/booth_sql.py index c376fb2..d4a1291 100644 --- a/booth_checker/booth_sqlite.py +++ b/booth_checker/booth_sql.py @@ -59,7 +59,4 @@ def _connect_with_retry(self, conn_params, retries=5, delay=2): retries, exc, ) - time.sleep(delay) - - -BoothSQLite = BoothPostgres + time.sleep(delay) \ No newline at end of file diff --git a/booth_discord/booth_sqlite.py b/booth_discord/booth_sql.py similarity index 99% rename from booth_discord/booth_sqlite.py rename to booth_discord/booth_sql.py index 1faf340..80b413d 100644 --- a/booth_discord/booth_sqlite.py +++ b/booth_discord/booth_sql.py @@ -281,7 +281,4 @@ def get_booth_order_number(self, booth_item_number, discord_user_id): def get_booth_item_count(self, discord_user_id): self.cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = %s', (discord_user_id,)) result = self.cursor.fetchone() - return result[0] if result else 0 - - -BoothSQLite = BoothPostgres + return result[0] if result else 0 \ No newline at end of file From 8f9dd75dc1ddd078022a1bc00a1947c6ee02f6a8 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Wed, 22 Oct 2025 10:32:45 +0900 Subject: [PATCH 17/27] =?UTF-8?q?refactor:=20booth=5Fsqlite=EC=97=90?= =?UTF-8?q?=EC=84=9C=20booth=5Fsql=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B3=80=EA=B2=BD=EB=90=9C=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=AA=85=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 4 ++-- booth_discord/__main__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index 1595614..54426e6 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -18,7 +18,7 @@ from shared import * import booth -import booth_sqlite +import booth_sql import cloudflare import llm_summary @@ -820,7 +820,7 @@ def strftime_now(): createFolder("./process") postgres_config = dict(config_json['postgres']) - booth_db = booth_sqlite.BoothSQLite(postgres_config) + booth_db = booth_sql.BoothPostgres(postgres_config) if not DRY_RUN: # booth_discord 컨테이너 시작 대기 diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index 026b8b0..a5ca5a7 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -2,7 +2,7 @@ import json import logging import booth as booth_module -import booth_sqlite +import booth_sql import booth_discord logging.basicConfig( @@ -24,7 +24,7 @@ def main(): # Initialize database and bot booth_crawler = booth_module.BoothCrawler(selenium_url) postgres_config = dict(config_json['postgres']) - booth_db = booth_sqlite.BoothSQLite(postgres_config, booth_crawler, logger) + booth_db = booth_sql.BoothPostgres(postgres_config, booth_crawler, logger) bot = booth_discord.DiscordBot(booth_db, logger) bot.run(discord_bot_token) From 2684cab44aef7c33fcd4f470e215bc0758423b47 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Wed, 22 Oct 2025 21:02:57 +0900 Subject: [PATCH 18/27] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth_sql.py | 320 +++++++++++++++++++------------------ 1 file changed, 168 insertions(+), 152 deletions(-) diff --git a/booth_discord/booth_sql.py b/booth_discord/booth_sql.py index 80b413d..130f571 100644 --- a/booth_discord/booth_sql.py +++ b/booth_discord/booth_sql.py @@ -1,7 +1,8 @@ import time +from contextlib import contextmanager + import psycopg from psycopg import errors as pg_errors -from contextlib import contextmanager, nullcontext class BoothPostgres: @@ -10,7 +11,6 @@ def __init__(self, conn_params, booth, logger): self.booth = booth self.conn = self._connect_with_retry(conn_params) self.conn.autocommit = True - self._transaction_depth = 0 with self.conn.transaction(): with self.conn.cursor() as cursor: @@ -46,13 +46,8 @@ def __init__(self, conn_params, booth, logger): FOREIGN KEY(booth_order_number) REFERENCES booth_items(booth_order_number) ) ''') - self.cursor = self.conn.cursor() def __del__(self): - try: - self.cursor.close() - except Exception: - pass try: self.conn.close() except Exception: @@ -76,152 +71,159 @@ def _connect_with_retry(self, conn_params, retries=5, delay=2): @contextmanager def _transaction(self): - savepoint = self._transaction_depth > 0 - self._transaction_depth += 1 - try: - with self.conn.transaction(savepoint=savepoint): - yield - finally: - self._transaction_depth -= 1 - + with self.conn.transaction(): + with self.conn.cursor() as cursor: + yield cursor + def add_booth_account(self, session_cookie, discord_user_id): - self.cursor.execute(''' - SELECT discord_user_id FROM booth_accounts - WHERE session_cookie = %s - ''', (session_cookie,)) - owner = self.cursor.fetchone() + with self.conn.cursor() as cursor: + cursor.execute(''' + SELECT discord_user_id FROM booth_accounts + WHERE session_cookie = %s + ''', (session_cookie,)) + owner = cursor.fetchone() if owner and owner[0] != discord_user_id: raise Exception("이미 다른 Discord 계정에 등록된 쿠키입니다.") existing_account = self.get_booth_account(discord_user_id) - with self._transaction(): + with self._transaction() as cursor: if existing_account: - self.cursor.execute(''' + cursor.execute(''' UPDATE booth_accounts SET session_cookie = %s WHERE discord_user_id = %s ''', (session_cookie, discord_user_id)) else: - self.cursor.execute(''' + cursor.execute(''' INSERT INTO booth_accounts (session_cookie, discord_user_id) VALUES (%s, %s) ''', (session_cookie, discord_user_id)) return self.get_booth_account(discord_user_id) - + def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding, summary_this, fbx_only): booth_account = self.get_booth_account(discord_user_id) if self.is_item_duplicate(booth_item_number, discord_user_id): raise Exception("이미 등록된 아이템입니다.") - if booth_account: - booth_order_info = self.booth.get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) - try: - with self._transaction(): - self.cursor.execute(''' - INSERT INTO booth_items ( - booth_order_number, - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - download_number_show, - changelog_show, - archive_this, - gift_item, - summary_this, - fbx_only - ) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - ''', (booth_order_info[1], # booth_order_number - booth_item_number, - discord_user_id, - item_name, - intent_encoding, - True, # download_number_show - True, # changelog_show - False, # archive_this - booth_order_info[0], # gift_item - summary_this, - fbx_only)) - self.add_discord_noti_channel(discord_channel_id, booth_order_info[1], use_transaction=False) - except pg_errors.IntegrityError as exc: - raise Exception("아이템 등록 중 충돌이 발생했습니다.") from exc - return booth_order_info[1] - else: + if not booth_account: raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") + booth_order_info = self.booth.get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) + try: + with self._transaction() as cursor: + cursor.execute(''' + INSERT INTO booth_items ( + booth_order_number, + booth_item_number, + discord_user_id, + item_name, + intent_encoding, + download_number_show, + changelog_show, + archive_this, + gift_item, + summary_this, + fbx_only + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ''', (booth_order_info[1], # booth_order_number + booth_item_number, + discord_user_id, + item_name, + intent_encoding, + True, # download_number_show + True, # changelog_show + False, # archive_this + booth_order_info[0], # gift_item + summary_this, + fbx_only)) + self.add_discord_noti_channel( + discord_channel_id, + booth_order_info[1], + use_transaction=False, + cursor=cursor, + ) + except pg_errors.IntegrityError as exc: + raise Exception("아이템 등록 중 충돌이 발생했습니다.") from exc + return booth_order_info[1] + def del_booth_account(self, discord_user_id): try: - self.cursor.execute(''' - SELECT EXISTS ( - SELECT 1 - FROM booth_items - WHERE discord_user_id = %s - ); - ''', (discord_user_id,)) - result = self.cursor.fetchone() + with self.conn.cursor() as cursor: + cursor.execute(''' + SELECT EXISTS ( + SELECT 1 + FROM booth_items + WHERE discord_user_id = %s + ); + ''', (discord_user_id,)) + result = cursor.fetchone() if result[0] == 1: raise Exception("BOOTH 아이템이 등록되어 있습니다. 먼저 아이템을 삭제해주세요.") - with self._transaction(): - self.cursor.execute(''' + with self._transaction() as cursor: + cursor.execute(''' DELETE FROM booth_accounts WHERE discord_user_id = %s ''', (discord_user_id,)) - deleted_accounts = self.cursor.rowcount - return deleted_accounts - except Exception as e: - raise Exception(e) - + return cursor.rowcount + except Exception as exc: + raise Exception(exc) + def del_booth_item(self, discord_user_id, booth_item_number): booth_account = self.get_booth_account(discord_user_id) - if booth_account: - try: - # 1. 삭제할 행의 booth_order_number를 먼저 조회합니다. - self.cursor.execute(''' + if not booth_account: + raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") + + try: + with self.conn.cursor() as cursor: + cursor.execute(''' SELECT booth_order_number FROM booth_items WHERE booth_item_number = %s AND discord_user_id = %s ''', (booth_item_number, discord_user_id)) - result = self.cursor.fetchone() - if not result: - raise Exception(f"Item {booth_item_number} not found for user {discord_user_id}") - - booth_order_number = result[0] - self.logger.debug(f"booth_order_number: {booth_order_number}") + result = cursor.fetchone() + if not result: + raise Exception(f"Item {booth_item_number} not found for user {discord_user_id}") + + booth_order_number = result[0] + self.logger.debug("booth_order_number: %s", booth_order_number) + + with self._transaction() as cursor: + deleted_channels = self.del_discord_noti_channel( + booth_order_number, + use_transaction=False, + cursor=cursor, + ) + cursor.execute(''' + DELETE FROM booth_items + WHERE booth_item_number = %s AND discord_user_id = %s + ''', (booth_item_number, discord_user_id)) + deleted_items = cursor.rowcount + return {'items_deleted': deleted_items, 'channels_deleted': deleted_channels} + except Exception as exc: + raise Exception(exc) - # 2. 조회된 booth_order_number를 사용하여 discord_noti_channels 테이블에서 먼저 삭제합니다. - with self._transaction(): - deleted_channels = self.del_discord_noti_channel(booth_order_number, use_transaction=False) - self.cursor.execute(''' - DELETE FROM booth_items - WHERE booth_item_number = %s AND discord_user_id = %s - ''', (booth_item_number, discord_user_id)) - deleted_items = self.cursor.rowcount - return {'items_deleted': deleted_items, 'channels_deleted': deleted_channels} - except Exception as e: - raise Exception(e) - else: - raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") - def get_booth_account(self, discord_user_id): - self.cursor.execute(''' - SELECT * FROM booth_accounts - WHERE discord_user_id = %s - ''', (discord_user_id,)) - result = self.cursor.fetchone() - if result: - return result - return None + with self.conn.cursor() as cursor: + cursor.execute(''' + SELECT * FROM booth_accounts + WHERE discord_user_id = %s + ''', (discord_user_id,)) + result = cursor.fetchone() + return result if result else None def is_item_duplicate(self, booth_item_number, discord_user_id): - self.cursor.execute(''' - SELECT * FROM booth_items - WHERE booth_item_number = %s AND discord_user_id = %s - ''', (booth_item_number, discord_user_id)) - result = self.cursor.fetchone() - return bool(result) - + with self.conn.cursor() as cursor: + cursor.execute(''' + SELECT 1 FROM booth_items + WHERE booth_item_number = %s AND discord_user_id = %s + ''', (booth_item_number, discord_user_id)) + return cursor.fetchone() is not None + def list_booth_items(self, discord_user_id, discord_channel_id): booth_account = self.get_booth_account(discord_user_id) - if booth_account: - self.cursor.execute(''' + if not booth_account: + raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") + + with self.conn.cursor() as cursor: + cursor.execute(''' SELECT bi.booth_item_number FROM booth_items bi JOIN discord_noti_channels dnc @@ -229,56 +231,70 @@ def list_booth_items(self, discord_user_id, discord_channel_id): WHERE bi.discord_user_id = %s AND dnc.discord_channel_id = %s; ''', (discord_user_id, discord_channel_id)) - return self.cursor.fetchall() - else: - raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") + return cursor.fetchall() - def add_discord_noti_channel(self, discord_channel_id, booth_order_number, use_transaction=True): - tx_context = self._transaction() if use_transaction else nullcontext() - with tx_context: - self.cursor.execute(''' - SELECT 1 FROM discord_noti_channels - WHERE discord_channel_id = %s AND booth_order_number = %s - ''', (discord_channel_id, booth_order_number)) - if self.cursor.fetchone(): - return False - - self.cursor.execute(''' - INSERT INTO discord_noti_channels (discord_channel_id, booth_order_number) - VALUES (%s, %s) - ''', (discord_channel_id, booth_order_number)) - return True + def add_discord_noti_channel(self, discord_channel_id, booth_order_number, use_transaction=True, cursor=None): + if cursor is not None: + return self._insert_discord_noti_channel(cursor, discord_channel_id, booth_order_number) + if use_transaction: + with self._transaction() as tx_cursor: + return self._insert_discord_noti_channel(tx_cursor, discord_channel_id, booth_order_number) + with self.conn.cursor() as standalone_cursor: + return self._insert_discord_noti_channel(standalone_cursor, discord_channel_id, booth_order_number) - def del_discord_noti_channel(self, booth_order_number, use_transaction=True): - self.logger.debug(f"del_discord_noti_channel - booth_order_number : {booth_order_number}") # 추가된 로그 - tx_context = self._transaction() if use_transaction else nullcontext() - with tx_context: - self.cursor.execute(''' - DELETE FROM discord_noti_channels WHERE booth_order_number = %s - ''', (booth_order_number,)) - return self.cursor.rowcount + def del_discord_noti_channel(self, booth_order_number, use_transaction=True, cursor=None): + self.logger.debug("del_discord_noti_channel - booth_order_number : %s", booth_order_number) + if cursor is not None: + return self._delete_discord_noti_channel(cursor, booth_order_number) + if use_transaction: + with self._transaction() as tx_cursor: + return self._delete_discord_noti_channel(tx_cursor, booth_order_number) + with self.conn.cursor() as standalone_cursor: + return self._delete_discord_noti_channel(standalone_cursor, booth_order_number) def update_discord_noti_channel(self, discord_user_id, discord_channel_id, booth_item_number): booth_order_number = self.get_booth_order_number(booth_item_number, discord_user_id) if not booth_order_number: raise Exception("Item not found") - with self._transaction(): - self.cursor.execute(''' + with self._transaction() as cursor: + cursor.execute(''' UPDATE discord_noti_channels SET discord_channel_id = %s WHERE booth_order_number = %s ''', (discord_channel_id, booth_order_number)) - return self.cursor.rowcount + return cursor.rowcount def get_booth_order_number(self, booth_item_number, discord_user_id): - self.cursor.execute(''' - SELECT booth_order_number FROM booth_items - WHERE booth_item_number = %s AND discord_user_id = %s - ''', (booth_item_number, discord_user_id)) - result = self.cursor.fetchone() + with self.conn.cursor() as cursor: + cursor.execute(''' + SELECT booth_order_number FROM booth_items + WHERE booth_item_number = %s AND discord_user_id = %s + ''', (booth_item_number, discord_user_id)) + result = cursor.fetchone() return result[0] if result else None def get_booth_item_count(self, discord_user_id): - self.cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = %s', (discord_user_id,)) - result = self.cursor.fetchone() - return result[0] if result else 0 \ No newline at end of file + with self.conn.cursor() as cursor: + cursor.execute('SELECT COUNT(*) FROM booth_items WHERE discord_user_id = %s', (discord_user_id,)) + result = cursor.fetchone() + return result[0] if result else 0 + + def _insert_discord_noti_channel(self, cursor, discord_channel_id, booth_order_number): + cursor.execute(''' + SELECT 1 FROM discord_noti_channels + WHERE discord_channel_id = %s AND booth_order_number = %s + ''', (discord_channel_id, booth_order_number)) + if cursor.fetchone(): + return False + + cursor.execute(''' + INSERT INTO discord_noti_channels (discord_channel_id, booth_order_number) + VALUES (%s, %s) + ''', (discord_channel_id, booth_order_number)) + return True + + def _delete_discord_noti_channel(self, cursor, booth_order_number): + cursor.execute(''' + DELETE FROM discord_noti_channels WHERE booth_order_number = %s + ''', (booth_order_number,)) + return cursor.rowcount From bd9542fddd03f3819b5c6cb975d3d9966b3fd180 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Fri, 24 Oct 2025 15:15:07 +0900 Subject: [PATCH 19/27] =?UTF-8?q?refactor:=20syslog=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 22 +++++++++++++---- booth_checker/booth_sql.py | 4 ++-- booth_discord/__main__.py | 29 ++++++++++++++++++----- config-sample.json | 7 ++++++ docker/booth-checker/Dockerfile | 3 ++- docker/booth-discord/Dockerfile | 3 ++- logging_setup.py | 42 +++++++++++++++++++++++++++++++++ 7 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 logging_setup.py diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index 54426e6..aa6be0d 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -21,6 +21,7 @@ import booth_sql import cloudflare import llm_summary +from logging_setup import attach_syslog_handler DRY_RUN = None @@ -32,17 +33,19 @@ def filter(self, record): record.order_num = getattr(thread_local, 'order_num', 'main') return True +LOG_FORMAT = '[%(asctime)s] - [%(levelname)s] - [%(order_num)s] - %(message)s' +DATE_FORMAT = '%Y-%m-%d %H:%M:%S' +formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT) + logger = logging.getLogger('BoothChecker') logger.setLevel(logging.INFO) logger.propagate = False if not logger.hasHandlers(): handler = logging.StreamHandler() - formatter = logging.Formatter( - '[%(asctime)s] - [%(levelname)s] - [%(order_num)s] - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - ) handler.setFormatter(formatter) logger.addHandler(handler) + +if all(not isinstance(f, ContextFilter) for f in logger.filters): logger.addFilter(ContextFilter()) class BoothCrawlError(Exception): @@ -778,6 +781,17 @@ def strftime_now(): if __name__ == "__main__": with open("config.json") as file: config_json = simdjson.load(file) + + logging_config = config_json.get('logging', {}) + syslog_config = logging_config.get('syslog', {}) + attach_syslog_handler(logger, syslog_config, formatter) + if syslog_config.get('enabled') and syslog_config.get('address'): + port_value = syslog_config.get('port', 514) + try: + port = int(port_value) + except (TypeError, ValueError): + port = port_value + logger.info("Syslog logging enabled: sending logs to %s:%s", syslog_config.get('address'), port) # Configure global settings discord_api_url = config_json['discord_api_url'] diff --git a/booth_checker/booth_sql.py b/booth_checker/booth_sql.py index d4a1291..8148feb 100644 --- a/booth_checker/booth_sql.py +++ b/booth_checker/booth_sql.py @@ -3,7 +3,7 @@ import psycopg -logger = logging.getLogger(__name__) +logger = logging.getLogger('BoothChecker') class BoothPostgres: @@ -59,4 +59,4 @@ def _connect_with_retry(self, conn_params, retries=5, delay=2): retries, exc, ) - time.sleep(delay) \ No newline at end of file + time.sleep(delay) diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index a5ca5a7..351ad22 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -4,18 +4,35 @@ import booth as booth_module import booth_sql import booth_discord +from logging_setup import attach_syslog_handler -logging.basicConfig( - level=logging.INFO, - format='[%(asctime)s] - [%(levelname)s] - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) -logger = logging.getLogger(__name__) +LOG_FORMAT = '[%(asctime)s] - [%(levelname)s] - %(message)s' +DATE_FORMAT = '%Y-%m-%d %H:%M:%S' +formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT) + +logger = logging.getLogger('BoothDiscord') +logger.setLevel(logging.INFO) +logger.propagate = False +if not logger.hasHandlers(): + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) def main(): # Load configuration with open("config.json") as file: config_json = json.load(file) + + logging_config = config_json.get('logging', {}) + syslog_config = logging_config.get('syslog', {}) + attach_syslog_handler(logger, syslog_config, formatter) + if syslog_config.get('enabled') and syslog_config.get('address'): + port_value = syslog_config.get('port', 514) + try: + port = int(port_value) + except (TypeError, ValueError): + port = port_value + logger.info("Syslog logging enabled: sending logs to %s:%s", syslog_config.get('address'), port) # Read configuration values discord_bot_token = config_json['discord_bot_token'] diff --git a/config-sample.json b/config-sample.json index b8523f2..dc6d091 100644 --- a/config-sample.json +++ b/config-sample.json @@ -1,6 +1,13 @@ { "dry_run": false, "refresh_interval": 600, + "logging": { + "syslog": { + "enabled": false, + "address": "", + "port": 5141 + } + }, "selenium_url": "http://localhost:4444/wd/hub", "postgres": { "host": "postgres", diff --git a/docker/booth-checker/Dockerfile b/docker/booth-checker/Dockerfile index 57a7fda..c103038 100644 --- a/docker/booth-checker/Dockerfile +++ b/docker/booth-checker/Dockerfile @@ -3,8 +3,9 @@ ENV TZ=Asia/Seoul RUN apt-get update && apt-get upgrade -y WORKDIR /root/boothchecker COPY ./booth_checker ./ +COPY ./logging_setup.py ./logging_setup.py COPY ./templates ./templates COPY ./docker/booth-checker/requirements.txt ./ RUN pip install --upgrade pip RUN pip install -r requirements.txt -CMD ["python3","__main__.py"] \ No newline at end of file +CMD ["python3","__main__.py"] diff --git a/docker/booth-discord/Dockerfile b/docker/booth-discord/Dockerfile index a2ef2fd..f719e67 100644 --- a/docker/booth-discord/Dockerfile +++ b/docker/booth-discord/Dockerfile @@ -3,7 +3,8 @@ ENV TZ=Asia/Seoul RUN apt-get update && apt-get upgrade -y WORKDIR /root/boothchecker COPY ./booth_discord ./ +COPY ./logging_setup.py ./logging_setup.py COPY ./docker/booth-discord/requirements.txt ./ RUN pip install --upgrade pip RUN pip install -r requirements.txt -CMD ["python3","__main__.py"] \ No newline at end of file +CMD ["python3","__main__.py"] diff --git a/logging_setup.py b/logging_setup.py new file mode 100644 index 0000000..a7df6a1 --- /dev/null +++ b/logging_setup.py @@ -0,0 +1,42 @@ +import logging +from logging.handlers import SysLogHandler +from typing import Optional + + +def attach_syslog_handler( + target_logger: logging.Logger, + syslog_config: dict, + formatter: Optional[logging.Formatter] = None, +) -> None: + """Attach a syslog handler to the provided logger when enabled in config.""" + if not syslog_config or not isinstance(syslog_config, dict): + return + + if not syslog_config.get("enabled"): + return + + address = syslog_config.get("address") + if not address: + target_logger.warning("Syslog logging enabled but no address configured; skipping SysLogHandler setup.") + return + + port_value = syslog_config.get("port", 514) + try: + port = int(port_value) + except (TypeError, ValueError): + target_logger.warning("Invalid syslog port '%s'; skipping SysLogHandler setup.", port_value) + return + + if any(isinstance(handler, SysLogHandler) for handler in target_logger.handlers): + return + + try: + syslog_handler = SysLogHandler(address=(address, port)) + except OSError as exc: + target_logger.error("Failed to initialize SysLogHandler for %s:%s: %s", address, port, exc) + return + + if formatter: + syslog_handler.setFormatter(formatter) + + target_logger.addHandler(syslog_handler) From c5027d24f9c1380d6659463399cc0fbc5bdce7fd Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Tue, 11 Nov 2025 15:15:01 +0900 Subject: [PATCH 20/27] =?UTF-8?q?fix:=20Issue=20#37=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/booth_discord.py | 3 +++ booth_discord/booth_sql.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/booth_discord/booth_discord.py b/booth_discord/booth_discord.py index 0ff4d2d..531122f 100644 --- a/booth_discord/booth_discord.py +++ b/booth_discord/booth_discord.py @@ -45,6 +45,7 @@ async def booth(interaction: discord.Interaction, cookie: str): @self.tree.command(name="item_add", description="BOOTH 아이템 등록") @app_commands.describe(item_number="BOOTH 상품 번호를 입력 해주세요") @app_commands.describe(item_name="아이템 이름을 입력 해주세요") + @app_commands.describe(order_number="수동으로 주문 번호를 입력해야 할 때에 사용") @app_commands.describe(intent_encoding="아이템 이름의 인코딩 방식을 입력해주세요 (기본값: shift_jis)") @app_commands.describe(summary_this="업데이트 내용 요약 (기본값: True)") @app_commands.describe(fbx_only="FBX 변경점만 확인 (기본값: False)") @@ -52,6 +53,7 @@ async def item_add( interaction: discord.Interaction, item_number: str, item_name: str = None, + order_number: str = None, intent_encoding: str = "shift_jis", summary_this: bool = True, fbx_only: bool = False @@ -62,6 +64,7 @@ async def item_add( interaction.user.id, interaction.channel_id, item_number, + order_number, item_name, intent_encoding, summary_this, diff --git a/booth_discord/booth_sql.py b/booth_discord/booth_sql.py index 130f571..eb7d014 100644 --- a/booth_discord/booth_sql.py +++ b/booth_discord/booth_sql.py @@ -100,14 +100,18 @@ def add_booth_account(self, session_cookie, discord_user_id): ''', (session_cookie, discord_user_id)) return self.get_booth_account(discord_user_id) - def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, item_name, intent_encoding, summary_this, fbx_only): + def add_booth_item(self, discord_user_id, discord_channel_id, booth_item_number, booth_order_number, item_name, intent_encoding, summary_this, fbx_only): booth_account = self.get_booth_account(discord_user_id) if self.is_item_duplicate(booth_item_number, discord_user_id): raise Exception("이미 등록된 아이템입니다.") if not booth_account: raise Exception("BOOTH 계정이 등록되어 있지 않습니다.") - booth_order_info = self.booth.get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) + if booth_order_number: + booth_order_info = (False, booth_order_number) + else: + booth_order_info = self.booth.get_booth_order_info(booth_item_number, ("_plaza_session_nktz7u", booth_account[0])) + try: with self._transaction() as cursor: cursor.execute(''' From 7c12d46617c9a11a485f2e7c89164f94daf9904e Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Wed, 19 Nov 2025 13:45:19 +0900 Subject: [PATCH 21/27] =?UTF-8?q?fix:=20fbx=20=ED=95=B4=EC=8B=9C=EA=B0=92?= =?UTF-8?q?=20=EC=B2=B4=EC=9D=B8=EC=A7=80=EB=A1=9C=EA=B7=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_checker/__main__.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/booth_checker/__main__.py b/booth_checker/__main__.py index aa6be0d..ca83332 100644 --- a/booth_checker/__main__.py +++ b/booth_checker/__main__.py @@ -269,17 +269,33 @@ def generate_fbx_changelog_and_summary(item_data, download_url_list, version_jso logger.error(f'An error occurred while parsing {filename}: {e}') logger.debug(traceback.format_exc()) + previous_hashes = {file_hash for file_hash in previous_fbx.values()} + current_hashes = {file_hash for file_hash in current_fbx.values()} + added = [] changed = [] + deleted = [] + + previous_remaining = dict(previous_fbx) + current_remaining = dict(current_fbx) - for name, new_hash in current_fbx.items(): - old_hash = previous_fbx.get(name) - if old_hash is None: - added.append(name) - elif old_hash != new_hash: + for name in set(previous_fbx.keys()) & set(current_fbx.keys()): + old_hash = previous_fbx[name] + new_hash = current_fbx[name] + if old_hash != new_hash: changed.append(name) + previous_remaining.pop(name, None) + current_remaining.pop(name, None) - deleted = [name for name in previous_fbx.keys() if name not in current_fbx] + for name, new_hash in current_remaining.items(): + if new_hash in previous_hashes: + continue + added.append(name) + + for name, old_hash in previous_remaining.items(): + if old_hash in current_hashes: + continue + deleted.append(name) if not added and not changed and not deleted: logger.info('No FBX hash differences detected; skipping changelog generation.') From ce0f869ade401dce0804ebe6a81ca8b3634fc171 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Fri, 21 Nov 2025 10:39:32 +0900 Subject: [PATCH 22/27] =?UTF-8?q?fix:=20fbx=5Fonly=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20config.json=EC=9C=BC=EB=A1=9C=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/__main__.py | 1 + booth_discord/booth_discord.py | 7 ++++--- config-sample.json | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index 351ad22..1d5dd36 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -37,6 +37,7 @@ def main(): # Read configuration values discord_bot_token = config_json['discord_bot_token'] selenium_url = config_json['selenium_url'] + fbx_only = config_json['fbx_only'] # Initialize database and bot booth_crawler = booth_module.BoothCrawler(selenium_url) diff --git a/booth_discord/booth_discord.py b/booth_discord/booth_discord.py index 531122f..842c2ff 100644 --- a/booth_discord/booth_discord.py +++ b/booth_discord/booth_discord.py @@ -7,12 +7,13 @@ import asyncio class DiscordBot(commands.Bot): - def __init__(self, booth_db, logger, *args, **kwargs): + def __init__(self, booth_db, logger, fbx_only, *args, **kwargs): intents = discord.Intents.default() intents.message_content = True super().__init__(command_prefix="/", intents=intents, *args, **kwargs) self.booth_db = booth_db self.logger = logger + self.fbx_only = fbx_only self.embed_message = None # on_ready에서 초기화 예정 self.app = Quart(__name__) # Quart 앱 초기화 self.setup_commands() @@ -48,7 +49,7 @@ async def booth(interaction: discord.Interaction, cookie: str): @app_commands.describe(order_number="수동으로 주문 번호를 입력해야 할 때에 사용") @app_commands.describe(intent_encoding="아이템 이름의 인코딩 방식을 입력해주세요 (기본값: shift_jis)") @app_commands.describe(summary_this="업데이트 내용 요약 (기본값: True)") - @app_commands.describe(fbx_only="FBX 변경점만 확인 (기본값: False)") + @app_commands.describe(fbx_only=f'FBX 변경점만 확인 (기본값: {str(self.fbx_only)})') async def item_add( interaction: discord.Interaction, item_number: str, @@ -56,7 +57,7 @@ async def item_add( order_number: str = None, intent_encoding: str = "shift_jis", summary_this: bool = True, - fbx_only: bool = False + fbx_only: bool = self.fbx_only ): try: await interaction.response.defer(ephemeral=True) diff --git a/config-sample.json b/config-sample.json index dc6d091..84a50b8 100644 --- a/config-sample.json +++ b/config-sample.json @@ -1,6 +1,7 @@ { "dry_run": false, "refresh_interval": 600, + "fbx_only": false, "logging": { "syslog": { "enabled": false, From 5b30cf1bc1e2d46b38d426f9b07e34a407211d73 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 27 Nov 2025 11:58:36 +0900 Subject: [PATCH 23/27] =?UTF-8?q?fix:=20DiscordBot=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- booth_discord/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/booth_discord/__main__.py b/booth_discord/__main__.py index 1d5dd36..b305d85 100644 --- a/booth_discord/__main__.py +++ b/booth_discord/__main__.py @@ -43,7 +43,7 @@ def main(): booth_crawler = booth_module.BoothCrawler(selenium_url) postgres_config = dict(config_json['postgres']) booth_db = booth_sql.BoothPostgres(postgres_config, booth_crawler, logger) - bot = booth_discord.DiscordBot(booth_db, logger) + bot = booth_discord.DiscordBot(booth_db, logger, fbx_only) bot.run(discord_bot_token) if __name__ == "__main__": From 6484b871a4291cb933aac76f09472d2f5c960f6a Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 27 Nov 2025 12:01:11 +0900 Subject: [PATCH 24/27] =?UTF-8?q?refactor:=20github=20aciton=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-build.yml | 37 ------------------- .../{beta-build.yml => develop-build.yml} | 8 ++-- 2 files changed, 4 insertions(+), 41 deletions(-) delete mode 100644 .github/workflows/dev-build.yml rename .github/workflows/{beta-build.yml => develop-build.yml} (86%) diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml deleted file mode 100644 index dfee743..0000000 --- a/.github/workflows/dev-build.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: dev - -on: - pull_request: - branches: - - dev - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push (booth-checker) - uses: docker/build-push-action@v6 - with: - push: true - file: ./docker/booth-checker/Dockerfile - tags: ogunarmaya/booth-checker:dev - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push (booth-discord) - uses: docker/build-push-action@v6 - with: - push: true - file: ./docker/booth-discord/Dockerfile - tags: ogunarmaya/booth-discord:dev - cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/beta-build.yml b/.github/workflows/develop-build.yml similarity index 86% rename from .github/workflows/beta-build.yml rename to .github/workflows/develop-build.yml index 4812e4b..2484508 100644 --- a/.github/workflows/beta-build.yml +++ b/.github/workflows/develop-build.yml @@ -1,9 +1,9 @@ -name: beta +name: develop on: push: branches: - - dev + - develop jobs: docker: @@ -23,7 +23,7 @@ jobs: with: push: true file: ./docker/booth-checker/Dockerfile - tags: ogunarmaya/booth-checker:beta + tags: ogunarmaya/booth-checker:develop cache-from: type=gha cache-to: type=gha,mode=max @@ -32,6 +32,6 @@ jobs: with: push: true file: ./docker/booth-discord/Dockerfile - tags: ogunarmaya/booth-discord:beta + tags: ogunarmaya/booth-discord:develop cache-from: type=gha cache-to: type=gha,mode=max \ No newline at end of file From 57de492a78a26cff416faff7a6fc9c34fa48be6c Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 27 Nov 2025 13:08:25 +0900 Subject: [PATCH 25/27] =?UTF-8?q?refactor:=20github=20aciton=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/develop-build.yml | 82 ++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/.github/workflows/develop-build.yml b/.github/workflows/develop-build.yml index 2484508..1d524e7 100644 --- a/.github/workflows/develop-build.yml +++ b/.github/workflows/develop-build.yml @@ -5,33 +5,91 @@ on: branches: - develop +env: + REGISTRY: ghcr.io + IMAGE_NAME_BOOTH_CHECKER: ${{ github.repository_owner }}/booth-checker + IMAGE_NAME_BOOTH_DISCORD: ${{ github.repository_owner }}/booth-discord + jobs: - docker: + build-and-push-images: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GH_TOKEN }} + + # ======================= + # booth-checker + # ======================= + - name: Extract metadata (booth-checker) + id: meta_booth_checker + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_CHECKER }} + tags: | + type=ref,event=branch + type=sha - - name: Build and push (booth-checker) + - name: Build and push Docker image (booth-checker) + id: push_booth_checker uses: docker/build-push-action@v6 with: - push: true + context: . file: ./docker/booth-checker/Dockerfile - tags: ogunarmaya/booth-checker:develop + push: true + tags: ${{ steps.meta_booth_checker.outputs.tags }} + labels: ${{ steps.meta_booth_checker.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - - name: Build and push (booth-discord) + + - name: Generate artifact attestation (booth-checker) + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_CHECKER }} + subject-digest: ${{ steps.push_booth_checker.outputs.digest }} + push-to-registry: true + + # ======================= + # booth-discord + # ======================= + - name: Extract metadata (booth-discord) + id: meta_booth_discord + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_DISCORD }} + tags: | + type=ref,event=branch + type=sha + + - name: Build and push Docker image (booth-discord) + id: push_booth_discord uses: docker/build-push-action@v6 with: - push: true + context: . file: ./docker/booth-discord/Dockerfile - tags: ogunarmaya/booth-discord:develop + push: true + tags: ${{ steps.meta_booth_discord.outputs.tags }} + labels: ${{ steps.meta_booth_discord.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + + - name: Generate artifact attestation (booth-discord) + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_DISCORD }} + subject-digest: ${{ steps.push_booth_discord.outputs.digest }} + push-to-registry: true \ No newline at end of file From 3a54a6da706e0ae226060da5c32d3e3e8e3fc144 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 27 Nov 2025 13:27:40 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refactor:=20github=20aciton=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-build.yml | 79 ++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/.github/workflows/latest-build.yml b/.github/workflows/latest-build.yml index db04d60..460878e 100644 --- a/.github/workflows/latest-build.yml +++ b/.github/workflows/latest-build.yml @@ -3,39 +3,98 @@ name: latest on: push: tags: - - 'v*' + - 'v*' # v1.0.0 같은 태그 푸시될 때만 실행 + +env: + REGISTRY: ghcr.io + IMAGE_NAME_BOOTH_CHECKER: ${{ github.repository_owner }}/booth-checker + IMAGE_NAME_BOOTH_DISCORD: ${{ github.repository_owner }}/booth-discord jobs: docker-build: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # ======================= + # booth-checker + # ======================= + - name: Extract metadata (booth-checker) + id: meta_booth_checker + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_CHECKER }} + tags: | + type=raw,value=latest + type=ref,event=tag - name: Build and push (booth-checker) + id: push_booth_checker uses: docker/build-push-action@v6 with: - push: true + context: . file: ./docker/booth-checker/Dockerfile - tags: ogunarmaya/booth-checker:latest + push: true + tags: ${{ steps.meta_booth_checker.outputs.tags }} + labels: ${{ steps.meta_booth_checker.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - + + - name: Generate artifact attestation (booth-checker) + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_CHECKER }} + subject-digest: ${{ steps.push_booth_checker.outputs.digest }} + push-to-registry: true + + # ======================= + # booth-discord + # ======================= + - name: Extract metadata (booth-discord) + id: meta_booth_discord + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_DISCORD }} + tags: | + type=raw,value=latest + type=ref,event=tag + - name: Build and push (booth-discord) + id: push_booth_discord uses: docker/build-push-action@v6 with: - push: true + context: . file: ./docker/booth-discord/Dockerfile - tags: ogunarmaya/booth-discord:latest + push: true + tags: ${{ steps.meta_booth_discord.outputs.tags }} + labels: ${{ steps.meta_booth_discord.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - + + - name: Generate artifact attestation (booth-discord) + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BOOTH_DISCORD }} + subject-digest: ${{ steps.push_booth_discord.outputs.digest }} + push-to-registry: true + publish-release: runs-on: ubuntu-latest needs: docker-build From f122c82d21b90fa69ae05e3853f13b3db3021e45 Mon Sep 17 00:00:00 2001 From: Ogunaru Date: Thu, 27 Nov 2025 13:27:47 +0900 Subject: [PATCH 27/27] =?UTF-8?q?refactor:=20README.md=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 453d636..4383939 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BoothChecker [![latest](https://github.com/MAX-FLAVOR/BoothChecker/actions/workflows/latest-build.yml/badge.svg)](https://github.com/MAX-FLAVOR/BoothChecker/actions/workflows/latest-build.yml) -[![dev](https://github.com/MAX-FLAVOR/BoothChecker/actions/workflows/dev-build.yml/badge.svg)](https://github.com/MAX-FLAVOR/BoothChecker/actions/workflows/dev-build.yml) +[![develop](https://github.com/MAX-FLAVOR/BoothChecker/actions/workflows/develop-build.yml/badge.svg)](https://github.com/MAX-FLAVOR/BoothChecker/actions/workflows/develop-build.yml) *** ### Docker-Compose