From 62a95c6fcce0e5c410d464f3ed79868565577a3d Mon Sep 17 00:00:00 2001 From: 9700X_PC <9700X_PC@gmail.com> Date: Thu, 26 Jun 2025 18:33:57 +0900 Subject: [PATCH] first commit --- .gitignore | 9 + ExtensionInstaller.spec | 38 + log.txt | 195 +++ main.py | 343 +++++ requirements.txt | 2 + wrmc_ext/background.js | 2427 +++++++++++++++++++++++++++++++ wrmc_ext/bannedWords.html | 244 ++++ wrmc_ext/bannedWords.js | 1652 +++++++++++++++++++++ wrmc_ext/content.js | 866 +++++++++++ wrmc_ext/icon.png | Bin 0 -> 20724 bytes wrmc_ext/manifest.json | 83 ++ wrmc_ext/manual.html | 649 +++++++++ wrmc_ext/manual.js | 38 + wrmc_ext/popup.html | 496 +++++++ wrmc_ext/popup.js | 1864 ++++++++++++++++++++++++ wrmc_ext/rest-modal.html | 296 ++++ wrmc_ext/rest-modal.js | 304 ++++ wrmc_ext/sayings.html | 1195 ++++++++++++++++ wrmc_ext/sayings.js | 2845 +++++++++++++++++++++++++++++++++++++ wrmc_ext/settings.html | 582 ++++++++ wrmc_ext/settings.js | 546 +++++++ wrmc_ext/zzim.html | 288 ++++ wrmc_ext/zzim.js | 616 ++++++++ 확장프로그램설치도구.spec | 38 + 24 files changed, 15616 insertions(+) create mode 100644 .gitignore create mode 100644 ExtensionInstaller.spec create mode 100644 log.txt create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 wrmc_ext/background.js create mode 100644 wrmc_ext/bannedWords.html create mode 100644 wrmc_ext/bannedWords.js create mode 100644 wrmc_ext/content.js create mode 100644 wrmc_ext/icon.png create mode 100644 wrmc_ext/manifest.json create mode 100644 wrmc_ext/manual.html create mode 100644 wrmc_ext/manual.js create mode 100644 wrmc_ext/popup.html create mode 100644 wrmc_ext/popup.js create mode 100644 wrmc_ext/rest-modal.html create mode 100644 wrmc_ext/rest-modal.js create mode 100644 wrmc_ext/sayings.html create mode 100644 wrmc_ext/sayings.js create mode 100644 wrmc_ext/settings.html create mode 100644 wrmc_ext/settings.js create mode 100644 wrmc_ext/zzim.html create mode 100644 wrmc_ext/zzim.js create mode 100644 확장프로그램설치도구.spec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..442ca8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +build/ +dist/ +*.pyc +*.pyo +*.pyd +include/ +lib/ +scripts/ +pyvenv.cfg diff --git a/ExtensionInstaller.spec b/ExtensionInstaller.spec new file mode 100644 index 0000000..37f8d89 --- /dev/null +++ b/ExtensionInstaller.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[('wrmc_ext', 'wrmc_ext')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='ExtensionInstaller', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..ea0deb1 --- /dev/null +++ b/log.txt @@ -0,0 +1,195 @@ +[2025-06-25 22:17:24] 관리자 권한 체크: 1 +[2025-06-25 22:17:24] 프로그램 시작 +[2025-06-25 22:17:24] 탐지된 브라우저: [{'name': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'}, {'name': 'Whale', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe'}, {'name': 'Edge', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'}] +[2025-06-25 22:17:28] 설치 버튼 클릭 +[2025-06-25 22:17:28] 선택된 브라우저: [{'name': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'}] +[2025-06-25 22:17:28] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 22:17:28] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 22:17:28] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:17:28] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:17:28] 결과 UI 표시 완료 +[2025-06-25 22:17:28] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 22:17:28] 크롬 실행중 여부: True +[2025-06-25 22:17:34] 크롬 실행중: 수동 안내 +[2025-06-25 22:17:40] 설치 안내 메시지 표시 +[2025-06-25 22:17:43] 관리자 권한 체크: 1 +[2025-06-25 22:17:43] 프로그램 시작 +[2025-06-25 22:17:43] 탐지된 브라우저: [{'name': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'}, {'name': 'Whale', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe'}, {'name': 'Edge', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'}] +[2025-06-25 22:17:47] 설치 버튼 클릭 +[2025-06-25 22:17:47] 선택된 브라우저: [{'name': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'}] +[2025-06-25 22:17:47] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 22:17:47] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 22:17:47] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:17:47] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:17:47] 결과 UI 표시 완료 +[2025-06-25 22:17:47] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 22:17:47] 크롬 실행중 여부: False +[2025-06-25 22:17:47] 크롬 확장페이지 새창으로 오픈 +[2025-06-25 22:17:54] 설치 안내 메시지 표시 +[2025-06-25 22:17:55] 경로 복사: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:22:41] 관리자 권한 체크: 1 +[2025-06-25 22:22:41] 프로그램 시작 +[2025-06-25 22:22:41] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 22:22:47] 설치 버튼 클릭 +[2025-06-25 22:22:47] 선택된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}] +[2025-06-25 22:22:47] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 22:22:47] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 22:22:47] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:22:47] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:22:47] 결과 UI 표시 완료 +[2025-06-25 22:22:47] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 22:22:47] 크롬 실행중 여부: False +[2025-06-25 22:22:47] 크롬 확장페이지 새창으로 오픈 +[2025-06-25 22:22:51] 설치 안내 메시지 표시 +[2025-06-25 22:22:52] 확장주소 복사: chrome://extensions/ +[2025-06-25 22:40:37] 관리자 권한 체크: 1 +[2025-06-25 22:40:37] 프로그램 시작 +[2025-06-25 22:40:37] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 22:40:42] 설치 버튼 클릭 +[2025-06-25 22:40:42] 선택된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}] +[2025-06-25 22:40:42] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 22:40:42] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 22:40:42] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:40:42] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:40:42] 결과 UI 표시 완료 +[2025-06-25 22:40:42] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 22:40:42] 크롬 실행중 여부: False +[2025-06-25 22:40:42] 크롬 확장페이지 새창으로 오픈 +[2025-06-25 22:40:56] 설치 안내 메시지 표시 +[2025-06-25 22:40:57] 확장주소 복사: chrome://extensions/ +[2025-06-25 22:43:36] 관리자 권한 체크: 1 +[2025-06-25 22:43:36] 프로그램 시작 +[2025-06-25 22:43:36] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 22:43:40] 설치 버튼 클릭 +[2025-06-25 22:43:40] 선택된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}] +[2025-06-25 22:43:40] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 22:43:40] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 22:43:40] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:43:40] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 22:43:40] 결과 UI 표시 완료 +[2025-06-25 22:43:40] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 22:43:40] 크롬 실행중 여부: False +[2025-06-25 22:43:40] 크롬 확장페이지 새창으로 오픈 +[2025-06-25 22:43:41] 설치 안내 메시지 표시 +[2025-06-25 23:19:53] 관리자 권한 체크: 1 +[2025-06-25 23:19:53] 프로그램 시작 +[2025-06-25 23:19:53] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:19:57] 설치 버튼 클릭 +[2025-06-25 23:19:57] 선택된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}] +[2025-06-25 23:19:57] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:19:57] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:19:57] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:19:57] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:19:57] 결과 UI 표시 완료 +[2025-06-25 23:19:57] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:19:57] 크롬 실행중 여부: False +[2025-06-25 23:19:57] 크롬 확장페이지 새창으로 오픈 +[2025-06-25 23:19:59] 설치 안내 메시지 표시 +[2025-06-25 23:23:42] 관리자 권한 체크: 1 +[2025-06-25 23:23:42] 프로그램 시작 +[2025-06-25 23:23:42] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:23:46] 설치 버튼 클릭 +[2025-06-25 23:23:46] 선택된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}] +[2025-06-25 23:23:46] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:23:46] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:23:46] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:23:46] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:23:46] 결과 UI 표시 완료 +[2025-06-25 23:23:46] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:23:46] 크롬 실행중 여부: True +[2025-06-25 23:23:50] 크롬 실행중: 수동 안내 +[2025-06-25 23:23:52] 설치 안내 메시지 표시 +[2025-06-25 23:24:02] 확장주소 복사: chrome://extensions/ +[2025-06-25 23:24:14] 폴더 경로 복사: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:26:05] 관리자 권한 체크: 1 +[2025-06-25 23:26:05] 프로그램 시작 +[2025-06-25 23:26:05] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:26:09] 설치 버튼 클릭 +[2025-06-25 23:26:09] 선택된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}] +[2025-06-25 23:26:09] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:26:09] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:26:09] Chrome 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:26:09] Chrome 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:26:09] 결과 UI 표시 완료 +[2025-06-25 23:26:09] Chrome 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:26:09] 크롬 실행중 여부: False +[2025-06-25 23:26:09] 크롬 확장페이지 새창으로 오픈 +[2025-06-25 23:26:10] 설치 안내 메시지 표시 +[2025-06-25 23:26:11] 확장주소 복사: chrome://extensions/ +[2025-06-25 23:26:24] 폴더 경로 복사: C:\Users\Administrator\AppData\Local\my_extension_installer\chrome_extension +[2025-06-25 23:28:06] 관리자 권한 체크: 1 +[2025-06-25 23:28:06] 프로그램 시작 +[2025-06-25 23:28:06] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:28:09] 설치 버튼 클릭 +[2025-06-25 23:28:09] 선택된 브라우저: [{'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}] +[2025-06-25 23:28:09] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:28:09] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:28:09] Whale 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:28:09] 결과 UI 표시 완료 +[2025-06-25 23:28:09] Whale 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:28:09] 웨일 확장페이지 새창으로 오픈 +[2025-06-25 23:28:13] 설치 안내 메시지 표시 +[2025-06-25 23:28:13] 확장주소 복사: whale://extensions/ +[2025-06-25 23:28:20] 폴더 경로 복사: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:31:07] 관리자 권한 체크: 1 +[2025-06-25 23:31:07] 프로그램 시작 +[2025-06-25 23:31:07] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:31:09] 설치 버튼 클릭 +[2025-06-25 23:31:09] 선택된 브라우저: [{'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}] +[2025-06-25 23:31:09] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:31:09] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:31:09] Whale 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:31:09] Whale 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:31:09] 결과 UI 표시 완료 +[2025-06-25 23:31:09] Whale 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:31:09] 웨일 확장페이지 새창으로 오픈 +[2025-06-25 23:31:16] 설치 안내 메시지 표시 +[2025-06-25 23:32:06] 관리자 권한 체크: 1 +[2025-06-25 23:32:06] 프로그램 시작 +[2025-06-25 23:32:06] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:32:08] 설치 버튼 클릭 +[2025-06-25 23:32:08] 선택된 브라우저: [{'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}] +[2025-06-25 23:32:08] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:32:08] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:32:08] Whale 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:32:08] Whale 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:32:08] 결과 UI 표시 완료 +[2025-06-25 23:32:08] Whale 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:32:08] 웨일 확장페이지 새창으로 오픈 +[2025-06-25 23:32:15] 설치 안내 메시지 표시 +[2025-06-25 23:32:16] 확장주소 복사: whale://extensions/ +[2025-06-25 23:32:36] 폴더 경로 복사: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:33:24] 관리자 권한 체크: 1 +[2025-06-25 23:33:24] 프로그램 시작 +[2025-06-25 23:33:24] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:33:26] 설치 버튼 클릭 +[2025-06-25 23:33:26] 선택된 브라우저: [{'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}] +[2025-06-25 23:33:26] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:33:26] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:33:26] Whale 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:33:26] Whale 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:33:26] 결과 UI 표시 완료 +[2025-06-25 23:33:26] Whale 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:33:26] 웨일 확장페이지 새창으로 오픈 +[2025-06-25 23:33:31] 설치 안내 메시지 표시 +[2025-06-25 23:33:31] 확장주소 복사: whale://extensions/ +[2025-06-25 23:33:41] 폴더 경로 복사: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:37:11] 관리자 권한 체크: 1 +[2025-06-25 23:37:11] 프로그램 시작 +[2025-06-25 23:37:11] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-25 23:37:12] 설치 버튼 클릭 +[2025-06-25 23:37:12] 선택된 브라우저: [{'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}] +[2025-06-25 23:37:12] wrmc_ext 폴더 경로: D:\py\AutoPercenty3_311\test\ext\wrmc_ext +[2025-06-25 23:37:12] 설치 베이스 디렉토리: C:\Users\Administrator\AppData\Local\my_extension_installer +[2025-06-25 23:37:12] Whale 기존 폴더 삭제: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:37:12] Whale 확장 폴더 복사 완료: C:\Users\Administrator\AppData\Local\my_extension_installer\whale_extension +[2025-06-25 23:37:12] 결과 UI 표시 완료 +[2025-06-25 23:37:12] Whale 확장 프로그램 설정페이지 오픈 시도 +[2025-06-25 23:37:12] 웨일 확장페이지 새창으로 오픈 +[2025-06-25 23:57:36] 설치 안내 메시지 표시 +[2025-06-26 11:54:08] 관리자 권한 체크: 1 +[2025-06-26 11:54:08] 프로그램 시작 +[2025-06-26 11:54:08] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] +[2025-06-26 17:28:21] 관리자 권한 체크: 1 +[2025-06-26 17:28:21] 프로그램 시작 +[2025-06-26 17:28:21] 탐지된 브라우저: [{'name': 'Chrome', 'display': 'Chrome', 'path': 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'url': 'chrome://extensions/'}, {'name': 'Whale', 'display': 'Whale (네이버 웨일)', 'path': 'C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe', 'url': 'whale://extensions/'}, {'name': 'Edge', 'display': 'Edge (마이크로소프트)', 'path': 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'url': 'edge://extensions/'}] diff --git a/main.py b/main.py new file mode 100644 index 0000000..ff5fb5f --- /dev/null +++ b/main.py @@ -0,0 +1,343 @@ +import sys +import os +import shutil +import subprocess +import ctypes +import psutil +import traceback +from pathlib import Path +from datetime import datetime + +from PySide6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox, QCheckBox, + QGroupBox, QSizePolicy, QScrollArea +) +from PySide6.QtCore import Qt +from PySide6.QtGui import QClipboard + +def get_resource_path(relative_path): + """PyInstaller로 만든 실행파일에서 리소스 경로를 올바르게 반환합니다.""" + try: + # PyInstaller가 생성한 임시 폴더 + base_path = sys._MEIPASS + except Exception: + # 개발 환경에서는 현재 스크립트의 디렉토리 + base_path = os.path.dirname(os.path.abspath(sys.argv[0])) + + return os.path.join(base_path, relative_path) + +def write_log(msg, exc_info=None): + try: + log_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "log.txt") + with open(log_path, "a", encoding="utf-8") as f: + now = datetime.now().strftime("[%Y-%m-%d %H:%M:%S] ") + f.write(now + msg + "\n") + if exc_info: + f.write(traceback.format_exc() + "\n") + except Exception as e: + print("로그 기록 실패:", e) + +def is_admin(): + try: + admin = ctypes.windll.shell32.IsUserAnAdmin() + write_log(f"관리자 권한 체크: {admin}") + return admin + except Exception: + write_log("관리자 권한 체크 중 오류", exc_info=True) + return False + +BROWSER_INFOS = [ + {"name": "Chrome", "display": "Chrome", "path": r"C:\Program Files\Google\Chrome\Application\chrome.exe", "url": "chrome://extensions/"}, + {"name": "Whale", "display": "Whale (네이버 웨일)", "path": r"C:\Program Files\Naver\Naver Whale\Application\whale.exe", "url": "whale://extensions/"}, + {"name": "Edge", "display": "Edge (마이크로소프트)", "path": r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", "url": "edge://extensions/"}, + {"name": "Edge2", "display": "Edge (마이크로소프트)", "path": r"C:\Program Files\Microsoft\Edge\Application\msedge.exe", "url": "edge://extensions/"} +] + +def get_install_base_dir(): + path = os.path.join(os.environ["LOCALAPPDATA"], "my_extension_installer") + write_log(f"설치 베이스 디렉토리: {path}") + return path + +def find_browsers(): + browser_list = [] + used = set() + for info in BROWSER_INFOS: + if os.path.exists(info["path"]) and info["name"] not in used: + browser_list.append({ + "name": info["name"] if info["name"] != "Edge2" else "Edge", + "display": info["display"], + "path": info["path"], + "url": info["url"] + }) + used.add(info["name"]) + write_log(f"탐지된 브라우저: {browser_list}") + return browser_list + +def open_browser_extensions(browser): + try: + write_log(f"{browser['name']} 확장 프로그램 설정페이지 오픈 시도") + if browser["name"] == "Chrome": + is_running = any('chrome.exe' in (p.name().lower() if hasattr(p, "name") else "") for p in psutil.process_iter()) + write_log(f"크롬 실행중 여부: {is_running}") + if not is_running: + subprocess.Popen([browser["path"], "--new-window", "chrome://extensions/"]) + write_log("크롬 확장페이지 새창으로 오픈") + else: + msg = ("크롬이 이미 실행중입니다!\n\n" + "주소창에 chrome://extensions/ 를 직접 입력하세요.") + QMessageBox.information(None, "수동 안내", msg) + write_log("크롬 실행중: 수동 안내") + elif browser["name"] == "Whale": + subprocess.Popen([browser["path"], "--new-window", "whale://extensions"]) + write_log("웨일 확장페이지 새창으로 오픈") + elif browser["name"] == "Edge": + subprocess.Popen([browser["path"], "--new-window", "edge://extensions"]) + write_log("엣지 확장페이지 새창으로 오픈") + else: + os.startfile(browser["path"]) + write_log(f"{browser['name']} 실행") + except Exception: + write_log(f"{browser['name']} 확장 프로그램 페이지 오픈 중 오류", exc_info=True) + +class ExtensionInstaller(QWidget): + def __init__(self): + super().__init__() + write_log("프로그램 시작") + self.setWindowTitle("🧩 크롬/웨일/엣지 확장 자동설치 마법사") + self.setMinimumSize(1100, 850) # 가로 900, 세로 700 + self.setStyleSheet(""" + QWidget { background: #20222B; color: #eef; font-size: 12px;} + QGroupBox { border: 2px solid #67c1f5; border-radius: 12px; margin-top: 14px; font-weight: bold; padding:13px; } + QPushButton { border-radius: 8px; background: #67c1f5; color: #181818; min-width: 130px; min-height: 44px; font-weight: bold; font-size: 12px; padding: 6px 16px;} + QPushButton:hover { background: #38b6ff; } + QLabel { font-size: 12px;} + QCheckBox { font-size: 12px;} + """) + try: + self.browsers = find_browsers() + except Exception: + self.browsers = [] + write_log("브라우저 탐색 실패", exc_info=True) + self.install_paths = {} + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + layout.setSpacing(20) + + title = QLabel("🧩 크롬/웨일/엣지 확장 자동설치 마법사") + title.setStyleSheet("font-size: 22px; font-weight: bold; color: #58b7ff; padding:20px 0 0 0") + layout.addWidget(title, alignment=Qt.AlignHCenter) + + guide = QLabel( + "설치 안내:
" + "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
" + "2. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
" + "3. 설치 후 브라우저별 단계별 안내를 참고하세요.
") + guide.setWordWrap(True) + guide.setStyleSheet("margin-bottom:12px;") + layout.addWidget(guide) + + browser_group = QGroupBox("설치할 브라우저 선택") + browser_layout = QHBoxLayout() + self.checkboxs = [] + for b in self.browsers: + cb = QCheckBox(f"{b['display']}") + cb.setChecked(False) + cb.setStyleSheet("margin-right:40px;") + cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.checkboxs.append(cb) + browser_layout.addWidget(cb) + browser_group.setLayout(browser_layout) + layout.addWidget(browser_group) + + self.install_btn = QPushButton("🚀 설치 시작") + self.install_btn.setMinimumHeight(54) + self.install_btn.setStyleSheet("background:#38b6ff; color:#222; font-size:17px;") + self.install_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.install_btn.clicked.connect(self.do_install) + layout.addWidget(self.install_btn) + + # 결과 부분 스크롤 지원(많이 길어질 경우) + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.result_group = QGroupBox("설치 완료 결과 (브라우저별 단계별 안내)") + self.result_group.setStyleSheet("QGroupBox { border: 2px solid #38b6ff; margin-top:18px; padding:24px; }") + self.result_layout = QVBoxLayout() + self.result_group.setLayout(self.result_layout) + self.result_group.hide() + self.scroll_area.setWidget(self.result_group) + layout.addWidget(self.scroll_area, stretch=1) + + help_label = QLabel("※ 복사 버튼 클릭시 클립보드에 저장됩니다. 폴더 경로는 꼭 붙여넣기 해주세요.\n" + "※ 크롬이 이미 실행중이면 주소창에 직접 chrome://extensions/ 입력 필요.") + help_label.setStyleSheet("color:#bbffaa; font-size:12px; margin:10px 0 0 0;") + help_label.setWordWrap(True) + layout.addWidget(help_label) + + self.setLayout(layout) + + def do_install(self): + write_log("설치 버튼 클릭") + DEVELOPER_MODE_GUIDE = { + "Chrome": "② 오른쪽 상단의 [개발자 모드]를 ON(체크)로 변경하세요.", + "Edge": "② 오른쪽 상단의 [개발자 모드]를 ON(체크)로 변경하세요.", + "Whale": "② 제일 하단의 [개발자 모드]를 ON(체크)로 변경하세요." + } + try: + checked = [cb.isChecked() for cb in self.checkboxs] + selected = [b for b, ch in zip(self.browsers, checked) if ch] + write_log(f"선택된 브라우저: {selected}") + if not selected: + QMessageBox.warning(self, "경고", "적어도 하나의 브라우저를 선택하세요.") + write_log("브라우저 미선택 - 경고") + return + + ext_folder = get_resource_path("wrmc_ext") + write_log(f"wrmc_ext 폴더 경로: {ext_folder}") + if not os.path.exists(ext_folder): + QMessageBox.critical(self, "오류", f"확장 프로그램 폴더가 없습니다:\n{ext_folder}") + write_log("wrmc_ext 폴더 없음 - 오류") + return + + self.install_paths = {} + for b in selected: + base_dir = get_install_base_dir() + os.makedirs(base_dir, exist_ok=True) + folder_name = f"{b['name'].lower()}_extension" + target_dir = os.path.join(base_dir, folder_name) + try: + if os.path.exists(target_dir): + shutil.rmtree(target_dir) + write_log(f"{b['name']} 기존 폴더 삭제: {target_dir}") + shutil.copytree(ext_folder, target_dir) + self.install_paths[b['name']] = target_dir + write_log(f"{b['name']} 확장 폴더 복사 완료: {target_dir}") + except Exception as e: + QMessageBox.critical(self, "오류", f"{b['name']} 설치 폴더 복사 실패: {e}") + write_log(f"{b['name']} 설치 폴더 복사 실패", exc_info=True) + return + + while self.result_layout.count(): + item = self.result_layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.deleteLater() + + # 단계별 안내(글자 안잘리도록, Expanding 적용, WordWrap, padding/여백 강화) + for b in selected: + browser_name = b['name'] + browser_disp = b['display'] + path = self.install_paths.get(browser_name, '') + url = b['url'] + box = QGroupBox(f"{browser_disp} 단계별 안내") + box.setStyleSheet("QGroupBox {margin-bottom:24px;}") + box_layout = QVBoxLayout() + box_layout.setSpacing(18) + # 1번 확장주소 복사 + row1 = QHBoxLayout() + lbl1 = QLabel("① 확장설정 페이지로 이동 :") + lbl1.setWordWrap(True) + lbl1.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + url_btn = QPushButton("확장주소 복사") + url_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + url_btn.setMinimumWidth(160) + url_lbl = QLabel(url) + url_lbl.setStyleSheet("color:#5be0ff; font-weight:bold;") + url_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse) + url_lbl.setWordWrap(True) + url_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + row1.addWidget(lbl1) + row1.addWidget(url_btn) + row1.addWidget(url_lbl) + row1.addStretch() + # 2번 개발자모드 체크 + row2 = QLabel(DEVELOPER_MODE_GUIDE.get(browser_name, "② [개발자 모드] 위치를 찾아 ON(체크)로 변경하세요.")) + row2.setTextFormat(Qt.RichText) + row2.setWordWrap(True) + # 3번 압축해제된 확장프로그램 로드 + row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 버튼 클릭") + row3.setTextFormat(Qt.RichText) + row3.setWordWrap(True) + # 4번 폴더 선택 및 복사 + row4 = QHBoxLayout() + lbl4 = QLabel("④ 아래 폴더 경로 복사→ [폴더 선택]에 붙여넣기 -> 아무것도 안나오지만 선택버튼 클릭") + lbl4.setWordWrap(True) + lbl4.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + path_btn = QPushButton("폴더 경로 복사") + path_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + path_btn.setMinimumWidth(160) + path_lbl = QLabel(path) + path_lbl.setStyleSheet("color:#eaff86; font-weight:bold;") + path_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse) + path_lbl.setWordWrap(True) + path_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + # 5번 확장프로그램 아이콘을 클릭하여 로그인 + row5 = QLabel("⑤ 주소창 옆 확장프로그램 아이콘을 클릭하여 로그인") + row5.setTextFormat(Qt.RichText) + row5.setWordWrap(True) + + row4.addWidget(lbl4) + row4.addWidget(path_btn) + row4.addWidget(path_lbl) + row4.addStretch() + + url_btn.clicked.connect(lambda checked, u=url: self.copy_to_clipboard(u, "확장주소")) + path_btn.clicked.connect(lambda checked, p=path: self.copy_to_clipboard(p, "폴더 경로")) + + box_layout.addLayout(row1) + box_layout.addWidget(row2) + box_layout.addWidget(row3) + box_layout.addLayout(row4) + box_layout.addWidget(row5) + box.setLayout(box_layout) + self.result_layout.addWidget(box) + + self.result_group.show() + write_log("결과 UI 표시 완료") + + for b in selected: + open_browser_extensions(b) + + msg = ( + "설치가 완료되었습니다!\n\n" + "브라우저별로 ①~④ 단계대로 진행하세요.\n" + "※ 크롬이 이미 실행중이면 주소창에 직접 chrome://extensions/ 입력 필요!" + ) + QMessageBox.information(self, "설치 안내", msg) + write_log("설치 안내 메시지 표시") + + except Exception: + write_log("설치 프로세스 전체 예외 발생", exc_info=True) + QMessageBox.critical(self, "오류", "예상치 못한 오류가 발생했습니다. log.txt를 확인해 주세요.") + + def copy_to_clipboard(self, value, kind): + try: + clipboard = QApplication.clipboard() + clipboard.setText(value, QClipboard.Clipboard) + write_log(f"{kind} 복사: {value}") + QMessageBox.information(self, "복사 완료", f"{kind}가 클립보드에 복사되었습니다:\n{value}") + except Exception: + write_log(f"{kind} 복사 함수 예외", exc_info=True) + QMessageBox.critical(self, "오류", f"{kind} 복사 중 오류가 발생했습니다.") + +if __name__ == "__main__": + try: + if not is_admin(): + QMessageBox.critical(None, "권한 부족", "이 프로그램은 관리자 권한으로 실행해야 할 수 있습니다.\n(브라우저 접근/복사 등)") + write_log("관리자 권한이 아님 - 재실행") + if getattr(sys, 'frozen', False): + script_path = sys.argv[0] + else: + script_path = os.path.abspath(__file__) + ctypes.windll.shell32.ShellExecuteW( + None, "runas", sys.executable, f'"{script_path}"', None, 1) + sys.exit(0) + + app = QApplication(sys.argv) + window = ExtensionInstaller() + window.show() + sys.exit(app.exec()) + except Exception: + write_log("메인 루프 전체 예외", exc_info=True) + raise diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8d9ac70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyside6 +psutil diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js new file mode 100644 index 0000000..9c53e72 --- /dev/null +++ b/wrmc_ext/background.js @@ -0,0 +1,2427 @@ +// background.js (Service Worker) + +chrome.runtime.onInstalled.addListener(() => { + // 컨텍스트 메뉴를 개별적으로 생성 (단축키 포함) + chrome.contextMenus.create({ + id: "searchTrademark", + title: "지재권 검색 (Ctrl+Shift+S)", + contexts: ["selection"] + }); + + chrome.contextMenus.create({ + id: "multiTranslate", + title: "멀티번역 (Ctrl+Shift+E)", + contexts: ["selection"] + }); + + chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); + + // 새 어록 감지 알람 생성 (1분마다) + chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); + + // 초기 마지막 확인 시간 설정 + chrome.storage.local.set({ lastSayingsCheck: Date.now() }); +}); + +// 단축키 명령어 처리 +chrome.commands.onCommand.addListener(async (command) => { + console.log(`[background.js] 단축키 명령어 실행: ${command}`); + + try { + // 현재 활성 탭 가져오기 + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab) { + console.error('[background.js] 활성 탭을 찾을 수 없습니다'); + return; + } + + // 선택된 텍스트 가져오기 + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + function: () => { + return window.getSelection().toString().trim(); + } + }); + + const selectedText = results[0]?.result; + if (!selectedText) { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '텍스트 선택 필요', + message: '먼저 텍스트를 선택해주세요.' + }); + return; + } + + // Content script 준비 확인 + await ensureContentScriptReady(tab.id); + + // 명령어에 따른 처리 + if (command === 'trademark-search') { + // 로딩 인디케이터 표시 + try { + await chrome.tabs.sendMessage(tab.id, { + action: "showLoading", + message: `🔄 "${selectedText.substring(0, 20)}${selectedText.length > 20 ? '...' : ''}" 지재권 검색 중...` + }); + } catch (loadingError) { + console.log('[단축키-지재권검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message); + } + + await handleTrademarkSearch(selectedText, tab); + + // 로딩 인디케이터 제거 + try { + await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" }); + } catch (e) { + console.log('[단축키-지재권검색] 로딩 인디케이터 제거 실패 (무시):', e.message); + } + + } else if (command === 'multi-translate') { + // 로딩 인디케이터 표시 + try { + await chrome.tabs.sendMessage(tab.id, { + action: "showLoading", + message: `🔄 "${selectedText.substring(0, 20)}${selectedText.length > 20 ? '...' : ''}" 번역 중...` + }); + } catch (loadingError) { + console.log('[단축키-멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message); + } + + await handleMultiTranslate({ selectionText: selectedText }); + + // 로딩 인디케이터 제거 + try { + await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" }); + } catch (e) { + console.log('[단축키-멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message); + } + + } else if (command === 'korean-to-chinese') { + await handleKoreanToChinese(selectedText, tab); + } + + } catch (error) { + console.error(`[background.js] 단축키 처리 중 오류:`, error); + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '오류 발생', + message: '단축키 처리 중 문제가 발생했습니다.' + }); + } +}); + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === "keepAlive") { + console.log("[background.js] 서비스 워커 유지 알람 실행됨"); + } else if (alarm.name === "checkNewSayings") { + checkForNewSayings(); + } +}); + +// 새 어록 확인 함수 +async function checkForNewSayings() { + try { + console.log("[background.js] 새 어록 확인 시작"); + + // Chrome 확장 프로그램 컨텍스트 확인 + if (!chrome || !chrome.storage || !chrome.storage.local) { + console.error("[background.js] Chrome 확장 프로그램 API에 접근할 수 없습니다."); + return; + } + + // 마지막 확인 시간 가져오기 (안전한 방식) + let lastSayingsCheck; + try { + const result = await chrome.storage.local.get("lastSayingsCheck"); + lastSayingsCheck = result.lastSayingsCheck; + } catch (storageError) { + console.error("[background.js] 스토리지 접근 오류:", storageError); + lastSayingsCheck = Date.now() - 60000; // 기본값: 1분 전 + } + + const lastCheckTime = lastSayingsCheck || Date.now() - 60000; + + // 액세스 토큰 가져오기 (안전한 방식) + let access_token; + try { + const result = await chrome.storage.local.get("access_token"); + access_token = result.access_token; + } catch (storageError) { + console.error("[background.js] 토큰 스토리지 접근 오류:", storageError); + return; + } + + if (!access_token) { + console.log("[background.js] 액세스 토큰이 없어 새 어록 확인을 건너뜁니다."); + return; + } + + // 올바른 Supabase URL과 헤더 사용 + const SUPABASE_URL = "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + // 새 어록 API 호출 + const apiUrl = `${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${new Date(lastCheckTime).toISOString()}&admin_approval=eq.true&order=created_at.desc`; + + console.log("[background.js] API 호출:", apiUrl); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${access_token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const newSayings = await response.json(); + + if (newSayings && newSayings.length > 0) { + console.log(`[background.js] ${newSayings.length}개의 새 어록을 발견했습니다.`); + + // 브라우저 알림 표시 (안전한 방식) + try { + if (chrome.notifications) { + chrome.notifications.create('newSayings', { + type: 'basic', + iconUrl: 'icon.png', + title: '새 어록 알림', + message: `${newSayings.length}개의 새로운 어록이 등록되었습니다.`, + buttons: [ + { title: '확인하기' }, + { title: '나중에' } + ] + }); + } + } catch (notificationError) { + console.error("[background.js] 알림 생성 오류:", notificationError); + } + + // 새 어록 데이터를 스토리지에 저장 (안전한 방식) + try { + await chrome.storage.local.set({ + pendingNewSayings: newSayings, + hasNewSayings: true + }); + console.log("[background.js] 새 어록 데이터 스토리지 저장 완료"); + } catch (storageError) { + console.error("[background.js] 새 어록 데이터 저장 오류:", storageError); + } + } else { + console.log("[background.js] 새 어록이 없습니다."); + } + + // 마지막 확인 시간 업데이트 (안전한 방식) + try { + await chrome.storage.local.set({ lastSayingsCheck: Date.now() }); + console.log("[background.js] 마지막 확인 시간 업데이트 완료"); + } catch (storageError) { + console.error("[background.js] 마지막 확인 시간 업데이트 오류:", storageError); + } + + } else { + const errorText = await response.text(); + console.error("[background.js] 새 어록 확인 실패:", response.status, response.statusText, errorText); + } + + } catch (error) { + console.error("[background.js] 새 어록 확인 중 전체 오류:", error); + + // 오류 유형별 처리 + if (error.message.includes('Could not establish connection')) { + console.error("[background.js] Chrome 확장 프로그램 연결 오류 - 확장 프로그램을 다시 로드하세요."); + } else if (error.message.includes('Receiving end does not exist')) { + console.error("[background.js] 메시지 수신자가 존재하지 않음 - 페이지를 새로고침하세요."); + } else if (error.name === 'TypeError' && error.message.includes('fetch')) { + console.error("[background.js] 네트워크 연결 오류 - 서버 연결을 확인하세요."); + } + } +} + +// 알림 클릭 처리 +chrome.notifications.onClicked.addListener((notificationId) => { + if (notificationId === 'newSayings') { + // 어록 관리 페이지 열기 + chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') }); + chrome.notifications.clear(notificationId); + } else if (notificationId === 'multiTranslateLogin') { + // 로그인 필요 알림 클릭 시 팝업 열기 + chrome.action.openPopup(); + chrome.notifications.clear(notificationId); + } else if (notificationId === 'multiTranslateLimit') { + // API 한도 초과 알림 클릭 시 팝업 열기 + chrome.action.openPopup(); + chrome.notifications.clear(notificationId); + } else if (notificationId === 'multiTranslateUserInfo') { + // 사용자 정보 오류 알림 클릭 시 팝업 열기 + chrome.action.openPopup(); + chrome.notifications.clear(notificationId); + } else if (notificationId === 'multiTranslateError') { + // 번역 오류 알림 클릭 시 알림만 제거 + chrome.notifications.clear(notificationId); + } else if (notificationId === 'multiTranslateBasic') { + // 기본 회원 안내 알림 클릭 시 알림만 제거 + chrome.notifications.clear(notificationId); + } else if (notificationId === 'multiTranslateException') { + // 예외 오류 알림 클릭 시 알림만 제거 + chrome.notifications.clear(notificationId); + } +}); + +// 알림 버튼 클릭 처리 +chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => { + if (notificationId === 'newSayings') { + if (buttonIndex === 0) { // 확인하기 + chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') }); + } + chrome.notifications.clear(notificationId); + } +}); + +// 컨텍스트 메뉴 클릭 시 처리 +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + const keyword = info.selectionText.trim(); + if (!keyword) return; + + // 지재권 검색 처리 + if (info.menuItemId === "searchTrademark") { + try { + // Content script 준비 확인 + await ensureContentScriptReady(tab.id); + + // 로딩 인디케이터 표시 + await chrome.tabs.sendMessage(tab.id, { + action: "showLoading", + message: `🔄 "${keyword.substring(0, 20)}${keyword.length > 20 ? '...' : ''}" 지재권 검색 중...` + }); + } catch (loadingError) { + console.log('[지재권 검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message); + } + + await handleTrademarkSearch(keyword, tab); + + // 로딩 인디케이터 제거 + try { + await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" }); + } catch (e) { + console.log('[지재권 검색] 로딩 인디케이터 제거 실패 (무시):', e.message); + } + } + + // 멀티번역 처리 + if (info.menuItemId === "multiTranslate") { + try { + // Content script 준비 확인 + await ensureContentScriptReady(tab.id); + + // 로딩 인디케이터 표시 + await chrome.tabs.sendMessage(tab.id, { + action: "showLoading", + message: `🔄 "${keyword.substring(0, 20)}${keyword.length > 20 ? '...' : ''}" 번역 중...` + }); + } catch (loadingError) { + console.log('[멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message); + } + + await handleMultiTranslate(info); + + // 로딩 인디케이터 제거 + try { + await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" }); + } catch (e) { + console.log('[멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message); + } + } +}); + +// 기존 지재권 검색 함수로 분리 +async function handleTrademarkSearch(keyword, tab) { + try { + // 1. 토큰 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "로그인 필요", + message: "지재권 검색을 사용하려면 먼저 로그인하세요." + }); + return; + } + + // 2. API 호출량 증가 및 한도 확인 + console.log('[지재권 검색] API 호출량 확인 시작'); + const apiCallResult = await incrementApiCallsAndCheckLimit(); + + if (!apiCallResult.success) { + console.error('[지재권 검색] API 호출량 한도 초과:', apiCallResult.error); + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "API 호출 한도 초과", + message: apiCallResult.error + }); + return; + } + + console.log('[지재권 검색] API 호출량 확인 완료:', { + current: apiCallResult.current, + limit: apiCallResult.limit, + remaining: apiCallResult.remaining + }); + + // 3. 사용자 정보 및 회원등급 확인 + const SUPABASE_URL = "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + // 사용자 기본 정보 가져오기 (토큰 검증) + const authUrl = `${SUPABASE_URL}/auth/v1/user`; + const authRes = await fetch(authUrl, { + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!authRes.ok) { + const errorText = await authRes.text(); + console.error("[지재권 검색] 토큰 검증 실패:", authRes.status, errorText); + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "인증 오류", + message: "세션이 만료되었습니다. 다시 로그인해주세요." + }); + return; + } + + const authUser = await authRes.json(); + console.log("[지재권 검색] 사용자 인증 성공:", authUser.email); + + // 사용자 상세 정보 및 회원등급 확인 (users 테이블에서 직접 조회) + const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + const detailsRes = await fetch(detailsUrl, { + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!detailsRes.ok) { + const errorText = await detailsRes.text(); + console.error("[지재권 검색] 사용자 정보 조회 실패:", detailsRes.status, errorText); + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "사용자 정보 오류", + message: "사용자 정보를 가져올 수 없습니다." + }); + return; + } + + const detailsData = await detailsRes.json(); + const userDetails = detailsData[0]; + + if (!userDetails) { + console.error("[지재권 검색] 사용자 정보 없음"); + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "사용자 정보 없음", + message: "사용자 정보를 찾을 수 없습니다." + }); + return; + } + + // 4. 회원등급 확인 (premium, vip만 허용) + const membershipLevel = userDetails.membership_level; + console.log("[지재권 검색] 사용자 회원등급:", membershipLevel); + + // premium 또는 vip가 아닌 경우 접근 거부 + if (!membershipLevel || (membershipLevel !== 'premium' && membershipLevel !== 'vip')) { + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "권한 부족", + message: `지재권 검색은 프리미엄/VIP 회원만 사용할 수 있습니다.\n현재 등급: ${membershipLevel || '기본'}` + }); + return; + } + + // 5. 권한이 있는 경우 지재권 검색 실행 + console.log(`[지재권 검색] 사용자: ${authUser.email}, 등급: ${membershipLevel}, 키워드: ${keyword}, API 호출: ${apiCallResult.current}/${apiCallResult.limit}`); + + const url = buildMarkInfoUrl(keyword); + + // 키워드 검색 실행 + const response = await fetch(url); + if (!response.ok) { + throw new Error(`네트워크 오류: ${response.status}`); + } + + const html = await response.text(); + const match = /]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/i.exec(html); + if (!match) { + throw new Error("__NUXT_DATA__ 태그를 찾을 수 없습니다."); + } + + let jsonString = match[1]; + let globalData; + try { + globalData = JSON.parse(jsonString); + } catch (e) { + throw new Error("JSON 파싱 실패: " + e.toString()); + } + + // 키워드 검색 결과 파싱 + const keywordResults = parseSearchResults(globalData); + if (!Array.isArray(keywordResults) || keywordResults.length === 0) { + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "검색 결과 없음", + message: `'${keyword}'에 대한 지재권 검색 결과가 없습니다.` + }); + return; + } + + // 최대 10건까지만 처리 + const limitedResults = keywordResults.slice(0, 10); + + // 각 결과의 출원번호로 상세 조회 + const detailPromises = limitedResults.map(result => { + const appNum = result.registration_info.applicationNum; + return fetchDetailInfo(appNum) + .then(detail => { + result.detail = detail; + return result; + }) + .catch(err => { + result.detailError = err.toString(); + return result; + }); + }); + + const allResults = await Promise.all(detailPromises); + + // 결과를 컨텐츠 스크립트로 전송 + try { + console.log(`[지재권 검색] 탭 ${tab.id}로 결과 전송 시도, 결과 개수: ${allResults.length}`); + + // 탭이 여전히 유효한지 확인 + const tabInfo = await chrome.tabs.get(tab.id); + if (!tabInfo || tabInfo.status !== 'complete') { + throw new Error('탭이 준비되지 않았습니다'); + } + + // 콘텐츠 스크립트 상태 확인을 위한 핑 테스트 + let contentScriptReady = false; + try { + console.log('[지재권 검색] 콘텐츠 스크립트 상태 확인 중...'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('핑 테스트 타임아웃')); + }, 2000); + + chrome.tabs.sendMessage(tab.id, { action: "ping" }, (response) => { + clearTimeout(timeout); + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + console.log('[지재권 검색] 콘텐츠 스크립트 준비 완료'); + contentScriptReady = true; + resolve(response); + } + }); + }); + } catch (pingError) { + console.log('[지재권 검색] 콘텐츠 스크립트 미준비, 동적 주입 필요:', pingError.message); + contentScriptReady = false; + } + + // 콘텐츠 스크립트가 준비되지 않은 경우 미리 주입 + if (!contentScriptReady) { + try { + console.log('[지재권 검색] 콘텐츠 스크립트 사전 주입 시작'); + + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ['content.js'] + }); + + console.log('[지재권 검색] 콘텐츠 스크립트 사전 주입 완료'); + + // 주입 후 대기 + await new Promise(resolve => setTimeout(resolve, 500)); + + } catch (preInjectionError) { + console.error('[지재권 검색] 사전 주입 실패:', preInjectionError); + } + } + + // 메시지 전송 시도 + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('메시지 전송 타임아웃')); + }, 5000); + + chrome.tabs.sendMessage(tab.id, { + action: "showTooltip", + detailInfo: allResults, + keyword: keyword + }, (response) => { + clearTimeout(timeout); + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + console.log('[지재권 검색] 메시지 전송 성공, 응답:', response); + resolve(response); + } + }); + }); + + console.log('[지재권 검색] 결과 전송 완료'); + + } catch (contentError) { + console.error('[지재권 검색] 컨텐츠 스크립트 오류:', contentError); + + // 컨텐츠 스크립트 오류 시 알림으로 대체 + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "지재권 검색 결과", + message: `'${keyword}' 검색 완료 (결과: ${allResults.length}건)\n상세 정보는 브라우저 콘솔을 확인하세요.` + }); + + console.log('[지재권 검색] 상세 결과:', allResults); + } + + } catch (error) { + console.error('[지재권 검색] 오류:', error); + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "지재권 검색 오류", + message: error.message || "검색 중 오류가 발생했습니다." + }); + } +} + +// 멀티번역 처리 함수 +async function handleMultiTranslate(info) { + const selectedText = info.selectionText?.trim(); + if (!selectedText) return; + + console.log('[background.js] 멀티번역 요청:', selectedText); + + try { + // 토큰 검증 + const token = await getStoredToken(); + if (!token) { + console.log('[background.js] 토큰 없음 - 로그인 필요 알림 표시'); + chrome.notifications.create('multiTranslateLogin', { + type: 'basic', + iconUrl: 'icon.png', + title: '멀티번역', + message: '로그인이 필요합니다. 확장 프로그램을 클릭하여 로그인해 주세요.' + }); + return; + } + + // API 호출량 증가 및 한도 확인 + console.log('[멀티번역] API 호출량 확인 시작'); + const apiCallResult = await incrementApiCallsAndCheckLimit(); + + if (!apiCallResult.success) { + console.error('[멀티번역] API 호출량 한도 초과:', apiCallResult.error); + chrome.notifications.create('multiTranslateLimit', { + type: 'basic', + iconUrl: 'icon.png', + title: 'API 호출 한도 초과', + message: apiCallResult.error + }); + return; + } + + console.log('[멀티번역] API 호출량 확인 완료:', { + current: apiCallResult.current, + limit: apiCallResult.limit, + remaining: apiCallResult.remaining + }); + + // 사용자 정보 가져오기 + const userInfo = await fetchUserInfo(token); + if (!userInfo) { + console.log('[background.js] 사용자 정보 없음 - 재로그인 필요 알림 표시'); + chrome.notifications.create('multiTranslateUserInfo', { + type: 'basic', + iconUrl: 'icon.png', + title: '멀티번역', + message: '사용자 정보를 가져올 수 없습니다. 다시 로그인해 주세요.' + }); + return; + } + + const userLevel = userInfo.membership_level || 'basic'; + console.log(`[background.js] 사용자 레벨: ${userLevel}, API 호출: ${apiCallResult.current}/${apiCallResult.limit}`); + + // 회원등급별 접근 권한 및 안내 메시지 + let accessMessage = ''; + switch (userLevel.toLowerCase()) { + case 'basic': + accessMessage = '기본 회원: Google, MyMemory 번역 엔진 사용 가능'; + break; + case 'premium': + accessMessage = '프리미엄 회원: Google, MyMemory, DeepL 번역 엔진 사용 가능'; + break; + case 'vip': + accessMessage = 'VIP 회원: 모든 번역 엔진 사용 가능 (ChatGPT, Gemini 포함)'; + break; + default: + accessMessage = '기본 회원: 제한된 번역 엔진 사용 가능'; + } + + // 회원등급별 사용 가능한 번역 엔진 결정 + const availableEngines = await getAvailableEngines(userLevel); + console.log(`[background.js] 사용 가능한 번역 엔진:`, availableEngines); + + // 번역 실행 + const translationResults = await performTranslations(selectedText, availableEngines); + + // 결과를 content script로 전송 + const tabs = await chrome.tabs.query({active: true, currentWindow: true}); + if (tabs[0]) { + await ensureContentScriptReady(tabs[0].id); + + chrome.tabs.sendMessage(tabs[0].id, { + action: "showTranslationTooltip", + originalText: selectedText, + results: translationResults, + userLevel: userLevel, + accessMessage: accessMessage + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[background.js] 번역 결과 전송 실패:', chrome.runtime.lastError); + chrome.notifications.create('multiTranslateError', { + type: 'basic', + iconUrl: 'icon.png', + title: '멀티번역', + message: '번역 결과를 표시할 수 없습니다.' + }); + } else { + console.log('[background.js] 번역 결과 전송 성공'); + + // 기본 회원인 경우 추가 안내 알림 + if (userLevel.toLowerCase() === 'basic') { + chrome.notifications.create('multiTranslateBasic', { + type: 'basic', + iconUrl: 'icon.png', + title: '멀티번역 완료', + message: `${accessMessage}\n프리미엄 업그레이드 시 더 많은 번역 엔진을 이용하실 수 있습니다.` + }); + } + } + }); + } + + } catch (error) { + console.error('[background.js] 멀티번역 처리 중 오류:', error); + chrome.notifications.create('multiTranslateException', { + type: 'basic', + iconUrl: 'icon.png', + title: '멀티번역 오류', + message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' + }); + } +} + +// Content Script가 준비되었는지 확인하고 필요시 주입 +async function ensureContentScriptReady(tabId) { + try { + // 핑 테스트로 content script 상태 확인 + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('핑 테스트 타임아웃')); + }, 1000); + + chrome.tabs.sendMessage(tabId, { action: "ping" }, (response) => { + clearTimeout(timeout); + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + console.log('[background.js] Content script 준비 완료'); + resolve(response); + } + }); + }); + } catch (pingError) { + console.log('[background.js] Content script 미준비, 주입 시도:', pingError.message); + + try { + // Content script 주입 + await chrome.scripting.executeScript({ + target: { tabId: tabId }, + files: ['content.js'] + }); + + console.log('[background.js] Content script 주입 완료'); + + // 주입 후 잠시 대기 + await new Promise(resolve => setTimeout(resolve, 500)); + + } catch (injectionError) { + console.error('[background.js] Content script 주입 실패:', injectionError); + throw injectionError; + } + } +} + +// 회원등급별 사용 가능한 번역 엔진 반환 (설정 고려) +async function getAvailableEngines(userLevel) { + const enginesByLevel = { + 'basic': ['google', 'mymemory'], + 'premium': ['google', 'mymemory', 'deepl'], + 'vip': ['google', 'mymemory', 'deepl', 'openai'] // gemini 제거 + }; + + // 소문자로 변환하여 비교 + const normalizedLevel = (userLevel || 'basic').toLowerCase(); + const levelEngines = enginesByLevel[normalizedLevel] || enginesByLevel.basic; + + console.log(`[background.js] 회원등급 정규화: ${userLevel} -> ${normalizedLevel}`); + console.log(`[background.js] 등급별 사용 가능한 엔진:`, levelEngines); + + // 사용자 설정 확인 + try { + const result = await chrome.storage.local.get('translation_engine_settings'); + const userSettings = result.translation_engine_settings || {}; + + console.log('[background.js] 사용자 번역 엔진 설정:', userSettings); + + // 등급별 사용 가능한 엔진 중에서 사용자가 활성화한 엔진만 필터링 + const enabledEngines = levelEngines.filter(engine => { + const isEnabled = userSettings[engine] !== false; // 기본값은 true + console.log(`[background.js] ${engine} 엔진 활성화 상태:`, isEnabled); + return isEnabled; + }); + + console.log(`[background.js] 최종 사용 가능한 엔진:`, enabledEngines); + + // 최소 1개 엔진은 활성화되어야 함 + if (enabledEngines.length === 0) { + console.warn('[background.js] 활성화된 번역 엔진이 없어 기본 엔진(Google) 사용'); + return ['google']; + } + + return enabledEngines; + + } catch (error) { + console.error('[background.js] 번역 엔진 설정 로드 실패:', error); + return levelEngines; // 오류 시 등급별 기본 엔진 사용 + } +} + +// 여러 번역 엔진으로 번역 실행 +async function performTranslations(text, engines) { + const results = []; + + // 각 엔진별로 병렬 번역 실행 + const translatePromises = engines.map(async (engine) => { + try { + const result = await translateWithEngine(text, engine); + return { + engine: engine, + success: true, + translatedText: result + }; + } catch (error) { + console.error(`[background.js] ${engine} 번역 실패:`, error); + return { + engine: engine, + success: false, + error: error.message || '번역 실패' + }; + } + }); + + const translationResults = await Promise.all(translatePromises); + return translationResults; +} + +// 개별 번역 엔진별 번역 함수 +async function translateWithEngine(text, engine) { + switch (engine) { + case 'google': + return await translateWithGoogle(text); + case 'mymemory': + return await translateWithMyMemory(text); + case 'deepl': + return await translateWithDeepL(text); + case 'openai': + return await translateWithOpenAI(text); + case 'gemini': + return await translateWithGemini(text); + default: + throw new Error(`지원하지 않는 번역 엔진: ${engine}`); + } +} + +// Google 번역 +async function translateWithGoogle(text) { + const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=ko&dt=t&q=${encodeURIComponent(text)}`); + + if (!response.ok) { + throw new Error('Google 번역 요청 실패'); + } + + const data = await response.json(); + + if (!data || !data[0] || !data[0][0] || !data[0][0][0]) { + throw new Error('Google 번역 응답 형식 오류'); + } + + return data[0][0][0]; +} + +// MyMemory 번역 (무료) +async function translateWithMyMemory(text) { + // 언어 감지를 위한 개선된 로직 + const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(text); + const isChinese = /[\u4e00-\u9fff]/.test(text); + const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(text.trim()); + + let sourceLang, targetLang; + + if (isKorean) { + sourceLang = 'ko'; + targetLang = 'zh'; // 한국어 -> 중국어 + } else if (isChinese) { + sourceLang = 'zh'; + targetLang = 'ko'; // 중국어 -> 한국어 + } else if (isEnglish) { + sourceLang = 'en'; + targetLang = 'ko'; // 영어 -> 한국어 + } else { + // 기본값: 영어 -> 한국어 + sourceLang = 'en'; + targetLang = 'ko'; + } + + const response = await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`); + + if (!response.ok) { + throw new Error('MyMemory 번역 요청 실패'); + } + + const data = await response.json(); + + if (!data || !data.responseData || !data.responseData.translatedText) { + throw new Error('MyMemory 번역 응답 형식 오류'); + } + + return data.responseData.translatedText; +} + +// DeepL 번역 +async function translateWithDeepL(text) { + console.log('[background.js] DeepL 번역 시작:', { textLength: text.length }); + + const apiKey = await getApiKey('deepl'); + if (!apiKey || !apiKey.authKey) { + console.error('[background.js] DeepL API 키가 설정되지 않았습니다:', apiKey); + throw new Error('DeepL API 키가 설정되지 않았습니다'); + } + + console.log('[background.js] DeepL API 키 확인 완료'); + + try { + // DeepL API 키 형식에 따라 엔드포인트 결정 + const isFreeKey = apiKey.authKey.endsWith(':fx'); + const apiUrl = isFreeKey + ? 'https://api-free.deepl.com/v2/translate' + : 'https://api.deepl.com/v2/translate'; + + console.log(`[background.js] DeepL API 엔드포인트: ${apiUrl} (Free Key: ${isFreeKey})`); + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `DeepL-Auth-Key ${apiKey.authKey}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `text=${encodeURIComponent(text)}&target_lang=KO` + }); + + console.log('[background.js] DeepL API 응답 상태:', { + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[background.js] DeepL API 에러 응답:', errorText); + throw new Error(`DeepL 번역 요청 실패 (${response.status}): ${errorText}`); + } + + const data = await response.json(); + console.log('[background.js] DeepL API 응답 데이터:', data); + + if (!data || !data.translations || !data.translations[0] || !data.translations[0].text) { + console.error('[background.js] DeepL 응답 형식 오류:', data); + throw new Error('DeepL 번역 응답 형식 오류'); + } + + console.log('[background.js] DeepL 번역 성공'); + return data.translations[0].text; + + } catch (error) { + console.error('[background.js] DeepL 번역 중 오류:', error); + throw error; + } +} + +// OpenAI (ChatGPT) 번역 +async function translateWithOpenAI(text) { + const apiKey = await getApiKey('openai'); + if (!apiKey || !apiKey.apiKey) { + throw new Error('OpenAI API 키가 설정되지 않았습니다'); + } + + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: '당신은 전문 번역가입니다. 주어진 텍스트를 한국어로 자연스럽게 번역하고 의미를 이해할수 있도록 의역도 추가해주세요.' + }, + { + role: 'user', + content: text + } + ], + max_tokens: 1000, + temperature: 0.3 + }) + }); + + if (!response.ok) { + throw new Error('OpenAI 번역 요청 실패'); + } + + const data = await response.json(); + + if (!data || !data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) { + throw new Error('OpenAI 번역 응답 형식 오류'); + } + + return data.choices[0].message.content.trim(); +} + +// Google Gemini 번역 +async function translateWithGemini(text) { + console.log('[background.js] Gemini 번역 시작:', { textLength: text.length }); + + const apiKey = await getApiKey('gemini'); + if (!apiKey || !apiKey.apiKey) { + console.error('[background.js] Gemini API 키가 설정되지 않았습니다:', apiKey); + throw new Error('Gemini API 키가 설정되지 않았습니다'); + } + + console.log('[background.js] Gemini API 키 확인 완료'); + + try { + // 더 간단한 프롬프트로 토큰 사용량 줄이기 + const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey.apiKey}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + contents: [{ + parts: [{ + text: `당신은 전문 번역가입니다. 주어진 텍스트를 한국어로 자연스럽게 번역하고 의미를 이해할수 있도록 의역도 추가해주세요.:\n\n${text}` + }] + }], + generationConfig: { + temperature: 0.1, + maxOutputTokens: 2048, + topP: 0.8, + topK: 10 + } + }) + }); + + console.log('[background.js] Gemini API 응답 상태:', { + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[background.js] Gemini API 에러 응답:', errorText); + throw new Error(`Gemini 번역 요청 실패 (${response.status}): ${errorText}`); + } + + const data = await response.json(); + console.log('[background.js] Gemini API 응답 데이터:', data); + + // 응답 구조 검사 및 텍스트 추출 + let translatedText = null; + + if (data && data.candidates && Array.isArray(data.candidates) && data.candidates.length > 0) { + const candidate = data.candidates[0]; + console.log('[background.js] Gemini 첫 번째 candidate:', candidate); + + // finishReason 확인 + if (candidate.finishReason === 'MAX_TOKENS') { + console.warn('[background.js] Gemini 응답이 토큰 제한으로 잘렸습니다'); + } + + // content.parts 구조 확인 + if (candidate.content && candidate.content.parts && Array.isArray(candidate.content.parts) && candidate.content.parts.length > 0) { + const part = candidate.content.parts[0]; + + // parts[0]이 문자열인 경우 + if (typeof part === 'string') { + translatedText = part; + } + // parts[0]이 객체이고 text 속성이 있는 경우 + else if (typeof part === 'object' && part.text) { + translatedText = part.text; + } + } + // content가 없고 바로 parts가 있는 경우 + else if (candidate.parts && Array.isArray(candidate.parts) && candidate.parts.length > 0) { + const part = candidate.parts[0]; + if (typeof part === 'string') { + translatedText = part; + } else if (typeof part === 'object' && part.text) { + translatedText = part.text; + } + } + // content가 없고 바로 text가 있는 경우 + else if (candidate.text) { + translatedText = candidate.text; + } + } + + if (!translatedText) { + console.error('[background.js] Gemini 응답에서 번역 텍스트를 찾을 수 없습니다:', data); + + // 디버깅을 위해 전체 응답 구조 로그 + if (data && data.candidates && data.candidates[0]) { + console.log('[background.js] Gemini candidate 구조:', JSON.stringify(data.candidates[0], null, 2)); + } + + throw new Error('Gemini 번역 응답에서 텍스트를 추출할 수 없습니다'); + } + + console.log('[background.js] Gemini 번역 성공:', translatedText.substring(0, 100) + '...'); + return translatedText.trim(); + + } catch (error) { + console.error('[background.js] Gemini 번역 중 오류:', error); + throw error; + } +} + +// API 키 저장소에서 가져오기 +async function getApiKey(service) { + try { + console.log(`[background.js] ${service} API 키 조회 시작`); + + const SUPABASE_URL = "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + // 먼저 간단한 테스트 호출 (인증 없이) + const testUrl = `${SUPABASE_URL}/rest/v1/api_keys?select=*&limit=1`; + console.log(`[background.js] 테스트 API 호출:`, testUrl); + + try { + const testResponse = await fetch(testUrl, { + method: 'GET', + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + console.log(`[background.js] 테스트 응답 상태:`, testResponse.status); + + if (testResponse.ok) { + const testData = await testResponse.json(); + console.log(`[background.js] 테스트 데이터:`, testData); + } else { + const testError = await testResponse.text(); + console.log(`[background.js] 테스트 오류:`, testError); + } + } catch (testErr) { + console.error(`[background.js] 테스트 호출 실패:`, testErr); + } + + // 이제 실제 인증된 호출 시도 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + console.error(`[background.js] Access token이 없습니다`); + return null; + } + + // Supabase에서 API 키 조회 + const apiUrl = `${SUPABASE_URL}/rest/v1/api_keys?select=*&source=eq.${service}&limit=1`; + + console.log(`[background.js] API 키 조회 URL:`, apiUrl); + console.log(`[background.js] 사용할 access_token:`, access_token ? access_token.substring(0, 20) + '...' : 'null'); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${access_token}`, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + } + }); + + console.log(`[background.js] API 응답 상태:`, response.status); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`[background.js] API 키 조회 실패: ${response.status}`, errorText); + return null; + } + + const apiKeys = await response.json(); + console.log(`[background.js] 조회된 API 키 개수:`, apiKeys?.length || 0); + + if (!apiKeys || apiKeys.length === 0) { + console.warn(`[background.js] ${service} API 키가 데이터베이스에 없습니다`); + return null; + } + + const apiKeyRecord = apiKeys[0]; + const apiKeyValue = apiKeyRecord.apikey; // 'api'에서 'apikey'로 변경 + + if (!apiKeyValue) { + console.error(`[background.js] ${service} API 키 값이 비어있습니다`); + return null; + } + + // 서비스별 API 키 구조 생성 + let formattedApiKey; + switch (service) { + case 'deepl': + formattedApiKey = { + authKey: apiKeyValue + }; + break; + case 'openai': + case 'gemini': + formattedApiKey = { + apiKey: apiKeyValue + }; + break; + default: + console.error(`[background.js] 지원하지 않는 서비스: ${service}`); + return null; + } + + console.log(`[background.js] ${service} API 키 로드 성공`); + return formattedApiKey; + + } catch (error) { + console.error(`[background.js] ${service} API 키 가져오기 실패:`, error); + return null; + } +} + +/* ===== 1. 키워드 검색 결과 파싱 (재귀적 인덱스 재활용 방식) ===== */ + +/** + * 키워드 검색 __NUXT_DATA__에서 등록정보(상표명, 출원번호 등)만 추출하여 결과 배열로 반환 + */ +function parseSearchResults(globalData) { + let results = []; + let i = 0; + while (i < globalData.length) { + let item = globalData[i]; + if (isPlainObject(item) && item.hasOwnProperty("applicationNum")) { + let regRes = extractRegistrationInfo(globalData, i); + let registration_info = regRes.registration_info; + i = regRes.nextIndex; + results.push({ registration_info }); + } else { + i++; + } + } + return results; +} + +/** + * 등록정보 추출 (키워드 검색용 – 재귀적 인덱스 재활용) + */ +function extractRegistrationInfo(globalData, startIndex) { + let regMapping = null; + let i = startIndex; + for (; i < globalData.length; i++) { + let item = globalData[i]; + if (isPlainObject(item) && item.hasOwnProperty("applicationNum")) { + regMapping = item; + break; + } + } + if (!regMapping) return { registration_info: {}, nextIndex: i }; + const registration_info = {}; + for (let key in regMapping) { + const ref = regMapping[key]; + if (typeof ref === "number" && ref < globalData.length) { + registration_info[key] = globalData[ref]; + } else { + registration_info[key] = ref; + } + } + return { registration_info, nextIndex: i + 1 }; +} + +/* ===== 2. 상세 검색 (출원번호 기반) – 단일 결과용, 단순 치환 방식 ===== */ + +/** + * 상세 조회: 주어진 출원번호(appNum)로 상세 페이지 __NUXT_DATA__를 가져와서 + * 등록정보, 권리정보, 출원인 정보를 추출합니다. + * (대리인 정보는 제외) + */ +function fetchDetailInfo(appNum) { + const detailUrl = `https://markinfo.kr/search/${appNum}`; + return fetch(detailUrl) + .then(resp => { + if (!resp.ok) throw new Error(`네트워크 오류: ${resp.status}`); + return resp.text(); + }) + .then(html => { + const match = /]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/i.exec(html); + if (!match) throw new Error("__NUXT_DATA__ 태그를 찾을 수 없습니다."); + let jsonString = match[1]; + let detailGlobalData; + try { + detailGlobalData = JSON.parse(jsonString); + } catch (e) { + throw new Error("JSON 파싱 실패: " + e.toString()); + } + if (!Array.isArray(detailGlobalData)) { + throw new Error("globalData가 배열이 아님"); + } + // 단일 결과이므로, mapping 객체 내 숫자값은 바로 치환 + const registration_info = detailExtractRegistrationInfo(detailGlobalData); + const rights_info = detailExtractRightsInfo(detailGlobalData); + const applicant_info = detailExtractApplicantInfo(detailGlobalData); + return { registration_info, rights_info, applicant_info }; + }); +} + +// 단일 상세 조회용 등록정보 추출 (숫자이면 한 번만 치환) +function detailExtractRegistrationInfo(globalData) { + let regMapping = null; + for (let item of globalData) { + if (isPlainObject(item) && item.hasOwnProperty("applicationNum")) { + regMapping = item; + break; + } + } + if (!regMapping) return {}; + const registration_info = {}; + for (let key in regMapping) { + const ref = regMapping[key]; + registration_info[key] = (typeof ref === "number" && ref < globalData.length) + ? globalData[ref] + : ref; + } + return registration_info; +} + +// 단일 상세 조회용 권리정보 추출 (숫자 치환만 진행) +function detailExtractRightsInfo(globalData) { + const rights_info = {}; + for (let item of globalData) { + if (isPlainObject(item) && + "classificationCode" in item && + "asignProductName" in item && + "asignProductNameEn" in item && + "similarCodes" in item && + !("applicationNum" in item)) { + let classificationCode = (typeof item.classificationCode === "number" && item.classificationCode < globalData.length) + ? globalData[item.classificationCode] + : item.classificationCode; + let asignProductName = (typeof item.asignProductName === "number" && item.asignProductName < globalData.length) + ? globalData[item.asignProductName] + : item.asignProductName; + let asignProductNameEn = (typeof item.asignProductNameEn === "number" && item.asignProductNameEn < globalData.length) + ? globalData[item.asignProductNameEn] + : item.asignProductNameEn; + let similarCodes = (typeof item.similarCodes === "number" && item.similarCodes < globalData.length) + ? globalData[item.similarCodes] + : item.similarCodes; + let designation = String(classificationCode); + if (!rights_info[designation]) { + rights_info[designation] = []; + } + rights_info[designation].push({ + asignProductName, + asignProductNameEn, + similarCodes + }); + } + } + return rights_info; +} + +// 단일 상세 조회용 출원인 정보 추출 – 오직 nationalCodeName와 applicantName만 표시, 그리고 출원날짜와 권리상태도 추가 +function detailExtractApplicantInfo(globalData) { + let mapping = {}; + for (let item of globalData) { + if (isPlainObject(item) && item.hasOwnProperty("applicantCode")) { + mapping["nationalCodeName"] = (typeof item["nationalCodeName"] === "number" && item["nationalCodeName"] < globalData.length) + ? globalData[item["nationalCodeName"]] + : item["nationalCodeName"]; + mapping["applicantName"] = (typeof item["applicantName"] === "number" && item["applicantName"] < globalData.length) + ? globalData[item["applicantName"]] + : item["applicantName"]; + // 추가: 출원날짜와 권리상태 (예: lastDisposalCodeName) + mapping["applicationDate"] = (typeof item["applicationDate"] === "number" && item["applicationDate"] < globalData.length) + ? globalData[item["applicationDate"]] + : item["applicationDate"]; + mapping["lastDisposalCodeName"] = (typeof item["lastDisposalCodeName"] === "number" && item["lastDisposalCodeName"] < globalData.length) + ? globalData[item["lastDisposalCodeName"]] + : item["lastDisposalCodeName"]; + break; + } + } + return { mapping }; +} + +/* ===== 유틸리티 함수 ===== */ +function isPlainObject(obj) { + return Object.prototype.toString.call(obj) === "[object Object]"; +} + +// 금지어 추가 메시지 리스너 (중복 제거됨 - 삭제 완료) + +// 금지어 추가 처리 함수 +async function handleAddBannedWord(message, sendResponse) { + try { + console.log('[background.js] 금지어 추가 요청 받음:', message.keyword, '등급:', message.grade); + + // 1. 토큰 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + sendResponse({ success: false, error: "로그인이 필요합니다." }); + return; + } + + // 2. 저장된 사용자 ID 확인 + const { user_id, user_email } = await chrome.storage.local.get(["user_id", "user_email"]); + if (!user_id) { + sendResponse({ success: false, error: "사용자 정보를 찾을 수 없습니다. 다시 로그인해주세요." }); + return; + } + + console.log("[금지어 추가] 저장된 사용자 정보 사용:", { user_id, user_email }); + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig(); + + // 3. 중복 체크 - 같은 사용자의 같은 banned_word가 이미 존재하는지 확인 + const duplicateCheckUrl = `${SUPABASE_URL}/rest/v1/user_banned_words?select=word_id&user_id=eq.${user_id}&banned_word=eq.${encodeURIComponent(message.keyword)}&limit=1`; + const duplicateCheckRes = await fetch(duplicateCheckUrl, { + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!duplicateCheckRes.ok) { + const errorText = await duplicateCheckRes.text(); + console.error("[금지어 추가] 중복 체크 실패:", duplicateCheckRes.status, errorText); + sendResponse({ success: false, error: "중복 체크 중 오류가 발생했습니다." }); + return; + } + + const duplicateData = await duplicateCheckRes.json(); + if (duplicateData && duplicateData.length > 0) { + console.log("[금지어 추가] 중복된 금지어 발견"); + sendResponse({ success: false, error: "이미 금지어 목록에 있는 단어입니다." }); + return; + } + + // 4. user_banned_words 테이블에 금지어 추가 + const bannedWordData = { + user_id: user_id, + banned_word: message.keyword, + grade: message.grade || '비허용', + created_at: new Date().toISOString() + }; + + console.log("[금지어 추가] user_banned_words에 삽입할 데이터:", bannedWordData); + + const bannedWordUrl = `${SUPABASE_URL}/rest/v1/user_banned_words`; + const bannedWordRes = await fetch(bannedWordUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + }, + body: JSON.stringify(bannedWordData) + }); + + if (!bannedWordRes.ok) { + const errorText = await bannedWordRes.text(); + console.error("[금지어 추가] user_banned_words 삽입 실패:", bannedWordRes.status, errorText); + sendResponse({ success: false, error: `금지어 추가 실패: ${errorText}` }); + return; + } + + const insertedBannedWord = await bannedWordRes.json(); + const bannedWordId = insertedBannedWord[0].word_id; + console.log("[금지어 추가] user_banned_words 삽입 성공, word_id:", bannedWordId); + + // 5. user_banned_words_kipris 테이블에 지재권 검색 결과 저장 + if (message.searchResults && Array.isArray(message.searchResults) && message.searchResults.length > 0) { + const kiprisPromises = message.searchResults.map(async (result, index) => { + // 상태 매핑 (파이썬 _map_status 로직 적용) + const rawStatus = result.registration_info?.status || result.detail?.registration_info?.lastDisposalCodeName || ""; + const mappedStatus = mapStatus(rawStatus); + + // 분류 코드 처리 (파이썬 로직 적용) + const rawClassificationCode = result.registration_info?.classificationCode || ""; + const { code: finalClassificationCode, description: finalCategoryDescription } = processClassificationCode(rawClassificationCode); + + // 권리정보 처리 (파이썬 category_description 로직) + let categoryDescription = finalCategoryDescription; + if (result.detail?.rights_info && Object.keys(result.detail.rights_info).length > 0) { + let processedRights = ""; + for (const [catCode, entries] of Object.entries(result.detail.rights_info)) { + processedRights += `카테고리 코드: ${catCode}\n`; + if (Array.isArray(entries)) { + for (const entry of entries) { + const name = (entry.asignProductName || "").trim(); + const similar = (entry.similarCodes || "").trim(); + processedRights += ` 지정상품군: ${name}, 유사군코드: ${similar}\n`; + } + } + processedRights += "\n"; + } + categoryDescription = processedRights; + } + + const kiprisData = { + user_id: user_id, + banned_word_id: bannedWordId, + application_status: mappedStatus, + registration_date: result.registration_info?.applicationDate || "", + applicant_name: result.detail?.applicant_info?.mapping?.applicantName || "", + classification_code: finalClassificationCode, + category_description: categoryDescription, + drawing: result.registration_info?.drawing || result.registration_info?.trademarkImage || "", + bigDrawing: null, + tradeMark_name: result.registration_info?.trademarkName || "", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + console.log(`[금지어 추가] user_banned_words_kipris에 삽입할 데이터 ${index + 1}:`, kiprisData); + + const kiprisUrl = `${SUPABASE_URL}/rest/v1/user_banned_words_kipris`; + const kiprisRes = await fetch(kiprisUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Prefer': 'return=minimal' + }, + body: JSON.stringify(kiprisData) + }); + + if (!kiprisRes.ok) { + const errorText = await kiprisRes.text(); + console.error(`[금지어 추가] user_banned_words_kipris 삽입 실패 ${index + 1}:`, kiprisRes.status, errorText); + throw new Error(`지재권 결과 저장 실패 ${index + 1}: ${errorText}`); + } + + console.log(`[금지어 추가] user_banned_words_kipris 삽입 성공 ${index + 1}`); + return true; + }); + + try { + await Promise.all(kiprisPromises); + console.log("[금지어 추가] 모든 지재권 결과 저장 완료"); + } catch (kiprisError) { + console.error("[금지어 추가] 지재권 결과 저장 중 오류:", kiprisError); + sendResponse({ + success: true, + warning: `금지어는 추가되었지만 일부 검색 결과 저장에 실패했습니다: ${kiprisError.message}` + }); + return; + } + } + + // 6. 성공 응답 + sendResponse({ + success: true, + message: `"${message.keyword}"이(가) 금지어 목록에 추가되었습니다. (등급: ${message.grade || '비허용'})` + }); + + } catch (error) { + console.error('[금지어 추가] 전체 오류:', error); + sendResponse({ + success: false, + error: `금지어 추가 중 오류가 발생했습니다: ${error.message}` + }); + } +} + +// 저장된 토큰 가져오기 +async function getStoredToken() { + try { + const result = await chrome.storage.local.get("access_token"); + return result.access_token || null; + } catch (error) { + console.error('[background.js] 토큰 가져오기 실패:', error); + return null; + } +} + +// 사용자 정보 가져오기 +async function fetchUserInfo(token) { + try { + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig(); + + // 사용자 기본 정보 가져오기 (토큰 검증) + const authUrl = `${SUPABASE_URL}/auth/v1/user`; + const authRes = await fetch(authUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!authRes.ok) { + console.error("[background.js] 토큰 검증 실패:", authRes.status); + return null; + } + + const authUser = await authRes.json(); + + // 사용자 상세 정보 및 회원등급 확인 + const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + const detailsRes = await fetch(detailsUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!detailsRes.ok) { + console.error("[background.js] 사용자 정보 조회 실패:", detailsRes.status); + return null; + } + + const detailsData = await detailsRes.json(); + return detailsData[0] || null; + + } catch (error) { + console.error('[background.js] 사용자 정보 가져오기 실패:', error); + return null; + } +} + +// 키워드 검색 URL (기본 코드 그대로) +function buildMarkInfoUrl(keyword) { + const encoded = encodeURIComponent(keyword); + return `https://markinfo.kr/search?page=1&size=20&sort=_score,desc&sort=applicationDate,desc&searchType=ST01&searchKeyword=${encoded}&statuses=APPLICATION&statuses=PUBLICATION&statuses=REGISTRATION`; +} + +// 상태 매핑 함수 (파이썬 _map_status와 동일) +function mapStatus(statusVal) { + if (typeof statusVal === 'string') { + const upperStatus = statusVal.toUpperCase(); + if (upperStatus === "REGISTRATION") { + return "등록"; + } else if (upperStatus === "APPLICATION") { + return "출원"; + } else if (upperStatus === "PUBLICATION") { + return "공고"; + } else { + return statusVal; + } + } else if (typeof statusVal === 'number') { + if (statusVal === 223) { + return "출원"; + } else if (statusVal === 192) { + return "등록"; + } else { + return "공고"; + } + } + return statusVal; +} + +// 분류 코드 처리 함수 +// 카테고리 설명 데이터 (kiprisCategories.json 내용) +const KIPRIS_CATEGORIES = { + "01": "공업/과학 및 사진용 및 농업/원예 및 임업용 화학제; 미가공 인조수지, 미가공 플라스틱; 소화 및 화재예방용 조성물; 조질제 및 땜납용 조제; 수피용 무두질제; 공업용 접착제; 퍼티 및 기타 페이스트 충전제; 퇴비, 거름, 비료; 산업용 및 과학용 생물학적 제제", + "02": "페인트, 니스, 래커; 방청제 및 목재 보존제; 착색제, 염료; 인쇄, 표시 및 판화용 잉크; 미가공 천연수지; 도장용/장식용/인쇄용/미술용 금속박(箔) 및 금속분(紛)", + "03": "비의료용 화장품 및 세면용품; 비의료용 치약; 향료, 에센셜 오일; 표백제 및 기타 세탁용 제제; 세정/광택 및 연마재", + "04": "공업용 오일 및 그리스, 왁스; 윤활제; 먼지흡수제, 먼지습윤제 및 먼지흡착제; 연료 및 발광체; 조명용 양초 및 심지", + "05": "약제, 의료용 및 수의과용 제제; 의료용 위생제; 의료용 또는 수의과용 식이요법 식품 및 제제, 유아용 식품; 인체용 및 동물용 식이보충제; 플라스터, 외상치료용 재료; 치과용 충전재료, 치과용 왁스; 소독제; 해충구제제; 살균제, 제초제", + "06": "일반금속 및 그 합금, 광석; 금속제 건축 및 구축용 재료; 금속제 이동식 건축물; 비전기용 일반금속제 케이블 및 와이어; 소형금속제품; 저장 또는 운반용 금속제 용기; 금고", + "07": "기계, 공작기계, 전동공구; 모터 및 엔진(육상차량용은 제외); 기계 커플링 및 전동장치 부품(육상차량용은 제외); 농기구(수동식 수공구는 제외); 부란기(孵卵器); 자동판매기", + "08": "수동식 수공구 및 수동기구; 커틀러리; 휴대 무기(화기는 제외); 면도기", + "09": "과학, 연구, 항법, 측량, 사진, 영화, 시청각, 광학, 계량, 측정, 신호, 탐지, 시험, 검사, 구명 및 교육용 기기; 전기 분배 또는 전기 사용의 전도, 전환, 변형, 축적, 조절 또는 통제를 위한 기기; 음향/영상 또는 데이터의 기록/전송/재생 또는 처리용 장치 및 기구; 기록 및 내려받기 가능한 미디어, 컴퓨터 소프트웨어, 빈 디지털 또는 아날로그 기록 및 저장매체; 동전작동식 기계장치; 금전등록기, 계산기; 컴퓨터 및 컴퓨터주변기기; 잠수복, 잠수마스크, 잠수용 귀마개, 다이버 및 수영용 노즈클립, 잠수용 장갑, 잠수용 호흡장치; 소화기기", + "10": "외과용, 내과용, 치과용 및 수의과용 기계기구; 의지(義肢), 의안(義眼) 및 의치(義齒); 정형외과용품; 봉합용 재료; 장애인용 치료 및 재활보조장치; 안마기; 유아수유용 기기 및 용품; 성활동용 기기 및 용품", + "11": "조명용, 가열용, 냉각용, 증기발생용, 조리용, 건조용, 환기용, 급수용, 위생용 장치 및 설비", + "12": "수송기계기구; 육상, 항공 또는 해상을 통해 이동하는 수송수단", + "13": "화기(火器); 탄약 및 발사체; 폭약; 폭죽", + "14": "귀금속 및 그 합금; 보석, 귀석 및 반귀석; 시계용구", + "15": "악기; 악보대 및 악기용 받침대; 지휘봉", + "16": "종이 및 판지; 인쇄물; 제본재료; 사진; 문방구 및 사무용품(가구는 제외); 문방구용 또는 가정용 접착제; 제도용구 및 미술용 재료; 회화용 솔; 교재; 포장용 플라스틱제 시트, 필름 및 가방; 인쇄활자, 프린팅블록", + "17": "미가공 및 반가공 고무, 구타페르카, 고무액(gum), 석면, 운모(雲母) 및 이들의 제품; 제조용 압출성형형태의 플라스틱 및 수지; 충전용, 마개용 및 절연용 재료; 비금속제 신축관, 튜브 및 호스", + "18": "가죽 및 모조가죽; 수피; 수하물가방 및 운반용 가방; 우산 및 파라솔; 걷기용 지팡이; 채찍 및 마구(馬具); 동물용 목걸이, 가죽끈 및 의류", + "19": "건축용 및 구축용 비금속제 건축재료; 건축용 비금속제 경질관(硬質管); 아스팔트, 피치, 타르 및 역청; 비금속제 이동식 건축물; 비금속제 기념물", + "20": "가구, 거울, 액자; 보관 또는 운송용 비금속제 컨테이너; 미가공 또는 반가공 뼈, 뿔, 고래수염 또는 나전(螺鈿); 패각; 해포석(海泡石); 호박(琥珀)(원석)", + "21": "가정용 또는 주방용 기구 및 용기; 조리기구 및 식기(포크, 나이프 및 스푼은 제외); 빗 및 스펀지; 솔(페인트 솔은 제외); 솔 제조용 재료; 청소용구; 비건축용 미가공 또는 반가공 유리; 유리제품, 도자기제품 및 토기제품", + "22": "로프 및 노끈; 망(網); 텐트 및 타폴린; 직물제 또는 합성재료제 차양; 돛; 하역물운반용 및 보관용 포대; 충전재료(고무/플라스틱/종이 및 판지제는 제외); 직물용 미가공 섬유 및 그 대용품", + "23": "직물용 실(絲)", + "24": "직물 및 직물대용품; 가정용 린넨; 직물 또는 플라스틱제 커튼", + "25": "의류, 신발, 모자", + "26": "레이스, 장식용 끈 및 자수포, 의류장식용 리본 및 나비매듭리본; 단추, 훅 및 아이(hooks and eyes), 핀 및 바늘; 조화(造花); 머리장식품; 가발", + "27": "카펫, 융단, 매트, 리놀륨 및 기타 바닥깔개용 재료; 비직물제 벽걸이", + "28": "오락용구, 장난감; 비디오게임장치; 체조 및 스포츠용품; 크리스마스트리용 장식품", + "29": "식육, 생선, 가금 및 엽조수; 고기진액; 보존처리/냉동/건조 및 조리된 과일 및 채소; 젤리, 잼, 콤폿; 달걀; 우유, 치즈, 버터, 요구르트 및 기타 유제품; 식용 유지(油脂)", + "30": "커피, 차(茶), 코코아 및 그 대용물; 쌀, 파스타 및 국수; 타피오카 및 사고(sago); 곡분 및 곡물 조제품; 빵, 페이스트리 및 과자; 초콜릿; 아이스크림, 셔벗 및 기타 식용 얼음; 설탕, 꿀, 당밀(糖蜜); 식품용 이스트, 베이킹 파우더; 소금, 조미료, 향신료, 보존처리된 허브; 식초, 소스 및 기타 조미료; 얼음", + "31": "미가공 농업, 수산양식, 원예 및 임업 생산물; 미가공 곡물 및 종자; 신선한 과실 및 채소, 신선한 허브; 살아 있는 식물 및 꽃; 구근(球根), 모종 및 재배용 곡물종자; 살아있는 동물; 동물용 사료 및 음료; 맥아", + "32": "맥주; 비알코올성 음료; 광천수 및 탄산수; 과실음료 및 과실주스; 시럽 및 비알코올성 음료용 제제", + "33": "알코올성 음료(맥주는 제외); 음료제조용 알코올성 제제", + "34": "담배 및 대용담배; 권연 및 여송연; 흡연자용 전자담배 및 기화기; 흡연용구; 성냥", + "35": "광고업; 사업관리/조직 및 경영업; 사무처리업", + "36": "금융, 통화 및 은행업; 보험서비스업; 부동산업", + "37": "건축서비스업; 설치 및 수리서비스업; 채광업/석유 및 가스 시추업", + "38": "통신서비스업", + "39": "운송업; 상품의 포장 및 보관업; 여행알선업", + "40": "재료처리업; 폐기물 재생업; 공기 정화 및 물 처리업; 인쇄 서비스업; 음식 및 음료수 보존업", + "41": "교육업; 훈련제공업; 연예오락업; 스포츠 및 문화활동업", + "42": "과학적, 기술적 서비스업 및 관련 연구, 디자인업; 산업분석, 산업연구 및 산업디자인 서비스업; 품질 관리 및 인증 서비스업; 컴퓨터 하드웨어 및 소프트웨어의 디자인 및 개발업", + "43": "식음료제공서비스업; 임시숙박시설업", + "44": "의료업; 수의업; 인간 또는 동물을 위한 위생 및 미용업; 농업, 수산양식, 원예 및 임업 서비스업", + "45": "법무서비스업; 유형의 재산 및 개인을 물리적으로 보호하기 위한 보안서비스업; 이성(異性) 소개업, 온라인 소셜 네트워킹 서비스업; 장례업; 베이비시팅업" +}; + +// 분류 코드 처리 함수 +function processClassificationCode(classificationCode) { + if (!classificationCode) return { code: "", description: "" }; + + let codes = []; + if (typeof classificationCode === 'string') { + codes = classificationCode.split(',').map(code => code.trim()); + } else if (Array.isArray(classificationCode)) { + codes = classificationCode; + } else { + codes = [String(classificationCode)]; + } + + const finalCode = codes.join('|'); + const mappedList = codes.map(code => { + const description = KIPRIS_CATEGORIES[code] || code; + return `[${code}] ${description}`; + }); + const finalDescription = mappedList.join('|') + '\n'; + + return { code: finalCode, description: finalDescription }; +} + +// 한국어를 중국어로 번역하여 텍스트 대체 +async function handleKoreanToChinese(selectedText, tab) { + if (!selectedText) { + chrome.notifications.create({ + type: 'basic', + title: '텍스트 선택 필요', + message: '번역할 텍스트를 먼저 선택해주세요.' + }); + return; + } + + console.log('[background.js] 한국어↔중국어 번역 요청:', selectedText); + + try { + // 언어 감지 + const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(selectedText); + const isChinese = /[\u4e00-\u9fff]/.test(selectedText); + + let translatedText; + let direction; + + if (isKorean && !isChinese) { + // 한국어 → 중국어 + translatedText = await translateText(selectedText, 'ko', 'zh'); + direction = '한국어 → 중국어'; + } else if (isChinese && !isKorean) { + // 중국어 → 한국어 + translatedText = await translateText(selectedText, 'zh', 'ko'); + direction = '중국어 → 한국어'; + } else if (isKorean && isChinese) { + // 한국어와 중국어가 섞여있는 경우 - 한국어 비율로 판단 + const koreanChars = (selectedText.match(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g) || []).length; + const chineseChars = (selectedText.match(/[\u4e00-\u9fff]/g) || []).length; + + if (koreanChars >= chineseChars) { + // 한국어가 더 많으면 중국어로 번역 + translatedText = await translateText(selectedText, 'ko', 'zh'); + direction = '한국어 → 중국어'; + } else { + // 중국어가 더 많으면 한국어로 번역 + translatedText = await translateText(selectedText, 'zh', 'ko'); + direction = '중국어 → 한국어'; + } + } else { + // 한국어도 중국어도 아닌 경우 + chrome.notifications.create({ + type: 'basic', + title: '지원하지 않는 언어', + message: '한국어 또는 중국어 텍스트를 선택해주세요.' + }); + return; + } + + // 선택된 텍스트를 번역된 텍스트로 대체 + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + function: replaceSelectedText, + args: [translatedText] + }); + + chrome.notifications.create({ + type: 'basic', + title: '번역 완료', + message: `${direction}로 변환되었습니다.` + }); + + } catch (error) { + console.error('[background.js] 한국어↔중국어 번역 중 오류:', error); + chrome.notifications.create({ + type: 'basic', + title: '번역 오류', + message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' + }); + } +} + +// 범용 번역 함수 +async function translateText(text, sourceLang, targetLang) { + const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`); + + if (!response.ok) { + throw new Error('Google 번역 요청 실패'); + } + + const data = await response.json(); + + if (!data || !data[0] || !data[0][0] || !data[0][0][0]) { + throw new Error('Google 번역 응답 형식 오류'); + } + + return data[0][0][0]; +} + +// 선택된 텍스트를 새로운 텍스트로 대체하는 함수 (content script에서 실행) +function replaceSelectedText(newText) { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + + // 선택된 텍스트가 input이나 textarea인지 확인 + const activeElement = document.activeElement; + if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { + // input/textarea의 경우 + const start = activeElement.selectionStart; + const end = activeElement.selectionEnd; + const value = activeElement.value; + + activeElement.value = value.substring(0, start) + newText + value.substring(end); + activeElement.selectionStart = start; + activeElement.selectionEnd = start + newText.length; + + // 변경 이벤트 트리거 + activeElement.dispatchEvent(new Event('input', { bubbles: true })); + activeElement.dispatchEvent(new Event('change', { bubbles: true })); + } else { + // 일반 텍스트 노드의 경우 + range.deleteContents(); + range.insertNode(document.createTextNode(newText)); + + // 새로운 텍스트 선택 + range.setStart(range.startContainer, range.startOffset - newText.length); + range.setEnd(range.startContainer, range.startOffset); + selection.removeAllRanges(); + selection.addRange(range); + } + } +} + +// ==================== 시간 알람 기능 ==================== + +class TimeAlarmManager { + constructor() { + this.workTimer = null; + this.breakTimer = null; + this.isBreakTime = false; + this.settings = { + enabled: true, + workTime: 60, // 분 + restTime: 5, // 분 + autoZzim: false + }; + this.startTime = null; + } + + async init() { + console.log('[TimeAlarm] 타이머 초기화 시작'); + + // 로그인 상태 확인 + try { + const { access_token } = await chrome.storage.local.get('access_token'); + if (!access_token) { + console.log('[TimeAlarm] 로그인되지 않음 - 타이머 시작하지 않음'); + return; + } + console.log('[TimeAlarm] 로그인 상태 확인됨'); + } catch (error) { + console.error('[TimeAlarm] 로그인 상태 확인 실패:', error); + return; + } + + await this.loadSettings(); + if (this.settings.enabled) { + this.startWorkTimer(); + console.log('[TimeAlarm] 시간 알람 시작:', this.settings); + } else { + console.log('[TimeAlarm] 시간 알람이 비활성화되어 있음'); + } + } + + async loadSettings() { + try { + const result = await chrome.storage.local.get('time_alarm_settings'); + this.settings = { ...this.settings, ...result.time_alarm_settings }; + console.log('[TimeAlarm] 설정 로드:', this.settings); + + // 설정 로드 후 타이머 재시작 + if (this.settings.enabled) { + console.log('[TimeAlarm] 설정 로드 후 타이머 재시작'); + this.startWorkTimer(); + } else { + console.log('[TimeAlarm] 시간 알람 비활성화 - 타이머 중지'); + this.stopAllTimers(); + } + } catch (error) { + console.error('[TimeAlarm] 설정 로드 실패:', error); + } + } + + startWorkTimer() { + if (this.workTimer) { + clearTimeout(this.workTimer); + } + + this.startTime = Date.now(); + const workTimeMs = this.settings.workTime * 60 * 1000; + + console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}분`); + + this.workTimer = setTimeout(() => { + this.showRestModal(); + }, workTimeMs); + } + + async showRestModal() { + try { + console.log('[TimeAlarm] 휴식 모달 표시'); + + // 휴식 모달 창 열기 + const popup = await chrome.windows.create({ + url: chrome.runtime.getURL('rest-modal.html'), + type: 'popup', + width: 500, + height: 910, + focused: true + }); + + // 휴식 시간 타이머 시작 + this.startBreakTimer(popup.id); + + } catch (error) { + console.error('[TimeAlarm] 휴식 모달 표시 실패:', error); + // 폴백: 알림으로 표시 + this.showRestNotification(); + } + } + + startBreakTimer(popupId) { + const breakTimeMs = this.settings.restTime * 60 * 1000; + + console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}분`); + + this.breakTimer = setTimeout(async () => { + try { + // 팝업 창 닫기 + await chrome.windows.remove(popupId); + } catch (error) { + console.log('[TimeAlarm] 팝업 창이 이미 닫혔습니다:', error); + } + + // 작업 완료 알림 + this.showWorkCompleteNotification(); + + // 다음 작업 타이머 시작 + this.startWorkTimer(); + + }, breakTimeMs); + } + + showRestNotification() { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '휴식 시간입니다! 🧘‍♀️', + message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.` + }); + } + + showWorkCompleteNotification() { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '휴식 완료! 🚀', + message: '이제 다시 열심히 월매출 1억을 향해 달려가세요! 💪' + }); + } + + async updateSettings(newSettings) { + this.settings = { ...this.settings, ...newSettings }; + + try { + await chrome.storage.local.set({ time_alarm_settings: this.settings }); + console.log('[TimeAlarm] 설정 업데이트:', this.settings); + + // 타이머 재시작 + if (this.settings.enabled) { + this.startWorkTimer(); + } else { + this.stopAllTimers(); + } + + } catch (error) { + console.error('[TimeAlarm] 설정 저장 실패:', error); + } + } + + stopAllTimers() { + if (this.workTimer) { + clearTimeout(this.workTimer); + this.workTimer = null; + } + if (this.breakTimer) { + clearTimeout(this.breakTimer); + this.breakTimer = null; + } + console.log('[TimeAlarm] 모든 타이머 중지'); + } + + // 현재 타이머 상태 반환 + getTimerStatus() { + if (!this.settings.enabled) { + return { + isRunning: false, + reason: '시간 알림이 비활성화됨' + }; + } + + if (!this.workTimer || !this.startTime) { + return { + isRunning: false, + reason: '작업 타이머가 실행 중이 아님' + }; + } + + // 남은 시간 계산 + const now = Date.now(); + const elapsed = now - this.startTime; + const totalWorkTime = this.settings.workTime * 60 * 1000; + const remainingTime = totalWorkTime - elapsed; + + if (remainingTime <= 0) { + return { + isRunning: false, + reason: '작업 시간이 이미 완료됨' + }; + } + + return { + isRunning: true, + remainingTime: remainingTime, + workTime: this.settings.workTime, + restTime: this.settings.restTime, + startTime: this.startTime, + elapsed: elapsed + }; + } +} + +// 시간 알람 매니저 인스턴스 +const timeAlarmManager = new TimeAlarmManager(); + +// 찜하기 기능 처리 +async function handleAddToZzim(message, sendResponse) { + try { + const config = await getStoredConfig(); + if (!config.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + const SUPABASE_URL = config.SUPABASE_URL || "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + // 사용자 정보 가져오기 + const userInfo = await fetchUserInfo(config.ACCESS_TOKEN); + if (!userInfo || !userInfo.user_id) { + throw new Error('사용자 정보를 가져올 수 없습니다'); + } + + // 찜하기 데이터 준비 + const zzimData = { + user_id: userInfo.user_id, + saying_id: message.sayingId, + saying_text: message.saying, + category: message.category, + target: message.target, + created_at: new Date().toISOString() + }; + + console.log('[AddToZzim] 찜하기 데이터:', zzimData); + + // API 호출 + const response = await fetch(`${SUPABASE_URL}/rest/v1/user_zzim_sayings`, { + method: 'POST', + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${config.ACCESS_TOKEN}`, + 'Content-Type': 'application/json', + 'Prefer': 'return=minimal' + }, + body: JSON.stringify(zzimData) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`찜하기 실패: ${response.status} - ${errorText}`); + } + + console.log('[AddToZzim] 찜하기 성공'); + sendResponse({ success: true }); + + } catch (error) { + console.error('[AddToZzim] 찜하기 실패:', error); + sendResponse({ success: false, error: error.message }); + } +} + +// 설정 정보 가져오기 함수 +async function getStoredConfig() { + try { + const result = await chrome.storage.local.get('settings_config'); + return result.settings_config || {}; + } catch (error) { + console.error('[Config] 설정 정보 로드 실패:', error); + return {}; + } +} + +// 메시지 리스너에 새로운 액션 추가 +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log('[Background] 메시지 수신:', message); + + // ... existing message handlers ... + + if (message.action === "addBannedWord") { + handleAddBannedWord(message, sendResponse); + return true; // 비동기 응답을 위해 true 반환 + } + + if (message.action === 'updateTimeAlarmSettings') { + timeAlarmManager.updateSettings(message.settings); + sendResponse({ success: true }); + return true; + } + + if (message.action === 'getTimerStatus') { + const status = timeAlarmManager.getTimerStatus(); + console.log('[Background] 타이머 상태 요청:', status); + sendResponse({ success: true, ...status }); + return true; + } + + if (message.action === 'startTimerAfterLogin') { + console.log('[Background] 로그인 후 타이머 시작 요청 받음'); + timeAlarmManager.init().then(() => { + console.log('[Background] 로그인 후 타이머 시작 성공'); + sendResponse({ success: true, message: '타이머가 시작되었습니다' }); + }).catch((error) => { + console.error('[Background] 로그인 후 타이머 시작 실패:', error); + sendResponse({ success: false, error: error.message }); + }); + return true; + } + + if (message.action === 'getBackendConfig') { + console.log('[Background] 백엔드 설정 요청 받음'); + try { + const config = getBackendConfig(); + console.log('[Background] 백엔드 설정 응답:', config); + sendResponse({ success: true, config: config }); + } catch (error) { + console.error('[Background] 백엔드 설정 오류:', error); + sendResponse({ success: false, error: error.message }); + } + return true; + } + + if (message.action === 'addToZzim') { + handleAddToZzim(message, sendResponse); + return true; // 비동기 응답을 위해 true 반환 + } +}); + +// 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거) +chrome.runtime.onStartup.addListener(() => { + console.log('[Background] 확장 프로그램 시작됨'); +}); + +chrome.runtime.onInstalled.addListener(() => { + console.log('[Background] 확장 프로그램 설치/업데이트됨'); +}); + +// 스토리지 변경 감지하여 시간 알람 설정 업데이트 +chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'local' && changes.time_alarm_settings) { + console.log('[Background] 시간 알람 설정 변경 감지:', changes.time_alarm_settings); + timeAlarmManager.loadSettings(); + } +}); + +// API 호출량 증가 및 한도 확인 함수 +async function incrementApiCallsAndCheckLimit() { + try { + console.log('[background.js] API 호출량 증가 및 한도 확인 시작'); + + // Chrome Storage에서 사용자 정보 가져오기 (popup.js에서 저장한 정보) + const storageData = await chrome.storage.local.get([ + 'access_token', + 'user_id', + 'user_membership_level', + 'user_api_limit', + 'user_current_api_calls' + ]); + + const token = storageData.access_token; + const userId = storageData.user_id; + const membershipLevel = storageData.user_membership_level; + const apiLimit = storageData.user_api_limit; + const currentCalls = storageData.user_current_api_calls || 0; + + console.log('[background.js] Storage에서 가져온 사용자 정보:', { + userId: userId, + membershipLevel: membershipLevel, + currentCalls: currentCalls, + apiLimit: apiLimit + }); + + // 필수 정보 확인 + if (!token) { + console.error('[background.js] 토큰이 없습니다'); + return { success: false, error: '로그인이 필요합니다' }; + } + + if (!userId) { + console.error('[background.js] 사용자 ID가 없습니다'); + return { success: false, error: '사용자 정보가 없습니다. 다시 로그인해주세요.' }; + } + + if (apiLimit === null || apiLimit === undefined) { + console.error('[background.js] API 한도 정보가 없습니다'); + return { success: false, error: 'API 한도 정보를 확인할 수 없습니다' }; + } + + console.log('[background.js] API 호출 현황:', { + current: currentCalls, + limit: apiLimit, + remaining: apiLimit - currentCalls + }); + + // 한도 확인 + if (currentCalls >= apiLimit) { + console.warn('[background.js] API 호출 한도 초과:', { + current: currentCalls, + limit: apiLimit + }); + return { + success: false, + error: `일일 API 호출 한도(${apiLimit}회)를 초과했습니다. 현재 ${currentCalls}회 사용했습니다.`, + current: currentCalls, + limit: apiLimit + }; + } + + // API 호출량 증가 (데이터베이스 업데이트) + const incrementResult = await incrementUserApiCalls(userId, token); + if (!incrementResult.success) { + console.error('[background.js] API 호출량 증가 실패:', incrementResult.error); + return { success: false, error: 'API 호출량 업데이트에 실패했습니다' }; + } + + const newCallCount = incrementResult.newCount; + + // Chrome Storage의 현재 호출량도 업데이트 + await chrome.storage.local.set({ + user_current_api_calls: newCallCount + }); + + console.log('[background.js] API 호출량 증가 완료:', { + previous: currentCalls, + current: newCallCount, + limit: apiLimit, + remaining: apiLimit - newCallCount + }); + + return { + success: true, + current: newCallCount, + limit: apiLimit, + remaining: apiLimit - newCallCount + }; + + } catch (error) { + console.error('[background.js] API 호출량 확인 중 오류:', error); + return { success: false, error: '시스템 오류가 발생했습니다' }; + } +} + +// 멤버십 레벨별 API 한도 가져오기 - 제거됨 (Chrome Storage 사용) + +// 사용자 API 호출량 증가 +async function incrementUserApiCalls(userId, token) { + try { + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig(); + + console.log('[background.js] 사용자 API 호출량 증가:', userId); + + // 먼저 현재 사용자 정보를 조회하여 current_api_calls 값 확인 + const getUserUrl = `${SUPABASE_URL}/rest/v1/users?select=current_api_calls&id=eq.${userId}&limit=1`; + const getUserRes = await fetch(getUserUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!getUserRes.ok) { + const errorText = await getUserRes.text(); + console.error('[background.js] 현재 API 호출량 조회 실패:', { + status: getUserRes.status, + statusText: getUserRes.statusText, + error: errorText + }); + return { success: false, error: `현재 값 조회 실패: ${getUserRes.status}` }; + } + + const userData = await getUserRes.json(); + if (userData.length === 0) { + console.error('[background.js] 사용자를 찾을 수 없음:', userId); + return { success: false, error: '사용자를 찾을 수 없습니다' }; + } + + const currentCalls = userData[0].current_api_calls || 0; + const newCalls = currentCalls + 1; + + console.log('[background.js] API 호출량 업데이트:', { + userId: userId, + current: currentCalls, + new: newCalls + }); + + // 새로운 값으로 업데이트 + const updateUrl = `${SUPABASE_URL}/rest/v1/users?id=eq.${userId}`; + const updateRes = await fetch(updateUrl, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + }, + body: JSON.stringify({ + current_api_calls: newCalls + }) + }); + + if (!updateRes.ok) { + const errorText = await updateRes.text(); + console.error('[background.js] API 호출량 업데이트 실패:', { + status: updateRes.status, + statusText: updateRes.statusText, + error: errorText + }); + return { success: false, error: `업데이트 실패: ${updateRes.status}` }; + } + + const updatedData = await updateRes.json(); + console.log('[background.js] API 호출량 업데이트 성공:', updatedData); + + return { success: true, data: updatedData, newCount: newCalls }; + + } catch (error) { + console.error('[background.js] API 호출량 증가 중 오류:', error); + return { success: false, error: error.message }; + } +} + +// 백엔드 설정 중앙 관리 +const BACKEND_CONFIG = { + SUPABASE_URL: "http://146.56.101.199:8000", + SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" +}; + +// 백엔드 설정 가져오기 함수 +function getBackendConfig() { + return { ...BACKEND_CONFIG }; +} + +// 검색 결과 개선을 위한 키워드 확장 함수 diff --git a/wrmc_ext/bannedWords.html b/wrmc_ext/bannedWords.html new file mode 100644 index 0000000..2609bd9 --- /dev/null +++ b/wrmc_ext/bannedWords.html @@ -0,0 +1,244 @@ + + + + + 금지어 관리 + + + +

🚫 금지어 관리

+ + +
+ 초기화 중... +
+ +
+ + +
+ +
+ + +
+ + + + + + + + + + + + +
순번금지어등급작업
+
+ + + + + + + + + \ No newline at end of file diff --git a/wrmc_ext/bannedWords.js b/wrmc_ext/bannedWords.js new file mode 100644 index 0000000..929ba4a --- /dev/null +++ b/wrmc_ext/bannedWords.js @@ -0,0 +1,1652 @@ +// 금지어 관리 모듈 +class BannedWordsManager { + constructor() { + // 초기값 설정 (chrome.storage에서 로드될 때까지 임시) + this.SUPABASE_URL = null; + this.SUPABASE_ANON_KEY = null; + this.DEBUG_MODE = true; + this.ACCESS_TOKEN = null; + this.isConfigLoaded = false; + this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지 + + this.debugLog('BannedWordsManager 생성자 시작 - 설정 로드 대기 중'); + + // 키프리스 모달 닫기 버튼 이벤트 등록 + const closeKipris = document.getElementById("close-kipris"); + if (closeKipris) { + closeKipris.addEventListener("click", () => { + document.getElementById("kipris-modal").style.display = "none"; + }); + } + + // 키프리스 모달 외부 클릭 시 닫기 + window.addEventListener("click", (e) => { + const modal = document.getElementById("kipris-modal"); + if (e.target === modal) { + modal.style.display = "none"; + } + }); + + // ESC 키로 모달 닫기 이벤트 등록 + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + // 키프리스 모달 닫기 + const kiprisModal = document.getElementById("kipris-modal"); + if (kiprisModal && kiprisModal.style.display === "block") { + kiprisModal.style.display = "none"; + e.preventDefault(); + return; + } + + // 현재 창이 별도 창인 경우 닫기 + if (window.location.href.includes('bannedWords.html') || window.location.href.includes('sayings.html')) { + window.close(); + e.preventDefault(); + } + } + }); + + this.debugLog('BannedWordsManager 생성자 완료'); + } + + // 설정 로드 함수 + async loadConfig() { + try { + this.debugLog('chrome.storage에서 설정 로드 시작'); + + // 1. bannedWords_config 우선 확인 + const configData = await chrome.storage.local.get('bannedWords_config'); + + if (configData.bannedWords_config) { + const config = configData.bannedWords_config; + this.debugLog('bannedWords_config에서 설정 로드', { + hasUrl: !!config.SUPABASE_URL, + hasKey: !!config.SUPABASE_ANON_KEY, + hasToken: !!config.ACCESS_TOKEN, + debugMode: config.DEBUG_MODE, + timestamp: config.timestamp, + age: Date.now() - config.timestamp + }); + + // 설정이 너무 오래된 경우 (5분 이상) 경고 + if (Date.now() - config.timestamp > 5 * 60 * 1000) { + this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp }); + } + + this.SUPABASE_URL = config.SUPABASE_URL; + this.SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY; + this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true; + this.ACCESS_TOKEN = config.ACCESS_TOKEN; + + } else { + // 2. 개별 설정 확인 (fallback) + this.debugLog('bannedWords_config가 없음, 개별 설정 확인 중'); + + const storageData = await chrome.storage.local.get([ + 'access_token', + 'SUPABASE_URL', + 'SUPABASE_ANON_KEY' + ]); + + this.debugLog('개별 설정 조회 결과', { + hasToken: !!storageData.access_token, + hasUrl: !!storageData.SUPABASE_URL, + hasKey: !!storageData.SUPABASE_ANON_KEY + }); + + // 기본값 설정 + this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000'; + this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'; + this.ACCESS_TOKEN = storageData.access_token; + } + + this.isConfigLoaded = true; + + this.debugLog('설정 로드 완료', { + SUPABASE_URL: this.SUPABASE_URL, + hasToken: !!this.ACCESS_TOKEN, + tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, + DEBUG_MODE: this.DEBUG_MODE, + isConfigLoaded: this.isConfigLoaded + }); + + // 설정 검증 + if (!this.SUPABASE_URL) { + throw new Error('SUPABASE_URL이 설정되지 않았습니다'); + } + if (!this.SUPABASE_ANON_KEY) { + throw new Error('SUPABASE_ANON_KEY가 설정되지 않았습니다'); + } + if (!this.ACCESS_TOKEN) { + throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다'); + } + + return true; + + } catch (error) { + this.debugLog('설정 로드 실패', { error: error.message }); + throw error; + } + } + + // 디버그 로깅 함수 + debugLog(message, data = null) { + if (this.DEBUG_MODE) { + console.log(`[BannedWords] ${message}`, data || ''); + this.updateDebugUI(`[${new Date().toLocaleTimeString()}] ${message}`); + } + } + + // 디버그 UI 업데이트 + updateDebugUI(message) { + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + if (this.DEBUG_MODE) { + // 디버그 모드일 때만 표시 + // 기존 버튼 유지하면서 로그 메시지만 업데이트 + const existingButton = debugElement.querySelector('button'); + const buttonHtml = existingButton ? existingButton.outerHTML : ''; + + debugElement.innerHTML = `${message}
${buttonHtml}`; + debugElement.style.display = "block"; + + // 버튼 이벤트 다시 등록 + if (existingButton) { + const newButton = debugElement.querySelector('button'); + if (newButton && !newButton.onclick) { + newButton.addEventListener('click', this.showDebugLogs.bind(this)); + } + } + } else { + // 디버그 모드가 아닐 때는 숨김 + debugElement.style.display = "none"; + debugElement.innerHTML = ""; + } + } + } + + // 초기화 및 데이터 로드 + async initialize() { + try { + this.debugLog('BannedWordsManager 초기화 시작'); + + // 1) 설정 로드 (chrome.storage에서) + this.debugLog('설정 로드 중...'); + await this.loadConfig(); + + // 2) 토큰 유효성 검증 + this.debugLog('토큰 유효성 검증 중...'); + await this.validateToken(); + + // 3) 버튼 이벤트 리스너 등록 (한 번만) + this.debugLog('버튼 이벤트 리스너 등록 중...'); + this.attachButtonEventListeners(); + + // 4) 금지어 목록 로드 + this.debugLog('금지어 목록 로드 시작'); + await this.loadBannedWords(); + + this.debugLog('BannedWordsManager 초기화 완료'); + + } catch (error) { + this.debugLog('초기화 실패', { error: error.message }); + this.renderError(error.message); + } + } + + // 토큰 유효성 검증 + async validateToken() { + try { + this.debugLog('토큰 검증 API 호출 준비', { + url: `${this.SUPABASE_URL}/auth/v1/user`, + hasToken: !!this.ACCESS_TOKEN, + tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, + tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none' + }); + + const headers = { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + this.debugLog('요청 헤더 정보', { + hasAuthorization: !!headers.Authorization, + hasApikey: !!headers.apikey, + authPreview: headers.Authorization ? headers.Authorization.substring(0, 20) + '...' : 'none' + }); + + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { + method: 'GET', + headers: headers, + mode: 'cors', // CORS 모드 명시적 설정 + credentials: 'omit' // 크로스 오리진 요청에서 자격 증명 제외 + }); + + this.debugLog('토큰 검증 API 응답', { + status: authRes.status, + statusText: authRes.statusText, + ok: authRes.ok, + type: authRes.type, + url: authRes.url, + headers: Object.fromEntries(authRes.headers.entries()) + }); + + if (!authRes.ok) { + let errorText = ''; + let errorJson = null; + + try { + const responseText = await authRes.text(); + errorText = responseText; + + // JSON 파싱 시도 + if (responseText.trim().startsWith('{')) { + errorJson = JSON.parse(responseText); + } + } catch (parseError) { + this.debugLog('응답 파싱 실패', { parseError: parseError.message }); + } + + this.debugLog('토큰 검증 실패 - 상세 정보', { + status: authRes.status, + statusText: authRes.statusText, + errorText: errorText, + errorJson: errorJson + }); + + throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorText}`); + } + + const userData = await authRes.json(); + this.debugLog('토큰 검증 성공', { + email: userData.email, + id: userData.id, + userData: userData + }); + + return userData; + + } catch (error) { + this.debugLog('토큰 검증 중 오류 - 상세 분석', { + errorName: error.name, + errorMessage: error.message, + errorStack: error.stack, + isNetworkError: error.name === 'TypeError' && error.message.includes('fetch'), + isCorsError: error.message.includes('CORS') || error.message.includes('cors'), + isTimeoutError: error.message.includes('timeout') || error.message.includes('Timeout') + }); + + // 에러 타입별 상세 메시지 + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.\n- URL: ${this.SUPABASE_URL}/auth/v1/user\n- 원본 에러: ${error.message}`); + } else if (error.message.includes('CORS')) { + throw new Error(`CORS 오류: 크로스 오리진 요청이 차단되었습니다.\n- 서버 CORS 설정을 확인해주세요\n- 원본 에러: ${error.message}`); + } else if (error.message.includes('timeout')) { + throw new Error(`요청 시간 초과: 서버 응답이 지연되고 있습니다.\n- 원본 에러: ${error.message}`); + } + + throw error; + } + } + + // 에러 렌더링 헬퍼 함수 + renderError(message) { + const tbody = document.getElementById("banned-words-tbody"); + const statsDiv = document.getElementById("banned-words-stats"); + + if (tbody) { + tbody.innerHTML = `오류: ${message}`; + } + + if (statsDiv) { + statsDiv.innerHTML = `
오류: ${message}
`; + } + + this.debugLog('에러 렌더링 완료', { message }); + } + + // 금지어 목록 로드 + async loadBannedWords() { + this.debugLog('금지어 목록 로드 시작'); + + const loading = document.getElementById("banned-words-loading"); + const tbody = document.getElementById("banned-words-tbody"); + + loading.style.display = "block"; + tbody.innerHTML = ""; + + try { + // 공통 헤더 설정 + const headers = { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + const fetchOptions = { + method: 'GET', + headers: headers, + mode: 'cors', + credentials: 'omit' + }; + + // 현재 사용자의 ID 가져오기 + this.debugLog('Auth API 호출 중...'); + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, fetchOptions); + + this.debugLog('Auth API 응답', { + status: authRes.status, + statusText: authRes.statusText, + ok: authRes.ok, + url: authRes.url + }); + + if (!authRes.ok) { + const errorDetail = await authRes.text(); + this.debugLog('Auth API 에러 상세', { + status: authRes.status, + error: errorDetail + }); + throw new Error(`사용자 정보를 가져올 수 없습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorDetail}`); + } + + const authUser = await authRes.json(); + this.debugLog('Auth 사용자 정보 수신', { + email: authUser.email, + id: authUser.id + }); + + // 사용자의 user_id 가져오기 + this.debugLog('Users API 호출 중...'); + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, fetchOptions); + + this.debugLog('Users API 응답', { + status: userRes.status, + statusText: userRes.statusText, + ok: userRes.ok, + url: userRes.url + }); + + if (!userRes.ok) { + const errorDetail = await userRes.text(); + this.debugLog('Users API 에러 상세', { + status: userRes.status, + error: errorDetail + }); + throw new Error(`사용자 정보를 찾을 수 없습니다 (${userRes.status}: ${userRes.statusText})\n응답: ${errorDetail}`); + } + + const userData = await userRes.json(); + this.debugLog('Users 데이터 수신', { + count: userData.length, + data: userData + }); + + if (!userData || userData.length === 0) { + throw new Error('사용자 데이터를 찾을 수 없습니다'); + } + + const userId = userData[0].id; + this.debugLog('사용자 ID 확인', { userId }); + + // 금지어 목록 가져오기 + this.debugLog('BannedWords API 호출 중...'); + const bannedWordsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, fetchOptions); + + this.debugLog('BannedWords API 응답', { + status: bannedWordsRes.status, + statusText: bannedWordsRes.statusText, + ok: bannedWordsRes.ok, + url: bannedWordsRes.url + }); + + if (!bannedWordsRes.ok) { + const errorDetail = await bannedWordsRes.text(); + this.debugLog('BannedWords API 에러 상세', { + status: bannedWordsRes.status, + error: errorDetail + }); + throw new Error(`금지어 목록을 가져올 수 없습니다 (${bannedWordsRes.status}: ${bannedWordsRes.statusText})\n응답: ${errorDetail}`); + } + + const bannedWords = await bannedWordsRes.json(); + this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length }); + + this.displayBannedWords(bannedWords); + } catch (error) { + this.debugLog('금지어 목록 로드 실패', { + errorName: error.name, + errorMessage: error.message, + isNetworkError: error.name === 'TypeError' && error.message.includes('fetch') + }); + + let errorMessage = error.message; + if (error.name === 'TypeError' && error.message.includes('fetch')) { + errorMessage = `네트워크 연결 오류: ${error.message}`; + } + + tbody.innerHTML = `오류: ${errorMessage}`; + } finally { + loading.style.display = "none"; + } + } + + // 금지어 목록 표시 + displayBannedWords(bannedWords) { + this.debugLog('금지어 목록 UI 업데이트 시작', { count: bannedWords.length }); + + const tbody = document.getElementById("banned-words-tbody"); + const statsDiv = document.getElementById("banned-words-stats"); + + if (bannedWords.length === 0) { + tbody.innerHTML = '등록된 금지어가 없습니다.'; + statsDiv.innerHTML = '
총 금지어 개수: 0개
'; + this.debugLog('금지어 없음 - 빈 상태 표시'); + return; + } + + // 통계 계산 + const totalCount = bannedWords.length; + const prohibitedCount = bannedWords.filter(word => word.grade === '금지').length; + const notAllowedCount = bannedWords.filter(word => word.grade === '비허용').length; + const otherCount = totalCount - prohibitedCount - notAllowedCount; + + this.debugLog('금지어 통계 계산', { + totalCount, + prohibitedCount, + notAllowedCount, + otherCount + }); + + // 통계 정보 표시 + let statsText = `총 금지어 개수: ${totalCount}개`; + if (prohibitedCount > 0 || notAllowedCount > 0) { + statsText += ` (금지: ${prohibitedCount}개, 비허용: ${notAllowedCount}개`; + if (otherCount > 0) { + statsText += `, 기타: ${otherCount}개`; + } + statsText += ')'; + } + + // 통계 정보와 추가 버튼을 함께 표시 + statsDiv.innerHTML = ` +
+
${statsText}
+ +
+ `; + + // 금지어 추가 버튼 이벤트 등록 + const addBtn = document.getElementById('add-banned-word-btn'); + if (addBtn) { + addBtn.addEventListener('click', () => this.showAddBannedWordModal()); + } + + // 테이블 데이터 표시 (word_id를 기본키로 사용) + tbody.innerHTML = bannedWords.map((word, index) => { + // 등급에 따른 배경색 설정 + let gradeStyle = 'background-color: #f8f9fa; color: #495057;'; // 기본 스타일 + let gradeBgColor = '#f8f9fa'; // 기본 배경색 + + if (word.grade === '금지') { + gradeStyle = 'background-color: #ff8c00; color: white; font-weight: bold;'; // 주황색 + gradeBgColor = '#fff3e0'; // 연한 주황색 배경 + } else if (word.grade === '비허용') { + gradeStyle = 'background-color: #ffd700; color: #333; font-weight: bold;'; // 노란색 + gradeBgColor = '#fffbf0'; // 연한 노란색 배경 + } + + return ` + + ${index + 1} + ${word.banned_word} + + ${word.grade || '값 없음'} + + + + + + + + `; + }).join(''); + + // 버튼 이벤트 리스너는 한 번만 등록 (초기화에서 처리) + // this.attachButtonEventListeners(); // 이 줄 제거 + + this.debugLog('금지어 목록 UI 업데이트 완료'); + } + + // 버튼 이벤트 리스너 등록 (한 번만 실행) + attachButtonEventListeners() { + this.debugLog('버튼 이벤트 리스너 등록 시작'); + + const tbody = document.getElementById("banned-words-tbody"); + if (!tbody) { + this.debugLog('tbody 요소를 찾을 수 없음'); + return; + } + + // 기존 이벤트 리스너 제거 (중복 방지) + if (this.buttonEventListenerAttached) { + this.debugLog('이미 이벤트 리스너가 등록되어 있음'); + return; + } + + // 이벤트 위임을 사용하여 동적으로 생성된 버튼들에 이벤트 등록 + const clickHandler = async (e) => { + const button = e.target.closest('button[data-action]'); + if (!button) return; + + e.preventDefault(); + + const action = button.getAttribute('data-action'); + const wordId = button.getAttribute('data-word-id'); + const wordWordId = button.getAttribute('data-word-word-id'); + const wordText = button.getAttribute('data-word-text'); + const wordGrade = button.getAttribute('data-word-grade'); + + this.debugLog('버튼 클릭 감지', { action, wordId, wordWordId, wordText, wordGrade }); + + // 버튼 비활성화 (중복 클릭 방지) + const originalText = button.textContent; + button.disabled = true; + + try { + switch (action) { + case 'view': + button.textContent = '🔄 로딩...'; + await this.viewKiprisResults(wordWordId, wordText); + break; + + case 'edit': + button.textContent = '🔄 수정 중...'; + await this.editGrade(wordId, wordGrade); + break; + + case 'delete': + button.textContent = '🔄 삭제 중...'; + await this.deleteBannedWord(wordId, wordText); + break; + + default: + this.debugLog('알 수 없는 액션', { action }); + } + } catch (error) { + this.debugLog('버튼 액션 처리 중 오류', { action, error: error.message }); + alert(`❌ 작업 중 오류가 발생했습니다: ${error.message}`); + } finally { + // 버튼 상태 복원 + button.textContent = originalText; + button.disabled = false; + } + }; + + tbody.addEventListener('click', clickHandler); + this.buttonEventListenerAttached = true; + + this.debugLog('버튼 이벤트 리스너 등록 완료'); + } + + // 등급 수정 (드롭박스 개선 버전) + async editGrade(wordId, currentGrade) { + this.debugLog('등급 수정 시작', { wordId, currentGrade }); + + // 모달 HTML 생성 + const modalId = 'grade-edit-modal'; + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + const modalHtml = ` +
+
+

🔧 등급 수정

+

현재 등급: ${currentGrade || '값 없음'}

+ +
+ + +
+ +
+ + +
+
+
+ `; + + // 모달을 body에 추가 + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modal = document.getElementById(modalId); + const gradeSelect = document.getElementById('grade-select'); + const confirmBtn = document.getElementById('grade-confirm-btn'); + const cancelBtn = document.getElementById('grade-cancel-btn'); + + // 드롭박스 변경 감지 + let hasChanged = false; + const originalValue = currentGrade; + + gradeSelect.addEventListener('change', () => { + hasChanged = gradeSelect.value !== originalValue; + if (hasChanged) { + confirmBtn.textContent = '수정하시겠습니까? (Enter)'; + confirmBtn.style.background = '#28a745'; + confirmBtn.style.borderColor = '#28a745'; + } else { + confirmBtn.textContent = '확인 (Enter)'; + confirmBtn.style.background = '#007bff'; + confirmBtn.style.borderColor = '#007bff'; + } + }); + + // 확인/취소 처리 함수 + const handleConfirm = async () => { + const selectedGrade = gradeSelect.value; + + if (selectedGrade === originalValue) { + this.debugLog('등급 변경 없음', { selectedGrade, originalValue }); + modal.remove(); + return; + } + + try { + confirmBtn.textContent = '🔄 수정 중...'; + confirmBtn.disabled = true; + cancelBtn.disabled = true; + + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + this.debugLog('등급 업데이트 API 호출', { wordId, selectedGrade }); + + const updateRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + grade: selectedGrade, + updated_at: new Date().toISOString() + }) + }); + + this.debugLog('등급 업데이트 API 응답', { + status: updateRes.status, + statusText: updateRes.statusText, + ok: updateRes.ok + }); + + if (!updateRes.ok) { + const errorDetail = await updateRes.text(); + this.debugLog('등급 업데이트 실패', { + status: updateRes.status, + error: errorDetail + }); + throw new Error(`등급 업데이트 실패 (${updateRes.status}: ${updateRes.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('등급 수정 성공', { wordId, selectedGrade }); + alert(`✅ 등급이 "${selectedGrade}"로 변경되었습니다.`); + + // 모달 닫기 + modal.remove(); + + // 목록 새로고침 + await this.loadBannedWords(); + + } catch (error) { + this.debugLog('등급 수정 실패', { + error: error.message, + wordId, + selectedGrade: gradeSelect.value + }); + alert(`❌ 등급 수정 중 오류가 발생했습니다:\n${error.message}`); + + // 버튼 상태 복원 + confirmBtn.textContent = '확인 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + } + }; + + const handleCancel = () => { + this.debugLog('등급 수정 취소됨'); + modal.remove(); + }; + + // 이벤트 리스너 등록 + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + + // 키보드 이벤트 처리 + const handleKeydown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleConfirm(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancel(); + } + }; + + modal.addEventListener('keydown', handleKeydown); + + // 모달 외부 클릭 시 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + handleCancel(); + } + }); + + // 드롭박스에 포커스 + gradeSelect.focus(); + } + + // 금지어 삭제 (개선된 버전) + async deleteBannedWord(wordId, word) { + this.debugLog('금지어 삭제 요청', { wordId, word }); + + // 사용자 확인 + const confirmMessage = `⚠️ 금지어 삭제 확인\n\n단어: "${word}"\n\n정말로 삭제하시겠습니까?\n\n※ 삭제된 데이터는 복구할 수 없습니다.`; + + if (!confirm(confirmMessage)) { + this.debugLog('금지어 삭제 취소됨', { wordId, word }); + return; + } + + try { + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + this.debugLog('금지어 삭제 API 호출', { wordId, word }); + + const deleteRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + this.debugLog('금지어 삭제 API 응답', { + status: deleteRes.status, + statusText: deleteRes.statusText, + ok: deleteRes.ok + }); + + if (!deleteRes.ok) { + const errorDetail = await deleteRes.text(); + this.debugLog('금지어 삭제 실패', { + status: deleteRes.status, + error: errorDetail + }); + throw new Error(`금지어 삭제 실패 (${deleteRes.status}: ${deleteRes.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('금지어 삭제 성공', { wordId, word }); + alert(`✅ "${word}" 금지어가 삭제되었습니다.`); + + // 목록 새로고침 + await this.loadBannedWords(); + + } catch (error) { + this.debugLog('금지어 삭제 실패', { + error: error.message, + wordId, + word + }); + alert(`❌ 금지어 삭제 중 오류가 발생했습니다:\n${error.message}`); + } + } + + // 키프리스 결과 보기 (완전히 개선된 버전) + async viewKiprisResults(wordWordId, word) { + this.debugLog('키프리스 결과 보기 시작', { wordWordId, word }); + + const modal = document.getElementById("kipris-modal"); + const loading = document.getElementById("kipris-loading"); + const results = document.getElementById("kipris-results"); + const title = document.getElementById("kipris-word-title"); + + // 모달 표시 및 초기화 + modal.style.display = "block"; + loading.style.display = "block"; + results.innerHTML = ""; + title.innerHTML = `

🔍 "${word}" 키프리스 검색 결과

`; + + // 모달 스크롤 스타일 적용 + modal.style.overflow = "auto"; + modal.style.maxHeight = "100vh"; + + // 모달 내용 컨테이너에 스크롤 스타일 적용 + const modalContent = modal.querySelector('.modal-content') || modal.querySelector('#kipris-modal > div'); + if (modalContent) { + modalContent.style.maxHeight = "90vh"; + modalContent.style.overflowY = "auto"; + modalContent.style.padding = "20px"; + modalContent.style.margin = "5vh auto"; + } + + // 결과 컨테이너에도 스크롤 스타일 적용 + results.style.maxHeight = "70vh"; + results.style.overflowY = "auto"; + results.style.padding = "10px"; + + try { + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + // word_id가 없거나 undefined인 경우 처리 + if (!wordWordId || wordWordId === 'undefined' || wordWordId === '') { + throw new Error('word_id가 없습니다. 데이터베이스에서 word_id 값을 확인해주세요.'); + } + + this.debugLog('키프리스 데이터 조회 API 호출', { + wordWordId, + word, + queryUrl: `${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc` + }); + + // 올바른 테이블 이름과 필드명 사용: word_id → banned_word_id + const resultsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + this.debugLog('키프리스 데이터 API 응답', { + status: resultsRes.status, + statusText: resultsRes.statusText, + ok: resultsRes.ok, + url: resultsRes.url + }); + + if (!resultsRes.ok) { + const errorDetail = await resultsRes.text(); + this.debugLog('키프리스 데이터 조회 실패', { + status: resultsRes.status, + error: errorDetail, + wordWordId, + word + }); + throw new Error(`키프리스 결과를 가져올 수 없습니다 (${resultsRes.status}: ${resultsRes.statusText})\n응답: ${errorDetail}`); + } + + const kiprisData = await resultsRes.json(); + this.debugLog('키프리스 결과 로드 성공', { + count: kiprisData.length, + wordWordId, + word, + sampleData: kiprisData.length > 0 ? { + hasApplicationStatus: !!kiprisData[0].application_status, + hasRegistrationDate: !!kiprisData[0].registration_date, + hasApplicantName: !!kiprisData[0].applicant_name, + hasClassificationCode: !!kiprisData[0].classification_code, + hasCategoryDescription: !!kiprisData[0].category_description, + hasDrawing: !!kiprisData[0].drawing, + bannedWordId: kiprisData[0].banned_word_id, + hasTradeMarkName: !!kiprisData[0].tradeMark_name + } : null + }); + + if (kiprisData.length === 0) { + results.innerHTML = ` +
+
📭
+
키프리스 검색 결과가 없습니다
+
해당 금지어에 대한 상표 검색 결과가 없습니다.
+
검색 조건: banned_word_id = ${wordWordId}
+
+ `; + return; + } + + // 키프리스 결과 표시 (스크롤 가능한 컨테이너 내에) + results.innerHTML = ` +
+
+ 📊 검색 결과 요약: 총 ${kiprisData.length}개의 키프리스 데이터를 찾았습니다. +
+ ${kiprisData.map((item, index) => { + // 상표명 불일치 검사 (빈값이 아닐 때만) + const tradeMarkName = item.tradeMark_name; + const isNameMismatch = tradeMarkName && + tradeMarkName.trim() !== '' && + word && + word.trim() !== '' && + tradeMarkName.toLowerCase() !== word.toLowerCase() && + !tradeMarkName.toLowerCase().includes(word.toLowerCase()) && + !word.toLowerCase().includes(tradeMarkName.toLowerCase()); + + return ` +
+
+

📋 검색 결과 ${index + 1}

+ 등록일: ${new Date(item.created_at).toLocaleDateString()} +
+ + + ${isNameMismatch ? ` +
+
+ ⚠️ + 상표명 불일치 감지 +
+
+
검색 키워드: "${word}"
+
등록 상표명: "${tradeMarkName}" (확인필요)
+
+
+ ` : ''} + +
+
+
+ 🏷️ 상표명: +
+ ${isNameMismatch ? + `${tradeMarkName} 불일치(확인필요)` : + (tradeMarkName || '정보 없음') + } +
+
+ +
+ 📋 출원상태: +
+ ${item.application_status || '정보 없음'} +
+
+ +
+ 📅 등록일: +
+ ${item.registration_date ? new Date(item.registration_date).toLocaleDateString() : '정보 없음'} +
+
+ +
+ 👤 출원인: +
+ ${item.applicant_name || '정보 없음'} +
+
+ +
+ 🏷️ 분류코드: +
+ ${item.classification_code || '정보 없음'} +
+
+
+ +
+ ${item.drawing ? ` +
+ 🖼️ 상표 도면: +
+ 상표 도면 +
+ 🖼️ 이미지를 불러올 수 없습니다 +
+
+
+ ` : ` +
+ 🖼️ 상표 도면: +
+ 📷 도면 정보 없음 +
+
+ `} +
+
+ + +
+ 📝 분류설명: +
+ ${item.category_description || '정보 없음'} +
+
+
+ `; + }).join('')} +
+ `; + + this.debugLog('키프리스 결과 UI 렌더링 완료', { count: kiprisData.length }); + + } catch (error) { + this.debugLog('키프리스 결과 로드 실패', { + error: error.message, + wordWordId, + word + }); + + results.innerHTML = ` +
+
+
오류가 발생했습니다
+
+ ${error.message} +
+
+ 디버그 정보: wordWordId=${wordWordId}, word="${word}" +
+
+ `; + } finally { + loading.style.display = "none"; + } + } + + // 로그 확인 함수 (토큰 상태 확인 대신) + showDebugLogs() { + this.debugLog('=== 디버그 로그 확인 ==='); + + // 현재 상태 정보 수집 + const statusInfo = { + '초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료', + 'SUPABASE_URL': this.SUPABASE_URL || '❌ 설정되지 않음', + 'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음', + 'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화', + '현재 시간': new Date().toLocaleString() + }; + + // 로그 정보를 문자열로 변환 + let logMessage = '🔍 현재 상태 정보:\n\n'; + for (const [key, value] of Object.entries(statusInfo)) { + logMessage += `${key}: ${value}\n`; + } + + // chrome.storage 상태 확인 + chrome.storage.local.get(null, (allData) => { + logMessage += '\n📦 Chrome Storage 내용:\n'; + logMessage += `- access_token: ${allData.access_token ? '있음' : '없음'}\n`; + logMessage += `- bannedWords_config: ${allData.bannedWords_config ? '있음' : '없음'}\n`; + + if (allData.bannedWords_config) { + const config = allData.bannedWords_config; + const age = Date.now() - config.timestamp; + logMessage += ` - 설정 나이: ${Math.floor(age / 1000)}초 전\n`; + logMessage += ` - URL: ${config.SUPABASE_URL ? '있음' : '없음'}\n`; + logMessage += ` - TOKEN: ${config.ACCESS_TOKEN ? '있음' : '없음'}\n`; + } + + // 콘솔에도 출력 + console.log('=== 디버그 로그 ==='); + console.log(statusInfo); + console.log('Chrome Storage:', allData); + + // 사용자에게 표시 + alert(logMessage); + + this.debugLog('디버그 로그 확인 완료'); + }); + } + + // 금지어 추가 모달 표시 + showAddBannedWordModal() { + this.debugLog('금지어 추가 모달 표시 시작'); + + // 모달 HTML 생성 + const modalId = 'add-banned-word-modal'; + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + const modalHtml = ` +
+
+

➕ 금지어 추가

+ +
+ + + + 💡 팁: 여러 단어를 한번에 등록하려면 콤마(,)로 구분하세요. 예: "단어1, 단어2, 단어3" + +
+ +
+ + +
+ +
+ + +
+
+
+ `; + + // 모달을 body에 추가 + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modal = document.getElementById(modalId); + const wordInput = document.getElementById('new-banned-word'); + const gradeSelect = document.getElementById('new-word-grade'); + const confirmBtn = document.getElementById('add-confirm-btn'); + const cancelBtn = document.getElementById('add-cancel-btn'); + + // 확인/취소 처리 함수 + const handleConfirm = async () => { + const inputText = wordInput.value.trim(); + const selectedGrade = gradeSelect.value; + + if (!inputText) { + alert('❌ 금지어를 입력해주세요.'); + wordInput.focus(); + return; + } + + // 콤마로 구분하여 여러 단어 처리 + const wordsToAdd = inputText.split(',').map(word => word.trim()).filter(word => word.length > 0); + + if (wordsToAdd.length === 0) { + alert('❌ 유효한 금지어를 입력해주세요.'); + wordInput.focus(); + return; + } + + this.debugLog('추가할 금지어 목록', { wordsToAdd, selectedGrade, count: wordsToAdd.length }); + + try { + confirmBtn.textContent = '🔄 추가 중...'; + confirmBtn.disabled = true; + cancelBtn.disabled = true; + + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + // 현재 사용자 정보 가져오기 + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!authRes.ok) { + throw new Error('사용자 인증 실패'); + } + + const authUser = await authRes.json(); + + // 사용자 ID 가져오기 + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!userRes.ok) { + throw new Error('사용자 정보 조회 실패'); + } + + const userData = await userRes.json(); + if (!userData || userData.length === 0) { + throw new Error('사용자 데이터를 찾을 수 없습니다'); + } + + const userId = userData[0].id; + + // 기존 금지어 목록 가져오기 (중복 검사용) + const existingRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=banned_word&user_id=eq.${userId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!existingRes.ok) { + throw new Error('기존 금지어 목록 조회 실패'); + } + + const existingWords = await existingRes.json(); + const existingWordSet = new Set(existingWords.map(item => item.banned_word.toLowerCase())); + + // 중복 검사 + const duplicateWords = []; + const newWords = []; + + wordsToAdd.forEach(word => { + if (existingWordSet.has(word.toLowerCase())) { + duplicateWords.push(word); + } else { + newWords.push(word); + } + }); + + this.debugLog('중복 검사 결과', { + totalWords: wordsToAdd.length, + newWords: newWords.length, + duplicateWords: duplicateWords.length, + duplicates: duplicateWords + }); + + // 중복된 단어가 있으면 사용자에게 알림 + if (duplicateWords.length > 0) { + const duplicateMessage = `⚠️ 다음 단어는 이미 등록되어 있습니다:\n${duplicateWords.join(', ')}\n\n`; + + if (newWords.length > 0) { + const proceed = confirm(`${duplicateMessage}새로운 단어만 추가하시겠습니까?\n추가될 단어: ${newWords.join(', ')}`); + if (!proceed) { + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + return; + } + } else { + alert(`${duplicateMessage}추가할 새로운 단어가 없습니다.`); + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + return; + } + } + + if (newWords.length === 0) { + alert('❌ 추가할 새로운 금지어가 없습니다.'); + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + return; + } + + // 새로운 금지어들을 배치로 추가 + const wordsData = newWords.map(word => ({ + banned_word: word, + grade: selectedGrade, + user_id: userId, + created_at: new Date().toISOString() + })); + + this.debugLog('금지어 배치 추가 API 호출', { count: wordsData.length, grade: selectedGrade }); + + const addRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(wordsData) + }); + + this.debugLog('금지어 추가 API 응답', { + status: addRes.status, + statusText: addRes.statusText, + ok: addRes.ok + }); + + if (!addRes.ok) { + const errorDetail = await addRes.text(); + this.debugLog('금지어 추가 실패', { + status: addRes.status, + error: errorDetail + }); + throw new Error(`금지어 추가 실패 (${addRes.status}: ${addRes.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('금지어 추가 성공', { newWords, selectedGrade }); + + let successMessage = `✅ ${newWords.length}개의 금지어가 추가되었습니다.\n\n`; + successMessage += `등급: ${selectedGrade}\n`; + successMessage += `추가된 단어: ${newWords.join(', ')}`; + + if (duplicateWords.length > 0) { + successMessage += `\n\n⚠️ 중복으로 제외된 단어: ${duplicateWords.join(', ')}`; + } + + alert(successMessage); + + // 모달 닫기 + modal.remove(); + + // 목록 새로고침 + await this.loadBannedWords(); + + } catch (error) { + this.debugLog('금지어 추가 실패', { + error: error.message, + wordsToAdd, + selectedGrade + }); + alert(`❌ 금지어 추가 중 오류가 발생했습니다:\n${error.message}`); + + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + } + }; + + const handleCancel = () => { + this.debugLog('금지어 추가 취소됨'); + modal.remove(); + }; + + // 이벤트 리스너 등록 + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + + // 키보드 이벤트 처리 + const handleKeydown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleConfirm(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancel(); + } + }; + + modal.addEventListener('keydown', handleKeydown); + + // 모달 외부 클릭 시 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + handleCancel(); + } + }); + + // 입력란에 포커스 + wordInput.focus(); + } +} + +// 페이지 로드 시 초기화 +document.addEventListener('DOMContentLoaded', async () => { + console.log('=== 금지어 관리 페이지 초기화 시작 ==='); + console.log('현재 시간:', new Date().toLocaleString()); + + // chrome.storage에서 설정 확인 + let debugMode = false; // 기본값 + try { + const configData = await chrome.storage.local.get('bannedWords_config'); + console.log('chrome.storage 설정 확인:', { + hasBannedWordsConfig: !!configData.bannedWords_config, + config: configData.bannedWords_config ? { + hasUrl: !!configData.bannedWords_config.SUPABASE_URL, + hasKey: !!configData.bannedWords_config.SUPABASE_ANON_KEY, + hasToken: !!configData.bannedWords_config.ACCESS_TOKEN, + debugMode: configData.bannedWords_config.DEBUG_MODE, + timestamp: configData.bannedWords_config.timestamp + } : null + }); + + // DEBUG_MODE 설정 확인 + if (configData.bannedWords_config && configData.bannedWords_config.DEBUG_MODE !== undefined) { + debugMode = configData.bannedWords_config.DEBUG_MODE; + } + + } catch (error) { + console.error('chrome.storage 설정 확인 실패:', error); + } + + // 디버그 요소 표시/숨김 처리 + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + if (debugMode) { + debugElement.style.display = "block"; + console.log('디버그 모드 활성화 - 디버그 정보 표시'); + } else { + debugElement.style.display = "none"; + console.log('디버그 모드 비활성화 - 디버그 정보 숨김'); + } + } + + // 로그 확인 버튼 이벤트 등록 (DEBUG_MODE일 때만) + const logCheckBtn = document.getElementById('token-check-btn'); // ID는 그대로 유지 (HTML 변경 최소화) + if (logCheckBtn) { + if (debugMode) { + console.log('디버그 모드: 로그 확인 버튼 이벤트 등록'); + logCheckBtn.textContent = '📋 로그 확인'; // 버튼 텍스트 변경 + logCheckBtn.style.display = "inline-block"; + + // 기존 이벤트 리스너 제거 후 새로 등록 + logCheckBtn.replaceWith(logCheckBtn.cloneNode(true)); + const newLogCheckBtn = document.getElementById('token-check-btn'); + + newLogCheckBtn.addEventListener('click', async (e) => { + e.preventDefault(); + console.log('로그 확인 버튼 클릭됨'); + + // 버튼 상태 변경 + const originalText = newLogCheckBtn.textContent; + newLogCheckBtn.textContent = '🔄 확인 중...'; + newLogCheckBtn.disabled = true; + + try { + // BannedWordsManager가 초기화되었는지 확인 + if (window.bannedWordsManager && typeof window.bannedWordsManager.showDebugLogs === 'function') { + await window.bannedWordsManager.showDebugLogs(); + } else { + console.error('BannedWordsManager가 초기화되지 않았습니다'); + alert('❌ 관리자가 초기화되지 않았습니다. 페이지를 새로고침해주세요.'); + } + } finally { + // 버튼 상태 복원 + newLogCheckBtn.textContent = originalText; + newLogCheckBtn.disabled = false; + } + }); + console.log('로그 확인 버튼 이벤트 등록 완료'); + } else { + console.log('디버그 모드 비활성화: 로그 확인 버튼 숨김'); + logCheckBtn.style.display = "none"; + } + } else { + if (debugMode) { + console.warn('로그 확인 버튼을 찾을 수 없음'); + } + } + + try { + // BannedWordsManager 초기화 + console.log('BannedWordsManager 인스턴스 생성 중...'); + window.bannedWordsManager = new BannedWordsManager(); + + console.log('BannedWordsManager 초기화 시작...'); + await window.bannedWordsManager.initialize(); + + console.log('✅ 금지어 관리 페이지 초기화 완료'); + + } catch (error) { + console.error('❌ 초기화 실패:', error); + + // UI에 오류 표시 + const statsElement = document.getElementById('banned-words-stats'); + if (statsElement) { + statsElement.innerHTML = ` +
+ ❌ 초기화 실패
+ ${error.message}
+ 로그인 후 다시 시도해주세요. +
+ `; + } + + const tbody = document.getElementById("banned-words-tbody"); + if (tbody) { + tbody.innerHTML = `초기화 실패: ${error.message}`; + } + + // 디버그 정보에도 표시 (DEBUG_MODE일 때만) + if (debugMode) { + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + debugElement.innerHTML = `❌ 초기화 실패: ${error.message}
`; + debugElement.style.display = "block"; + + // 버튼 이벤트 다시 등록 + const newBtn = document.getElementById('token-check-btn'); + if (newBtn) { + newBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + // 간단한 로그 정보 표시 + const logInfo = `❌ 초기화 실패 상태\n\n오류: ${error.message}\n\n시간: ${new Date().toLocaleString()}`; + alert(logInfo); + }); + } + } + } + } + + console.log('=== DOMContentLoaded 이벤트 처리 완료 ==='); +}); + +// 전역 함수로 등록 (HTML에서 호출할 수 있도록) +window.bannedWordsManager = null; \ No newline at end of file diff --git a/wrmc_ext/content.js b/wrmc_ext/content.js new file mode 100644 index 0000000..9699c14 --- /dev/null +++ b/wrmc_ext/content.js @@ -0,0 +1,866 @@ +// content.js + +let lastContextMenuPos = null; +let tooltipEl = null; +let currentKeyword = null; // 현재 검색 키워드 저장 +let loadingIndicator = null; // 로딩 인디케이터 요소 + +// 마우스 위치 추적 +let currentMousePos = { x: 0, y: 0 }; +document.addEventListener('mousemove', (e) => { + currentMousePos = { x: e.pageX, y: e.pageY }; +}); + +document.addEventListener("contextmenu", (e) => { + lastContextMenuPos = { x: e.pageX, y: e.pageY }; +}); + +// ESC 키로 모달 닫기 +document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + if (tooltipEl) removeTooltip(); + if (loadingIndicator) removeLoadingIndicator(); + } +}); + +// 로딩 인디케이터 생성 및 표시 +function showLoadingIndicator(message, position = null) { + // 기존 로딩 인디케이터가 있으면 제거 + if (loadingIndicator) { + removeLoadingIndicator(); + } + + loadingIndicator = document.createElement("div"); + loadingIndicator.id = "markinfo-loading"; + loadingIndicator.style.cssText = ` + position: fixed; + z-index: 9999999; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 12px 20px; + border-radius: 8px; + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 14px; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + gap: 10px; + animation: fadeIn 0.3s ease-out; + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.2); + `; + + // 스피너 아이콘 + const spinner = document.createElement("div"); + spinner.style.cssText = ` + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top: 2px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + `; + + // 메시지 텍스트 + const messageEl = document.createElement("span"); + messageEl.textContent = message; + + loadingIndicator.appendChild(spinner); + loadingIndicator.appendChild(messageEl); + + // CSS 애니메이션 정의 + if (!document.getElementById('markinfo-loading-styles')) { + const style = document.createElement('style'); + style.id = 'markinfo-loading-styles'; + style.textContent = ` + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + @keyframes fadeIn { + 0% { opacity: 0; transform: translateY(-10px); } + 100% { opacity: 1; transform: translateY(0); } + } + @keyframes fadeOut { + 0% { opacity: 1; transform: translateY(0); } + 100% { opacity: 0; transform: translateY(-10px); } + } + `; + document.head.appendChild(style); + } + + document.body.appendChild(loadingIndicator); + + // 위치 설정 + const pos = position || getSelectionPosition() || currentMousePos; + positionLoadingIndicator(loadingIndicator, pos); + + console.log('[content.js] 로딩 인디케이터 표시:', message); +} + +// 로딩 인디케이터 제거 +function removeLoadingIndicator() { + if (loadingIndicator) { + loadingIndicator.style.animation = 'fadeOut 0.3s ease-out'; + setTimeout(() => { + if (loadingIndicator && loadingIndicator.parentNode) { + loadingIndicator.parentNode.removeChild(loadingIndicator); + } + loadingIndicator = null; + }, 300); + console.log('[content.js] 로딩 인디케이터 제거'); + } +} + +// 선택된 텍스트의 위치 가져오기 +function getSelectionPosition() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + return { + x: rect.left + window.pageXOffset + rect.width / 2, + y: rect.top + window.pageYOffset - 10 + }; + } + } + return null; +} + +// 로딩 인디케이터 위치 설정 +function positionLoadingIndicator(indicator, pos) { + indicator.style.left = (pos.x - 50) + "px"; // 중앙 정렬을 위해 조정 + indicator.style.top = (pos.y - 50) + "px"; + + // 화면 경계 체크 + const rect = indicator.getBoundingClientRect(); + const docWidth = document.documentElement.clientWidth; + const docHeight = document.documentElement.clientHeight; + + if (rect.right > docWidth) { + indicator.style.left = (docWidth - rect.width - 10) + "px"; + } + if (rect.left < 0) { + indicator.style.left = "10px"; + } + if (rect.bottom > docHeight) { + indicator.style.top = (docHeight - rect.height - 10) + "px"; + } + if (rect.top < 0) { + indicator.style.top = "10px"; + } +} + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log('[content.js] 메시지 수신:', message); + + // 핑 테스트 응답 + if (message.action === "ping") { + console.log('[content.js] 핑 메시지 수신, 응답 전송'); + sendResponse({ status: "ready" }); + return true; + } + + // 로딩 인디케이터 표시 요청 + if (message.action === "showLoading") { + showLoadingIndicator(message.message, message.position); + sendResponse({ success: true }); + return true; + } + + // 로딩 인디케이터 제거 요청 + if (message.action === "hideLoading") { + removeLoadingIndicator(); + sendResponse({ success: true }); + return true; + } + + if (message.action === "showTooltip") { + console.log(`[content.js] showTooltip 메시지 수신, 키워드: ${message.keyword}, 결과 개수: ${message.detailInfo?.length || 0}`); + + // 로딩 인디케이터 제거 + removeLoadingIndicator(); + + // 현재 키워드 저장 + currentKeyword = message.keyword; + + try { + if (!tooltipEl) { + tooltipEl = document.createElement("div"); + tooltipEl.id = "markinfo-tooltip"; + tooltipEl.style.position = "absolute"; + tooltipEl.style.zIndex = "999999"; + tooltipEl.style.background = "#fff"; + tooltipEl.style.border = "1px solid #ccc"; + tooltipEl.style.borderRadius = "8px"; + tooltipEl.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; + tooltipEl.style.fontFamily = "'Roboto', sans-serif"; + tooltipEl.style.fontSize = "14px"; + tooltipEl.style.color = "#333"; + tooltipEl.style.maxWidth = "600px"; + tooltipEl.style.maxHeight = "500px"; + // flex 컬럼 레이아웃으로 구성 + tooltipEl.style.display = "flex"; + tooltipEl.style.flexDirection = "column"; + + // 헤더: 항상 보이는 영역 (sticky) + const headerDiv = document.createElement("div"); + headerDiv.id = "markinfo-tooltip-header"; + headerDiv.style.position = "sticky"; + headerDiv.style.top = "0"; + headerDiv.style.background = "#fff"; + headerDiv.style.padding = "12px 16px"; + headerDiv.style.borderBottom = "1px solid #ccc"; + headerDiv.style.display = "flex"; + headerDiv.style.justifyContent = "space-between"; + headerDiv.style.alignItems = "center"; + + // 헤더 내부: 검색 키워드와 제작자 정보를 수직 정렬 + const headerContent = document.createElement("div"); + headerContent.style.display = "flex"; + headerContent.style.flexDirection = "column"; + + // 검색 키워드 제목 + const titleElem = document.createElement("h2"); + titleElem.id = "tooltip-title"; + titleElem.style.margin = "0"; + titleElem.style.fontSize = "20px"; + titleElem.style.color = "#2c3e50"; + headerContent.appendChild(titleElem); + + // 제작자 정보 (작은 글씨) + const creatorElem = document.createElement("span"); + creatorElem.id = "tooltip-creator"; + creatorElem.textContent = "내차는언제타냐: 지재권 검색기 (ESC키로 닫기)"; + creatorElem.style.fontSize = "12px"; + creatorElem.style.color = "#7f8c8d"; + headerContent.appendChild(creatorElem); + + headerDiv.appendChild(headerContent); + + // 헤더 버튼 영역 + const headerButtons = document.createElement("div"); + headerButtons.style.display = "flex"; + headerButtons.style.gap = "8px"; + + // 금지어 추가 버튼 (헤더) + const addBannedBtn = document.createElement("button"); + addBannedBtn.id = "add-banned-word-btn"; + addBannedBtn.textContent = "내 금지어에 추가"; + addBannedBtn.style.padding = "6px 12px"; + addBannedBtn.style.backgroundColor = "#f39c12"; + addBannedBtn.style.color = "#fff"; + addBannedBtn.style.border = "none"; + addBannedBtn.style.borderRadius = "4px"; + addBannedBtn.style.cursor = "pointer"; + addBannedBtn.style.fontSize = "12px"; + addBannedBtn.onclick = () => addToBannedWords(currentKeyword); + headerButtons.appendChild(addBannedBtn); + + // 내부 닫기 버튼 (헤더 우측) + const headerCloseBtn = document.createElement("button"); + headerCloseBtn.textContent = "닫기"; + headerCloseBtn.style.padding = "6px 10px"; + headerCloseBtn.style.backgroundColor = "#e74c3c"; + headerCloseBtn.style.color = "#fff"; + headerCloseBtn.style.border = "none"; + headerCloseBtn.style.borderRadius = "4px"; + headerCloseBtn.style.cursor = "pointer"; + headerCloseBtn.onclick = removeTooltip; + headerButtons.appendChild(headerCloseBtn); + + headerDiv.appendChild(headerButtons); + + // 본문 영역 (스크롤 가능) + const bodyDiv = document.createElement("div"); + bodyDiv.id = "markinfo-tooltip-body"; + bodyDiv.style.padding = "16px"; + bodyDiv.style.overflowY = "auto"; + bodyDiv.style.flex = "1 1 auto"; + + tooltipEl.appendChild(headerDiv); + tooltipEl.appendChild(bodyDiv); + + document.body.appendChild(tooltipEl); + + // 글로벌 닫기 버튼 (항상 보이는 우측 상단) + ensureGlobalCloseButton(); + } + + // 업데이트: 헤더 제목에 검색 키워드 설정 + document.getElementById("tooltip-title").textContent = "검색 키워드: " + message.keyword; + renderDetailInfo(message.detailInfo, message.keyword); + + if (lastContextMenuPos) { + positionTooltip(tooltipEl, lastContextMenuPos); + } else { + tooltipEl.style.top = "10px"; + tooltipEl.style.left = "10px"; + } + + console.log('[content.js] 툴팁 표시 완료'); + + // 성공 응답 전송 + sendResponse({ success: true, message: "툴팁이 성공적으로 표시되었습니다." }); + + } catch (error) { + console.error('[content.js] 툴팁 표시 중 오류:', error); + + // 오류 응답 전송 + sendResponse({ success: false, error: error.message }); + } + + // 비동기 응답을 위해 true 반환 + return true; + } + + // 멀티번역 결과 표시 + if (message.action === "showTranslationTooltip") { + console.log(`[content.js] showTranslationTooltip 메시지 수신, 원문: ${message.originalText}, 결과 개수: ${message.results?.length || 0}`); + + // 로딩 인디케이터 제거 + removeLoadingIndicator(); + + try { + showTranslationResults(message.originalText, message.results, message.userLevel); + + // 성공 응답 전송 + sendResponse({ success: true, message: "번역 결과가 성공적으로 표시되었습니다." }); + + } catch (error) { + console.error('[content.js] 번역 결과 표시 중 오류:', error); + + // 오류 응답 전송 + sendResponse({ success: false, error: error.message }); + } + + // 비동기 응답을 위해 true 반환 + return true; + } +}); + +function positionTooltip(tooltip, pos) { + tooltip.style.left = (pos.x + 10) + "px"; + tooltip.style.top = (pos.y + 10) + "px"; + const rect = tooltip.getBoundingClientRect(); + const docWidth = document.documentElement.clientWidth; + const docHeight = document.documentElement.clientHeight; + if (rect.right > docWidth) { + tooltip.style.left = (docWidth - rect.width - 10) + "px"; + } + if (rect.bottom > docHeight) { + tooltip.style.top = (docHeight - rect.height - 10) + "px"; + } +} + +function removeTooltip() { + if (tooltipEl && tooltipEl.parentNode) { + tooltipEl.parentNode.removeChild(tooltipEl); + } + tooltipEl = null; + const globalClose = document.getElementById("tooltip-global-close"); + if (globalClose && globalClose.parentNode) { + globalClose.parentNode.removeChild(globalClose); + } +} + +function ensureGlobalCloseButton() { + if (!document.getElementById("tooltip-global-close")) { + const btn = document.createElement("button"); + btn.id = "tooltip-global-close"; + btn.textContent = "닫기"; + btn.style.position = "fixed"; + btn.style.top = "20px"; + btn.style.right = "20px"; + btn.style.padding = "8px 12px"; + btn.style.backgroundColor = "#e74c3c"; + btn.style.color = "#fff"; + btn.style.border = "none"; + btn.style.borderRadius = "4px"; + btn.style.cursor = "pointer"; + btn.style.zIndex = "1000000"; + btn.onclick = removeTooltip; + document.body.appendChild(btn); + } +} + +function renderDetailInfo(results, keyword) { + // 검색 결과를 전역 변수에 저장 + window.currentSearchResults = results; + + const bodyDiv = document.getElementById("markinfo-tooltip-body"); + if (!bodyDiv) return; + let html = `
`; + // 결과가 없을 경우 안전한 단어 표시 + if (results.error) { + html += `

오류: ${results.error}

`; + } else if (!Array.isArray(results) || results.length === 0) { + html += `

지식재산권이 없는 안전한 단어

`; + } else { + results.forEach((result, idx) => { + html += `
`; + + // 결과 헤더 (제목과 개별 금지어 추가 버튼) + html += `
`; + html += `

${idx + 1}번 결과

`; + // html += ``; + html += `
`; + + // 상세 검색 결과 (출원번호 상세 조회) + if (result.detail) { + html += `

상세 정보

`; + const dreg = result.detail.registration_info; + + // 상표명 불일치 검사 및 표시 + const trademarkName = dreg.trademarkName || "(상표명 없음)"; + const isNameMismatch = trademarkName !== "(상표명 없음)" && + keyword && + trademarkName.toLowerCase() !== keyword.toLowerCase() && + !trademarkName.toLowerCase().includes(keyword.toLowerCase()) && + !keyword.toLowerCase().includes(trademarkName.toLowerCase()); + + if (isNameMismatch) { + html += `

상표명: ${trademarkName} (불일치-확인필요)

`; + } else { + html += `

상표명: ${trademarkName}

`; + } + + html += `

출원번호: ${dreg.applicationNum || "(출원번호 없음)"}

`; + html += `

출원날짜: ${dreg.applicationDate || "(출원날짜 없음)"}

`; + html += `

권리상태: ${dreg.lastDisposalCodeName || "(권리상태 없음)"}

`; + html += `

공고번호: ${dreg.publicationNum || "(공고번호 없음)"}

`; + html += `

등록번호: ${dreg.registerNum || "(등록번호 없음)"}

`; + + html += `

권리정보

`; + if (result.detail.rights_info && Object.keys(result.detail.rights_info).length > 0) { + for (let key in result.detail.rights_info) { + html += `

카테고리 코드: ${key}

`; + result.detail.rights_info[key].forEach(item => { + html += `

- 지정상품명: ${item.asignProductName || ""}

`; + html += `

  영문: ${item.asignProductNameEn || ""}

`; + html += `

  유사군코드: ${item.similarCodes || ""}

`; + }); + } + } else { + html += `

(권리정보 없음)

`; + } + + html += `

출원인 정보

`; + if (result.detail.applicant_info && result.detail.applicant_info.mapping) { + const mapping = result.detail.applicant_info.mapping; + html += `

국가명: ${mapping.nationalCodeName || "(없음)"}

`; + html += `

출원인명: ${mapping.applicantName || "(없음)"}

`; + } else { + html += `

(출원인 정보 없음)

`; + } + } else if (result.detailError) { + html += `

상세 정보 검색 오류: ${result.detailError}

`; + } + html += `
`; + }); + } + html += `
`; + bodyDiv.innerHTML = html; +} + +// 금지어 추가 함수 +async function addToBannedWords(keyword) { + try { + console.log(`[content.js] 금지어 추가 시작: ${keyword}`); + + // Grade 선택 모달 표시 + const selectedGrade = await showGradeSelectionModal(keyword); + if (!selectedGrade) { + console.log('[content.js] 사용자가 금지어 추가를 취소했습니다.'); + return; + } + + // 백그라운드 스크립트에 금지어 추가 요청 + chrome.runtime.sendMessage({ + action: "addBannedWord", + keyword: keyword, + grade: selectedGrade, + searchResults: getCurrentSearchResults() + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[content.js] 금지어 추가 메시지 전송 실패:', chrome.runtime.lastError); + alert('금지어 추가 중 오류가 발생했습니다.'); + return; + } + + if (response && response.success) { + console.log('[content.js] 금지어 추가 성공'); + alert(`"${keyword}"이(가) 금지어 목록에 추가되었습니다. (등급: ${selectedGrade})`); + + // 금지어 추가 버튼 비활성화 + const addBtn = document.getElementById("add-banned-word-btn"); + if (addBtn) { + addBtn.textContent = "추가 완료"; + addBtn.disabled = true; + addBtn.style.backgroundColor = "#95a5a6"; + addBtn.style.cursor = "not-allowed"; + } + } else { + console.error('[content.js] 금지어 추가 실패:', response?.error); + alert(`금지어 추가 실패: ${response?.error || '알 수 없는 오류'}`); + } + }); + + } catch (error) { + console.error('[content.js] 금지어 추가 중 오류:', error); + alert('금지어 추가 중 오류가 발생했습니다.'); + } +} + +// Grade 선택 모달 함수 +function showGradeSelectionModal(keyword) { + return new Promise((resolve) => { + // 모달 배경 + const modalOverlay = document.createElement('div'); + modalOverlay.style.position = 'fixed'; + modalOverlay.style.top = '0'; + modalOverlay.style.left = '0'; + modalOverlay.style.width = '100%'; + modalOverlay.style.height = '100%'; + modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + modalOverlay.style.zIndex = '1000000'; + modalOverlay.style.display = 'flex'; + modalOverlay.style.justifyContent = 'center'; + modalOverlay.style.alignItems = 'center'; + + // 모달 컨테이너 + const modalContainer = document.createElement('div'); + modalContainer.style.backgroundColor = '#fff'; + modalContainer.style.borderRadius = '8px'; + modalContainer.style.padding = '24px'; + modalContainer.style.maxWidth = '400px'; + modalContainer.style.width = '90%'; + modalContainer.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)'; + modalContainer.style.fontFamily = "'Roboto', sans-serif"; + + // 제목 + const title = document.createElement('h3'); + title.textContent = '금지어 등급 선택'; + title.style.margin = '0 0 16px 0'; + title.style.color = '#2c3e50'; + title.style.fontSize = '18px'; + modalContainer.appendChild(title); + + // 키워드 표시 + const keywordLabel = document.createElement('p'); + keywordLabel.textContent = `키워드: "${keyword}"`; + keywordLabel.style.margin = '0 0 16px 0'; + keywordLabel.style.color = '#7f8c8d'; + keywordLabel.style.fontSize = '14px'; + modalContainer.appendChild(keywordLabel); + + // 설명 + const description = document.createElement('p'); + description.textContent = '이 키워드의 금지 등급을 선택해주세요:'; + description.style.margin = '0 0 12px 0'; + description.style.color = '#34495e'; + description.style.fontSize = '14px'; + modalContainer.appendChild(description); + + // Grade 선택 드롭박스 + const gradeSelect = document.createElement('select'); + gradeSelect.style.width = '100%'; + gradeSelect.style.padding = '8px 12px'; + gradeSelect.style.border = '1px solid #bdc3c7'; + gradeSelect.style.borderRadius = '4px'; + gradeSelect.style.fontSize = '14px'; + gradeSelect.style.marginBottom = '20px'; + + // 옵션 추가 + const gradeOptions = [ + { value: '비허용', text: '비허용(단어제거)' }, + { value: '금지', text: '금지(상품금지)' } + ]; + + gradeOptions.forEach(option => { + const optionElement = document.createElement('option'); + optionElement.value = option.value; + optionElement.textContent = option.text; + if (option.value === '비허용') { + optionElement.selected = true; // 기본값: 비허용 + } + gradeSelect.appendChild(optionElement); + }); + + modalContainer.appendChild(gradeSelect); + + // 버튼 컨테이너 + const buttonContainer = document.createElement('div'); + buttonContainer.style.display = 'flex'; + buttonContainer.style.justifyContent = 'flex-end'; + buttonContainer.style.gap = '8px'; + + // 취소 버튼 + const cancelButton = document.createElement('button'); + cancelButton.textContent = '취소'; + cancelButton.style.padding = '8px 16px'; + cancelButton.style.backgroundColor = '#95a5a6'; + cancelButton.style.color = '#fff'; + cancelButton.style.border = 'none'; + cancelButton.style.borderRadius = '4px'; + cancelButton.style.cursor = 'pointer'; + cancelButton.style.fontSize = '14px'; + + cancelButton.onclick = () => { + document.body.removeChild(modalOverlay); + resolve(null); // 취소 + }; + + // 확인 버튼 + const confirmButton = document.createElement('button'); + confirmButton.textContent = '추가'; + confirmButton.style.padding = '8px 16px'; + confirmButton.style.backgroundColor = '#f39c12'; + confirmButton.style.color = '#fff'; + confirmButton.style.border = 'none'; + confirmButton.style.borderRadius = '4px'; + confirmButton.style.cursor = 'pointer'; + confirmButton.style.fontSize = '14px'; + + confirmButton.onclick = () => { + const selectedGrade = gradeSelect.value; + document.body.removeChild(modalOverlay); + resolve(selectedGrade); + }; + + buttonContainer.appendChild(cancelButton); + buttonContainer.appendChild(confirmButton); + modalContainer.appendChild(buttonContainer); + + // ESC 키로 닫기 + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + document.body.removeChild(modalOverlay); + document.removeEventListener('keydown', handleKeyDown); + resolve(null); + } + }; + + document.addEventListener('keydown', handleKeyDown); + + modalOverlay.appendChild(modalContainer); + document.body.appendChild(modalOverlay); + + // 드롭박스에 포커스 + gradeSelect.focus(); + }); +} + +// 현재 검색 결과 데이터 가져오기 +function getCurrentSearchResults() { + // 현재 표시된 검색 결과 데이터를 반환 + // 이 데이터는 renderDetailInfo에서 사용된 results와 동일해야 함 + return window.currentSearchResults || []; +} + +// 번역 결과 모달 표시 +function showTranslationResults(originalText, results, userLevel) { + console.log('[content.js] 번역 결과 표시 시작'); + + let translationTooltip = document.getElementById("translation-tooltip"); + + if (!translationTooltip) { + translationTooltip = document.createElement("div"); + translationTooltip.id = "translation-tooltip"; + translationTooltip.style.position = "fixed"; + translationTooltip.style.zIndex = "9999999"; + translationTooltip.style.background = "#fff"; + translationTooltip.style.border = "2px solid #3498db"; + translationTooltip.style.borderRadius = "12px"; + translationTooltip.style.boxShadow = "0 8px 24px rgba(0,0,0,0.2)"; + translationTooltip.style.fontFamily = "'Roboto', sans-serif"; + translationTooltip.style.fontSize = "14px"; + translationTooltip.style.color = "#333"; + translationTooltip.style.minWidth = "400px"; + translationTooltip.style.maxWidth = "800px"; + translationTooltip.style.maxHeight = "600px"; + translationTooltip.style.display = "flex"; + translationTooltip.style.flexDirection = "column"; + + // 헤더 + const header = document.createElement("div"); + header.style.background = "linear-gradient(135deg, #3498db, #2980b9)"; + header.style.color = "#fff"; + header.style.padding = "16px 20px"; + header.style.borderRadius = "10px 10px 0 0"; + header.style.display = "flex"; + header.style.justifyContent = "space-between"; + header.style.alignItems = "center"; + + // 헤더 내용 + const headerContent = document.createElement("div"); + + const titleElem = document.createElement("h2"); + titleElem.style.margin = "0"; + titleElem.style.fontSize = "18px"; + titleElem.textContent = "멀티번역 결과"; + headerContent.appendChild(titleElem); + + const subtitleElem = document.createElement("div"); + subtitleElem.style.fontSize = "12px"; + subtitleElem.style.opacity = "0.9"; + subtitleElem.style.marginTop = "4px"; + subtitleElem.textContent = `회원등급: ${userLevel || 'Basic'} | ESC키로 닫기`; + headerContent.appendChild(subtitleElem); + + header.appendChild(headerContent); + + // 닫기 버튼 + const closeBtn = document.createElement("button"); + closeBtn.textContent = "×"; + closeBtn.style.background = "none"; + closeBtn.style.border = "2px solid #fff"; + closeBtn.style.color = "#fff"; + closeBtn.style.borderRadius = "50%"; + closeBtn.style.width = "30px"; + closeBtn.style.height = "30px"; + closeBtn.style.cursor = "pointer"; + closeBtn.style.fontSize = "18px"; + closeBtn.style.lineHeight = "1"; + closeBtn.onclick = removeTranslationTooltip; + header.appendChild(closeBtn); + + // 본문 + const body = document.createElement("div"); + body.id = "translation-body"; + body.style.padding = "20px"; + body.style.overflowY = "auto"; + body.style.flex = "1 1 auto"; + + translationTooltip.appendChild(header); + translationTooltip.appendChild(body); + + document.body.appendChild(translationTooltip); + + // ESC 키로 닫기 + document.addEventListener("keydown", function(e) { + if (e.key === "Escape") { + removeTranslationTooltip(); + } + }); + } + + // 본문 업데이트 + renderTranslationResults(originalText, results, userLevel); + + // 위치 설정 (화면 중앙) + translationTooltip.style.top = "50%"; + translationTooltip.style.left = "50%"; + translationTooltip.style.transform = "translate(-50%, -50%)"; + + console.log('[content.js] 번역 결과 표시 완료'); +} + +// 번역 결과 렌더링 +function renderTranslationResults(originalText, results, userLevel) { + const body = document.getElementById("translation-body"); + if (!body) return; + + let html = ''; + + // 원문 표시 + html += ` +
+

원문

+

${originalText}

+
+ `; + + // 번역 결과가 있을 경우 + if (results && results.length > 0) { + html += '
'; + + results.forEach((result, index) => { + const engineName = getEngineDisplayName(result.engine); + const isSuccess = result.success; + + // 각 번역 결과 카드 + html += ` +
+
+

+ ${engineName} +

+ + ${isSuccess ? '성공' : '실패'} + +
+ + ${isSuccess ? + `

${result.translatedText}

` : + `

${result.error || '번역 실패'}

` + } +
+ `; + }); + + html += '
'; + } else { + // 번역 결과가 없는 경우 + html += ` +
+

번역 결과가 없습니다.

+

다시 시도해 주세요.

+
+ `; + } + + // 회원등급별 가이드 메시지 + html += ` +
+

+ 💡 ${userLevel || 'Basic'} 회원으로 이용 중입니다. + ${getUserLevelGuide(userLevel)} +

+
+ `; + + body.innerHTML = html; +} + +// 엔진 이름 표시용 변환 +function getEngineDisplayName(engine) { + const displayNames = { + 'google': '구글 번역', + 'deepl': 'DeepL', + 'openai': 'ChatGPT', + 'gemini': 'Google Gemini', + 'mymemory': 'MyMemory' + }; + return displayNames[engine] || engine; +} + +// 회원등급별 가이드 메시지 +function getUserLevelGuide(userLevel) { + const normalizedLevel = (userLevel || 'basic').toLowerCase(); + + switch(normalizedLevel) { + case 'vip': + return '모든 번역 엔진을 이용할 수 있습니다!'; + case 'premium': + return '무료 번역과 DeepL을 이용할 수 있습니다. VIP로 업그레이드하면 ChatGPT, Gemini도 이용 가능합니다.'; + case 'basic': + default: + return '현재 무료 번역만 이용 가능합니다. 프리미엄 회원으로 업그레이드하면 더 많은 번역 엔진을 이용할 수 있습니다.'; + } +} + +// 번역 툴팁 제거 +function removeTranslationTooltip() { + const translationTooltip = document.getElementById("translation-tooltip"); + if (translationTooltip) { + translationTooltip.remove(); + console.log('[content.js] 번역 툴팁 제거됨'); + } +} diff --git a/wrmc_ext/icon.png b/wrmc_ext/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1c0eaf8c3a73be488b4586c38d9cd44b9066f509 GIT binary patch literal 20724 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 ryON)IPm_}FY%{?A!~et2Z#)"], + "js": ["content.js"], + "run_at": "document_end" + } + ], + "action": { + "default_popup": "popup.html", + "default_icon": "icon.png", + "default_title": "내차는언제타냐 통합 확장" + }, + "commands": { + "trademark-search": { + "suggested_key": { + "default": "Ctrl+Shift+S" + }, + "description": "지재권 검색" + }, + "multi-translate": { + "suggested_key": { + "default": "Ctrl+Shift+E" + }, + "description": "멀티번역" + }, + "korean-to-chinese": { + "suggested_key": { + "default": "Ctrl+Shift+Z" + }, + "description": "한국어↔중국어 양방향 번역" + } + }, + "web_accessible_resources": [ + { + "resources": [ + "bannedWords.html", + "bannedWords.js", + "sayings.html", + "sayings.js", + "zzim.html", + "zzim.js", + "settings.html", + "settings.js", + "rest-modal.html", + "rest-modal.js", + "manual.html", + "manual.js" + ], + "matches": [""] + } + ] +} diff --git a/wrmc_ext/manual.html b/wrmc_ext/manual.html new file mode 100644 index 0000000..607b0b7 --- /dev/null +++ b/wrmc_ext/manual.html @@ -0,0 +1,649 @@ + + + + + + 내차는언제타냐 통합확장기 매뉴얼 + + + +
+
+

📚 내차는언제타냐 통합확장기

+

매뉴얼 및 기능 가이드

+
+ +
+
+ + + + + + + +
+ +
+

🏠 확장 프로그램 개요

+ +
+ 환영합니다! 내차는언제타냐 통합확장기는 지재권검색, 멀티번역, 시간관리 등 다양한 기능을 제공하는 올인원 도구입니다. +
+ +
+ 주의사항: 지재권검색 및 멀티번역은 API 호출량을 소모합니다. 회원등급에 따라 일일 사용량이 제한됩니다. +
+ +
+
+

🔍 지재권검색 기능

+

키프리스(KIPRIS) 지재권검색을 자동화하여 빠르고 정확한 상표조사를 지원합니다.

+
    +
  • 검색할 단어를 드래그 후 단축키(컨트롤+쉬프트+S) 입력
  • +
  • 또는 우클릭으로 해당 지재권을 Search하고 결과 수집
  • +
  • 현재 활성된 상태의 결과만 필터링
  • +
  • 해당하는 상품군을 바로 확인할 수 있음
  • +
  • 내 금지어목록에 바로 적용 및 편집알바생과 연동
  • +
+
+ +
+

🌐 멀티번역 기능

+

여러 언어로 동시 번역하여 애매한 중국어를 이해할수 있도록 지원합니다.

+
    +
  • 멀티번역: Ctrl+Shift+E
  • +
  • 또는 우클릭으로 해당 문장을 Translate하고 결과 수집
  • +
  • 무료로 구글번역과 메모리 번역지원
  • +
  • 일정등급 이상은 딥러닝 번역 DeepL 사용가능
  • +
  • 챗GPT로 의역 지원가능 (Gemini는 현재 점검 중)
  • +
  • 원클릭으로 많은 엔진의 결과를 확인할수 있음
  • +
+
+ +
+

🌐 원키번역 기능

+

구글 기계번역으로 매우 빠르게 번역할수 있습니다.

+
    +
  • 번역할 문장이나 단어를 드래그 후 단축키(컨트롤+쉬프트+Z) 입력
  • +
  • 웹페이지에서 바로 한글과 중국어를 빠르게 번역
  • +
  • 위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역
  • +
  • 웹페이지에서 중국어를 드래그 후 단축키로 바로 번역 결과 확인
  • +
+
+ +
+

⏰ 시간관리 기능

+

포모도로 기법 기반의 시간관리로 생산성을 극대화합니다.

+
    +
  • 자동 작업/휴식 타이머
  • +
  • 휴식시간 추천 활동
  • +
  • 실시간 타이머 알림
  • +
  • 개인화된 시간 설정
  • +
+
+ +
+

🚫 금지어 관리

+

지재권검색 시 제외할 키워드를 효율적으로 관리합니다.

+
    +
  • 개인별 금지어 목록
  • +
  • 실시간 금지어 추가/삭제
  • +
  • 금지어 등급관리 및 검색결과 확인
  • +
  • 수동금지어 등록 및 파생금지어 등록가능
  • +
+
+
+ +
+ 현재 버전: v1.0.0 | 최종 업데이트: 2025년 6월 +
+
+ +
+

🔍 지재권검색 사용법

+ +
+ 키프리스(KIPRIS)에서 자동으로 지재권검색을 수행하고 결과를 정리해주는 기능입니다. +
+ +
+

1️⃣ 지재권검색 시작하기

+
    +
  • 검색하고 싶은 단어를 드래그 후 우클릭 - 지재권 검색 선택
  • +
  • 검색하고 싶은 단어를 드래그 후 단축키(Search) - Ctrl + Shift + S
  • +
  • 로그인 후 지재권검색 기능 활성화
  • +
+
+ +
+

2️⃣ 검색 과정

+
    +
  • 해당단어를 키프리스에서 검색실행
  • +
  • 검색 결과 유효한 권리상태만 필터링
  • +
  • 지재권 해당하는 상품군 정리
  • +
  • 실시간 진행상황 팝업 표시
  • +
+
+ +
+

3️⃣ 결과 확인 및 추가

+
    +
  • 검색 완료 후 결과 요약 표시
  • +
  • 검색결과를 내 금지어에 원클릭 등록가능
  • +
  • 상표 이미지 및 상세정보 포함
  • +
+
+ + +
+ +
+

🌐 멀티번역 사용법

+ +
+ 하나의 텍스트나 문장을 여러 번역엔진으로 동시에 번역하여 번역기 마다 다른 애매한 내용들을 정리해줍니다. +
+ +
+

1️⃣ 번역 시작하기

+
    +
  • 번역할 텍스트를 드래그 후 우클릭 - 멀티번역 선택
  • +
  • 번역할 텍스트를 드래그 후 단축키(Translate) - Ctrl + Shift + T
  • +
  • 설정에서 선택한 엔진들로 번역
  • +
+
+ +
+

2️⃣ 지원 엔진

+
+
+ 무료 엔진: +
    +
  • 구글 기계번역
  • +
  • myMemory 메모리 번역
  • +
+
+
+ 프리미엄등급 엔진: +
    +
  • 무료엔진
  • +
  • DeepL 딥러닝 번역
  • +
+
+
+
+ VIP등급 엔진 - 의역 지원: +
    +
  • 챗GPT 의역 번역
  • +
  • 구글 제미나이 의역 번역
  • +
+
+
+ +
+

3️⃣ 번역 결과 활용

+
    +
  • 실시간 번역 결과 확인
  • +
  • 자연스러운 딥러닝 엔진인 Deepl 사용
  • +
  • 맥락을 이해하고 현재 단어나 문장이 어떤의미인지 어떨때 쓰이는지 파악
  • +
+
+
+ +
+

⏰ 시간관리 기능

+ +
+ 포모도로 기법을 활용한 자동 시간관리로 생산성을 향상시킵니다. +
+ +
+

1️⃣ 시간관리 설정

+
    +
  • 설정 페이지에서 "시간 알림" 활성화
  • +
  • 작업 시간 설정 (기본: 60분)
  • +
  • 휴식 시간 설정 (기본: 5분)
  • +
  • 알림 방식 선택
  • +
+
+ +
+

2️⃣ 자동 타이머 작동

+
    +
  • 로그인 시 자동으로 작업 타이머 시작
  • +
  • 작업 시간 완료 시 휴식 모달 표시
  • +
  • 휴식 시간 동안 추천 활동 제공
  • +
  • 휴식 시간 동안 타냐대장경 제공
  • +
  • 휴식 완료 후 자동으로 다음 작업 시작
  • +
+
+ +
+

3️⃣ 휴식 시간 추천 활동

+
+
+ 신체 활동: +
    +
  • 가벼운 스트레칭
  • +
  • 목과 어깨 마사지
  • +
  • 잠깐 산책하기
  • +
+
+
+ 정신 건강: +
    +
  • 깊은 호흡 연습
  • +
  • 간단한 명상
  • +
  • 긍정적 생각하기
  • +
+
+
+
+ +
+

4️⃣ 타이머 상태 확인

+
    +
  • 팝업에서 다음 휴식까지 남은 시간 확인
  • +
  • 작업/휴식 사이클 진행상황 표시
  • +
  • 일일 작업 시간 통계
  • +
  • 생산성 향상 팁 제공
  • +
+
+
+ +
+

🚫 금지어 관리

+ +
+ 상품편집시 지식재산권을 피하기 위해금지 키워드를 관리합니다. +
+ +
+

1️⃣ 금지어 추가하기

+
    +
  • 설정 페이지에서 "금지어 관리" 선택
  • +
  • 새로운 금지어 입력
  • +
  • "추가" 버튼 클릭하여 저장
  • +
  • 실시간으로 금지어 목록 업데이트
  • +
+
+ +
+

2️⃣ 금지어 관리

+
    +
  • 기존 금지어 목록 확인
  • +
  • 불필요한 금지어 삭제
  • +
  • 금지어 등급변경
  • +
  • 금지어 검색 및 필터링
  • +
+
+ +
+

3️⃣ 금지어 적용

+
    +
  • 편집알바생과 연동된 금지어 목록
  • +
+
+ +
+ 팁: 자주 나타나는 불필요한 키워드들을 금지어로 등록하면 편집알바생의 효율성이 크게 향상됩니다. +
+ +
+ +
+
+ +
+

💬 타냐대장경

+ +
+ 업무에 도움이 되는 타냐센세의 대장경과 조언을 모아놓은 특별한 기능입니다. +
+ +
+

1️⃣ 타냐대장경이란?

+
    +
  • 업무 효율성을 높이는 대장경 모음
  • +
  • 동기부여와 영감을 주는 메시지
  • +
  • 관리자가 승인한 검증된 조언
  • +
+
+ +
+

2️⃣ 사용 방법

+
    +
  • 팝업에서 "타냐대장경" 버튼 클릭
  • +
  • 오늘의 대장경 확인
  • +
  • 대장경 카테고리별 탐색
  • +
+
+ +
+

3️⃣ 대장경 카테고리

+
+
+ 업무 효율성: +
    +
  • 시간 관리 조언
  • +
  • 생산성 향상 팁
  • +
  • 마켓의 로직변화관리
  • +
  • 타냐센세의 계절별 조언
  • +
+
+
+ 동기부여: +
    +
  • 성공 마인드셋
  • +
  • 도전 정신 격려
  • +
  • 긍정적 사고방식
  • +
+
+
+
+
+ +
+

⚙️ 설정 가이드

+ +
+ 확장 프로그램의 모든 기능을 개인 취향에 맞게 설정할 수 있습니다. +
+ +
+

1️⃣ 번역 설정

+
    +
  • 번역엔진 활성화
  • +
+
+ +
+

2️⃣ 시간관리 설정

+
    +
  • 시간 알림 ON/OFF
  • +
  • 작업 시간 설정 (분 단위)
  • +
  • 휴식 시간 설정 (분 단위)
  • +
  • 알림 방식 선택
  • +
+
+ +
+

3️⃣ 찜 관리 설정

+
    +
  • 추후 업데이트
  • +
  • 찜 품앗이
  • +
+
+
+
+ + + + \ No newline at end of file diff --git a/wrmc_ext/manual.js b/wrmc_ext/manual.js new file mode 100644 index 0000000..48b8a6b --- /dev/null +++ b/wrmc_ext/manual.js @@ -0,0 +1,38 @@ +// 탭 전환 기능 +document.addEventListener('DOMContentLoaded', function() { + // 탭 전환 기능 + document.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', () => { + console.log('탭 클릭됨:', tab.dataset.tab); + + // 모든 탭과 콘텐츠에서 active 클래스 제거 + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + + // 클릭된 탭과 해당 콘텐츠에 active 클래스 추가 + tab.classList.add('active'); + const targetContent = document.getElementById(tab.dataset.tab); + if (targetContent) { + targetContent.classList.add('active'); + console.log('탭 전환 완료:', tab.dataset.tab); + } else { + console.error('탭 콘텐츠를 찾을 수 없습니다:', tab.dataset.tab); + } + }); + }); + + console.log('매뉴얼 탭 기능 초기화 완료'); +}); + +// 필요한 버튼 클릭 이벤트 함수들만 유지 +function openSettings() { + chrome.tabs.create({ url: chrome.runtime.getURL('settings.html') }); +} + +function openBannedWords() { + chrome.tabs.create({ url: chrome.runtime.getURL('bannedWords.html') }); +} + +function openSayings() { + chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') }); +} \ No newline at end of file diff --git a/wrmc_ext/popup.html b/wrmc_ext/popup.html new file mode 100644 index 0000000..5af4375 --- /dev/null +++ b/wrmc_ext/popup.html @@ -0,0 +1,496 @@ + + + + + 내차는언제타냐 통합확장기 + + + + + +

내차는언제타냐 통합확장 로그인

+ + +
+ 초기화 중... +
+ + +
+ +
+ +
가입한 이메일 주소를 입력하세요
+
+
+ +
+ + 👁️ +
영문, 숫자 포함 6자 이상 입력
+
+
+ +
+ + +
+
+ + +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + diff --git a/wrmc_ext/popup.js b/wrmc_ext/popup.js new file mode 100644 index 0000000..36dc0d2 --- /dev/null +++ b/wrmc_ext/popup.js @@ -0,0 +1,1864 @@ +// Chrome Extension 환경에서 안정적으로 동작하도록 수정 +// 백엔드 설정은 background.js에서 중앙 관리됨 + +// 백엔드 설정 가져오기 함수 +async function getBackendConfig() { + try { + console.log('[popup.js] 백엔드 설정 요청 시작'); + + const response = await chrome.runtime.sendMessage({ + action: 'getBackendConfig' + }); + + console.log('[popup.js] 백엔드 설정 응답:', response); + + if (response && response.success && response.config) { + console.log('[popup.js] 백엔드 설정 로드 성공:', response.config); + return response.config; + } else { + throw new Error('백엔드 설정 응답이 올바르지 않습니다'); + } + } catch (error) { + console.error('[popup.js] 백엔드 설정 로드 실패:', error); + console.log('[popup.js] 폴백 설정 사용'); + + // 폴백 설정 + const fallbackConfig = { + SUPABASE_URL: "http://146.56.101.199:8000", + SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }; + + console.log('[popup.js] 폴백 설정:', fallbackConfig); + return fallbackConfig; + } +} + +// 디버그 모드 플래그 (개발 시에만 true로 설정) +// const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김 +const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김 + +// 로그 레벨 정의 +const LOG_LEVELS = { + ERROR: 'ERROR', + WARN: 'WARN', + INFO: 'INFO', + DEBUG: 'DEBUG' +}; + +// 로그 저장 및 관리 시스템 +class Logger { + constructor() { + this.maxLogs = 100; // 최대 저장할 로그 수 + } + + // 로그 저장 + async saveLog(level, message, data = null) { + const timestamp = new Date().toISOString(); + const logEntry = { + timestamp, + level, + message, + data: data ? JSON.stringify(data) : null + }; + + try { + const { loginLogs = [] } = await chrome.storage.local.get('loginLogs'); + loginLogs.unshift(logEntry); // 최신 로그를 앞에 추가 + + // 최대 로그 수 제한 + if (loginLogs.length > this.maxLogs) { + loginLogs.splice(this.maxLogs); + } + + await chrome.storage.local.set({ loginLogs }); + } catch (error) { + console.error('로그 저장 실패:', error); + } + } + + // 로그 조회 + async getLogs() { + try { + const { loginLogs = [] } = await chrome.storage.local.get('loginLogs'); + return loginLogs; + } catch (error) { + console.error('로그 조회 실패:', error); + return []; + } + } + + // 로그 삭제 + async clearLogs() { + try { + await chrome.storage.local.remove('loginLogs'); + return true; + } catch (error) { + console.error('로그 삭제 실패:', error); + return false; + } + } + + // 통합 로그 함수 + async log(level, message, data = null) { + // 콘솔에 출력 + const consoleMessage = `[${level}] ${message}`; + switch (level) { + case LOG_LEVELS.ERROR: + console.error(consoleMessage, data); + break; + case LOG_LEVELS.WARN: + console.warn(consoleMessage, data); + break; + case LOG_LEVELS.INFO: + console.info(consoleMessage, data); + break; + case LOG_LEVELS.DEBUG: + if (DEBUG_MODE) console.log(consoleMessage, data); + break; + } + + // 스토리지에 저장 + await this.saveLog(level, message, data); + + // 디버그 정보 업데이트 + if (DEBUG_MODE) { + updateDebugInfo(`[${level}] ${message}`); + } + } +} + +// 전역 로거 인스턴스 +const logger = new Logger(); + +// 디버그 정보 업데이트 함수 (수정됨) +function updateDebugInfo(message) { + const debugEl = document.getElementById("debug-info"); + if (debugEl) { + if (DEBUG_MODE) { + // 기존 로그 버튼 찾기 + const existingButton = debugEl.querySelector('button'); + + // 메시지만 업데이트 (버튼은 유지) + if (existingButton) { + // 버튼이 있으면 텍스트 노드만 업데이트 + const textNode = debugEl.firstChild; + if (textNode && textNode.nodeType === Node.TEXT_NODE) { + textNode.textContent = message; + } else { + // 텍스트 노드가 없으면 새로 생성 + debugEl.insertBefore(document.createTextNode(message), debugEl.firstChild); + } + } else { + // 버튼이 없으면 전체 내용 설정 + debugEl.textContent = message; + } + + debugEl.style.display = "block"; + console.log('디버그 정보 업데이트:', message); + } else { + debugEl.style.display = "none"; + } + } else { + console.warn('debug-info 요소를 찾을 수 없습니다'); + } +} + +// 로그 표시 함수 +async function showLogs() { + const logs = await logger.getLogs(); + const logWindow = window.open('', '_blank', 'width=800,height=600'); + + const logHtml = ` + + + + 로그인 로그 + + + +

로그인 로그

+
+ + +
+
+ ${logs.map(log => ` +
+
${new Date(log.timestamp).toLocaleString()}
+
[${log.level}]
+
${log.message}
+ ${log.data ? `
${log.data}
` : ''} +
+ `).join('')} +
+ + + + `; + + logWindow.document.write(logHtml); + logWindow.document.close(); +} + +// 이메일 형식 검증 함수 +function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +// Supabase 클라이언트 초기화 (직접 fetch 사용) +async function supabaseAuth(email, password) { + await logger.log(LOG_LEVELS.INFO, '로그인 API 호출 시작', { email }); + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + const url = `${SUPABASE_URL}/auth/v1/token?grant_type=password`; + await logger.log(LOG_LEVELS.DEBUG, '로그인 API URL', { url }); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + }, + body: JSON.stringify({ + email: email, + password: password + }) + }); + + await logger.log(LOG_LEVELS.DEBUG, '로그인 API 응답 상태', { + status: response.status, + statusText: response.statusText, + ok: response.ok, + headers: Object.fromEntries(response.headers.entries()) + }); + + if (!response.ok) { + const error = await response.json(); + await logger.log(LOG_LEVELS.ERROR, '로그인 API 응답 오류', { + status: response.status, + error: error + }); + throw new Error(error.error_description || error.message || '로그인 실패'); + } + + const result = await response.json(); + await logger.log(LOG_LEVELS.INFO, '로그인 API 응답 성공', { + hasAccessToken: !!result.access_token, + tokenLength: result.access_token ? result.access_token.length : 0 + }); + return result; + + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '로그인 API 호출 중 네트워크 오류', { + error: error.message, + name: error.name, + url: url + }); + + // 네트워크 에러 메시지 개선 + if (error.name === 'TypeError' && (error.message.includes('fetch') || error.message.includes('Failed to fetch'))) { + throw new Error(`서버에 연결할 수 없습니다.\n- 인터넷 연결을 확인해주세요\n- 서버가 실행 중인지 확인해주세요\n- 방화벽 설정을 확인해주세요\n\n기술적 세부사항: ${error.message}`); + } + + throw error; + } +} + +// 모달 드래그 기능 +function makeModalDraggable(modal) { + const modalContent = modal.querySelector('.modal-content'); + const modalHeader = modal.querySelector('.modal-header'); + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + modalHeader.addEventListener('mousedown', dragStart); + document.addEventListener('mousemove', drag); + document.addEventListener('mouseup', dragEnd); + + function dragStart(e) { + if (e.target.classList.contains('close')) return; + + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + + if (e.target === modalHeader) { + isDragging = true; + } + } + + function drag(e) { + if (isDragging) { + e.preventDefault(); + + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + + xOffset = currentX; + yOffset = currentY; + + setTranslate(currentX, currentY, modalContent); + } + } + + function dragEnd(e) { + initialX = currentX; + initialY = currentY; + isDragging = false; + } + + function setTranslate(xPos, yPos, el) { + el.style.transform = `translate(${xPos}px, ${yPos}px)`; + } +} + +// 모달 리사이즈 기능 +function makeModalResizable(modal) { + const modalContent = modal.querySelector('.modal-content'); + const resizer = document.createElement('div'); + resizer.className = 'resizer'; + modalContent.appendChild(resizer); + + let isResizing = false; + let originalWidth; + let originalHeight; + let originalX; + let originalY; + + resizer.addEventListener('mousedown', initResize); + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', stopResize); + + function initResize(e) { + isResizing = true; + originalWidth = modalContent.offsetWidth; + originalHeight = modalContent.offsetHeight; + originalX = e.clientX; + originalY = e.clientY; + e.preventDefault(); + } + + function resize(e) { + if (!isResizing) return; + + const width = originalWidth + (e.clientX - originalX); + const height = originalHeight + (e.clientY - originalY); + + if (width > 400) { + modalContent.style.width = width + 'px'; + } + if (height > 300) { + modalContent.style.height = height + 'px'; + } + } + + function stopResize() { + isResizing = false; + } +} + +// 모달 초기화 함수 +function initializeModal(modalId) { + const modal = document.getElementById(modalId); + if (!modal) return; + + makeModalDraggable(modal); + makeModalResizable(modal); + + // 모달 외부 클릭 시 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.display = 'none'; + } + }); + + // 닫기 버튼 + const closeBtn = modal.querySelector('.close'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + modal.style.display = 'none'; + }); + } +} + +// JWT 토큰 유효성 검증 함수 추가 +async function validateToken(token) { + if (!token) { + await logger.log(LOG_LEVELS.DEBUG, '토큰이 없음'); + return false; + } + + try { + // JWT 토큰 만료 시간 확인 (클라이언트 사이드) + const payload = JSON.parse(atob(token.split('.')[1])); + const currentTime = Math.floor(Date.now() / 1000); + + if (payload.exp && payload.exp < currentTime) { + await logger.log(LOG_LEVELS.WARN, '토큰이 만료됨', { + exp: payload.exp, + currentTime, + expired: true + }); + return false; + } + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + // 서버에서 토큰 유효성 검증 + const authUrl = `${SUPABASE_URL}/auth/v1/user`; + const authRes = await fetch(authUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (authRes.ok) { + await logger.log(LOG_LEVELS.INFO, '토큰 유효성 검증 성공'); + return true; + } else { + await logger.log(LOG_LEVELS.WARN, '토큰 유효성 검증 실패', { + status: authRes.status, + statusText: authRes.statusText + }); + return false; + } + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '토큰 유효성 검증 중 오류', { + error: error.message + }); + return false; + } +} + +// 자동 로그인 시도 함수 추가 +async function attemptAutoLogin() { + updateDebugInfo('🔄 자동 로그인 확인 중...'); + + try { + // 1. 기존 토큰 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + + if (access_token) { + // 저장된 사용자 정보가 있으면 먼저 표시 (빠른 UI 응답) + const savedUserInfo = await chrome.storage.local.get([ + 'user_email', 'user_membership_level', 'user_api_limit', + 'user_current_api_calls', 'user_expire_date' + ]); + + if (savedUserInfo.user_email) { + updateDebugInfo('⚡ 저장된 정보로 빠른 표시 중...'); + await logger.log(LOG_LEVELS.INFO, '저장된 사용자 정보로 빠른 UI 표시'); + + // UI 즉시 업데이트 + document.getElementById("login-section").style.display = "none"; + document.getElementById("user-info-section").style.display = "block"; + document.getElementById("user-email").textContent = savedUserInfo.user_email; + document.getElementById("user-level").textContent = savedUserInfo.user_membership_level || "기본"; + + // 사용량 표시 + const currentCalls = savedUserInfo.user_current_api_calls || 0; + let usageText = currentCalls.toString(); + if (savedUserInfo.user_api_limit) { + usageText = `${currentCalls}/${savedUserInfo.user_api_limit}`; + } + document.getElementById("user-usage").textContent = usageText; + + // 만료일 표시 + document.getElementById("user-expire").textContent = savedUserInfo.user_expire_date || "없음"; + + // 타이머 동기화 + await handleTimerInitOrSync(false); + } + + // 백그라운드에서 토큰 유효성 검증 + updateDebugInfo('🔍 백그라운드에서 토큰 검증 중...'); + await logger.log(LOG_LEVELS.INFO, '백그라운드에서 토큰 유효성 검증 시작'); + + const isValidToken = await validateToken(access_token); + + if (isValidToken) { + // 토큰이 유효하면 바로 사용자 정보 로드 + updateDebugInfo('✅ 유효한 토큰으로 자동 로그인 중...'); + await logger.log(LOG_LEVELS.INFO, '유효한 토큰으로 자동 로그인 성공'); + await loadUserInfo(access_token); + + // 기존 토큰으로 자동 로그인이므로 타이머 동기화만 수행 + await handleTimerInitOrSync(false); + return true; + } else { + // 토큰이 유효하지 않으면 제거 + updateDebugInfo('🗑️ 만료된 토큰 제거 중...'); + await logger.log(LOG_LEVELS.WARN, '만료된 토큰 제거'); + await chrome.storage.local.remove("access_token"); + } + } + + // 2. 자동 로그인 설정 확인 + const { autoLogin, savedEmail, savedPassword } = await chrome.storage.local.get([ + "autoLogin", "savedEmail", "savedPassword" + ]); + + if (autoLogin && savedEmail && savedPassword) { + updateDebugInfo('🔄 저장된 계정으로 자동 로그인 시도 중...'); + await logger.log(LOG_LEVELS.INFO, '저장된 계정으로 자동 로그인 시도'); + + // 저장된 정보로 자동 로그인 (UI 필드는 채우지 않음) + try { + updateDebugInfo('🔐 자동 로그인 진행 중...'); + const authResult = await supabaseAuth(savedEmail, savedPassword); + + if (authResult.access_token) { + const token = authResult.access_token; + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + await chrome.storage.local.set({ + access_token: token, + SUPABASE_URL, + SUPABASE_ANON_KEY + }); + + updateDebugInfo('✅ 자동 로그인 성공! 사용자 정보 로드 중...'); + await loadUserInfo(token); + await handleTimerInitOrSync(true); + + return true; + } + } catch (autoLoginError) { + updateDebugInfo(`❌ 자동 로그인 실패: ${autoLoginError.message}`); + await logger.log(LOG_LEVELS.ERROR, '자동 로그인 실패', { error: autoLoginError.message }); + + // 자동 로그인 실패 시 저장된 정보를 UI에 표시만 하고 사용자가 직접 로그인하도록 안내 + document.getElementById("email").value = savedEmail; + document.getElementById("password").value = savedPassword; + document.getElementById("save-login").checked = true; + document.getElementById("auto-login").checked = true; + } + } + + // 3. 자동 로그인이 불가능한 경우 + updateDebugInfo('✅ 초기화 완료 - 로그인이 필요합니다'); + await logger.log(LOG_LEVELS.DEBUG, '자동 로그인 불가 - 수동 로그인이 필요합니다'); + return false; + + } catch (error) { + updateDebugInfo(`❌ 자동 로그인 확인 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '자동 로그인 확인 중 오류', { error: error.message }); + return false; + } +} + +// DOM 로드 완료 후 실행 +document.addEventListener('DOMContentLoaded', async function() { + // 즉시 디버그 정보 표시 + updateDebugInfo('DOM 로드 완료, 초기화 시작...'); + + await logger.log(LOG_LEVELS.INFO, 'DOM 로드 완료, 초기화 시작'); + + // 디버그 정보 표시/숨김 처리 + const debugEl = document.getElementById("debug-info"); + if (debugEl && DEBUG_MODE) { + debugEl.style.display = "block"; + + // 로그 보기 버튼 생성 및 추가 + if (!debugEl.querySelector('button')) { + const logButton = document.createElement('button'); + logButton.textContent = '📋 로그 보기'; + logButton.onclick = showLogs; + + // 줄바꿈 추가 후 버튼 추가 + debugEl.appendChild(document.createElement('br')); + debugEl.appendChild(logButton); + + console.log('로그 보기 버튼 추가됨'); + } + } + + updateDebugInfo('요소 존재 확인 중...'); + + try { + // 요소 존재 확인 + const requiredElements = ['email', 'password', 'login-btn', 'password-toggle']; + const missingElements = []; + + for (const elementId of requiredElements) { + if (!document.getElementById(elementId)) { + missingElements.push(elementId); + } + } + + if (missingElements.length > 0) { + updateDebugInfo(`❌ 필수 요소 누락: ${missingElements.join(', ')}`); + await logger.log(LOG_LEVELS.ERROR, '필수 요소 누락', { missingElements }); + return; + } + + updateDebugInfo('✅ 모든 필수 요소 확인됨'); + await logger.log(LOG_LEVELS.DEBUG, '모든 필수 요소 확인됨'); + + // 저장된 로그인 정보 로드 (UI 표시용) + try { + const saved = await chrome.storage.local.get(["savedEmail", "savedPassword", "autoLogin"]); + await logger.log(LOG_LEVELS.DEBUG, '저장된 설정 로드 완료', { + hasSavedEmail: !!saved.savedEmail, + autoLogin: saved.autoLogin + }); + + if (saved.savedEmail) document.getElementById("email").value = saved.savedEmail; + if (saved.savedPassword) document.getElementById("password").value = saved.savedPassword; + document.getElementById("save-login").checked = !!saved.savedEmail; + document.getElementById("auto-login").checked = !!saved.autoLogin; + + } catch (error) { + updateDebugInfo(`❌ 설정 로드 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '저장된 설정 로드 실패', { error: error.message }); + } + + // 🔐 비밀번호 보기 (눌렀을 때만 보이기) + const passwordToggle = document.getElementById("password-toggle"); + const passwordInput = document.getElementById("password"); + + if (passwordToggle && passwordInput) { + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 토글 이벤트 등록'); + + // 마우스 이벤트 + passwordToggle.addEventListener("mousedown", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 보기 - mousedown'); + passwordInput.type = "text"; + }); + + passwordToggle.addEventListener("mouseup", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 숨기기 - mouseup'); + passwordInput.type = "password"; + }); + + passwordToggle.addEventListener("mouseleave", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 숨기기 - mouseleave'); + passwordInput.type = "password"; + }); + + // 터치 이벤트 + passwordToggle.addEventListener("touchstart", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 보기 - touchstart'); + passwordInput.type = "text"; + }); + + passwordToggle.addEventListener("touchend", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 숨기기 - touchend'); + passwordInput.type = "password"; + }); + } + + // 이메일 입력 필드 이벤트 + const emailInput = document.getElementById("email"); + if (emailInput) { + await logger.log(LOG_LEVELS.DEBUG, '이메일 필드 이벤트 등록'); + + emailInput.addEventListener("keypress", async (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const password = document.getElementById("password").value.trim(); + + if (password) { + // 비밀번호가 있으면 로그인 시도 + await logger.log(LOG_LEVELS.INFO, '이메일 필드에서 Enter - 로그인 시도'); + doLogin(); + } else { + // 비밀번호가 없으면 비밀번호 필드로 포커스 이동 + await logger.log(LOG_LEVELS.DEBUG, '이메일 필드에서 Enter - 비밀번호 필드로 이동'); + document.getElementById("password").focus(); + } + } + }); + } + + // 비밀번호 입력 필드 이벤트 + if (passwordInput) { + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 필드 이벤트 등록'); + + passwordInput.addEventListener("keypress", async (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '비밀번호 필드에서 Enter - 로그인 시도'); + doLogin(); + } + }); + } + +// 로그인 버튼 + const loginBtn = document.getElementById("login-btn"); + if (loginBtn) { + await logger.log(LOG_LEVELS.DEBUG, '로그인 버튼 이벤트 등록'); + + loginBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '로그인 버튼 클릭됨'); + doLogin(); + }); + } + +// 로그아웃 버튼 + const logoutBtn = document.getElementById("logout-btn"); + if (logoutBtn) { + await logger.log(LOG_LEVELS.DEBUG, '로그아웃 버튼 이벤트 등록'); + + logoutBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '로그아웃 버튼 클릭됨'); + + // 토큰과 자동로그인 설정 제거 + await chrome.storage.local.remove(["access_token", "autoLogin", "isTimerInitialized"]); + await logger.log(LOG_LEVELS.INFO, '로그아웃 완료 - 토큰, 자동로그인 설정 및 타이머 초기화 상태 제거'); + + location.reload(); + }); + } + // 금지어 관리 버튼 + const bannedWordsBtn = document.getElementById("banned-words-btn"); + if (bannedWordsBtn) { + await logger.log(LOG_LEVELS.DEBUG, '금지어 관리 버튼 이벤트 등록'); + + bannedWordsBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '금지어 관리 버튼 클릭됨'); + + try { + // 현재 로그인된 토큰 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + alert('로그인이 필요합니다.'); + return; + } + + // 토큰 유효성 재확인 + const isValid = await validateToken(access_token); + if (!isValid) { + alert('세션이 만료되었습니다. 다시 로그인해주세요.'); + await chrome.storage.local.remove("access_token"); + location.reload(); + return; + } + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + // chrome.storage에 설정값 저장 (새 창에서 사용할 수 있도록) + await chrome.storage.local.set({ + 'bannedWords_config': { + SUPABASE_URL: SUPABASE_URL, + SUPABASE_ANON_KEY: SUPABASE_ANON_KEY, + DEBUG_MODE: DEBUG_MODE, + ACCESS_TOKEN: access_token, + timestamp: Date.now() + } + }); + + await logger.log(LOG_LEVELS.INFO, '금지어 관리 창 설정 저장 완료', { + hasUrl: !!SUPABASE_URL, + hasKey: !!SUPABASE_ANON_KEY, + hasToken: !!access_token, + debugMode: DEBUG_MODE + }); + + // 크롬 익스텐션 리소스 URL로 새 창 열기 + const url = chrome.runtime.getURL('bannedWords.html'); + const newWindow = window.open(url, '_blank', 'width=1000,height=700'); + + if (!newWindow) { + throw new Error('새 창을 열 수 없습니다. 팝업 차단을 확인해주세요.'); + } + + await logger.log(LOG_LEVELS.INFO, '금지어 관리 창 열기 완료'); + + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '금지어 관리 창 열기 실패', { error: error.message }); + alert('금지어 관리 창을 열 수 없습니다: ' + error.message); + } + }); + } + + // 설정 버튼 + const settingsBtn = document.getElementById("settings-btn"); + if (settingsBtn) { + await logger.log(LOG_LEVELS.DEBUG, '설정 버튼 이벤트 등록'); + + settingsBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '설정 버튼 클릭됨'); + + try { + // 현재 로그인된 토큰 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + alert('로그인이 필요합니다.'); + return; + } + + // 토큰 유효성 재확인 + const isValid = await validateToken(access_token); + if (!isValid) { + alert('세션이 만료되었습니다. 다시 로그인해주세요.'); + await chrome.storage.local.remove("access_token"); + location.reload(); + return; + } + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + // chrome.storage에 설정값 저장 (새 창에서 사용할 수 있도록) + await chrome.storage.local.set({ + 'settings_config': { + SUPABASE_URL: SUPABASE_URL, + SUPABASE_ANON_KEY: SUPABASE_ANON_KEY, + DEBUG_MODE: DEBUG_MODE, + ACCESS_TOKEN: access_token, + timestamp: Date.now() + } + }); + + await logger.log(LOG_LEVELS.INFO, '설정 창 설정 저장 완료', { + hasUrl: !!SUPABASE_URL, + hasKey: !!SUPABASE_ANON_KEY, + hasToken: !!access_token, + debugMode: DEBUG_MODE + }); + + // 새 창에서 설정 페이지 열기 + chrome.windows.create({ + url: chrome.runtime.getURL('settings.html'), + type: 'popup', + width: 900, + height: 900 + }); + + await logger.log(LOG_LEVELS.INFO, '설정 창 열기 완료'); + + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '설정 창 열기 실패', { error: error.message }); + alert('설정 창을 열 수 없습니다: ' + error.message); + } + }); + } + + // 타냐대장경보기 버튼 클릭 이벤트 + document.getElementById('sayings-btn').addEventListener('click', async () => { + console.log('타냐대장경보기 버튼 클릭됨'); + + try { + // 현재 로그인된 토큰 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + alert('로그인이 필요합니다.'); + return; + } + + // 토큰 유효성 재확인 + const isValid = await validateToken(access_token); + if (!isValid) { + alert('세션이 만료되었습니다. 다시 로그인해주세요.'); + await chrome.storage.local.remove("access_token"); + location.reload(); + return; + } + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + // chrome.storage에 설정값 저장 (금지어 관리와 동일한 설정 사용) + const settingsToSave = { + SUPABASE_URL: SUPABASE_URL, + SUPABASE_ANON_KEY: SUPABASE_ANON_KEY, + DEBUG_MODE: DEBUG_MODE, + ACCESS_TOKEN: access_token, + timestamp: Date.now() + }; + + console.log('타냐대장경보기용 설정을 chrome.storage에 저장 중...', { + url: !!settingsToSave.SUPABASE_URL, + key: !!settingsToSave.SUPABASE_ANON_KEY, + token: !!settingsToSave.ACCESS_TOKEN, + debug: settingsToSave.DEBUG_MODE + }); + + await chrome.storage.local.set({ + 'sayings_config': settingsToSave + }); + + console.log('타냐대장경보기 설정 저장 완료, 새 창 열기'); + + // 새 창에서 타냐대장경보기 페이지 열기 + chrome.windows.create({ + url: chrome.runtime.getURL('sayings.html'), + type: 'popup', + width: 1200, + height: 800 + }); + + } catch (error) { + console.error('타냐대장경보기 창 열기 실패:', error); + alert('타냐대장경보기 창을 열 수 없습니다: ' + error.message); + } + }); + + // 찜관리 버튼 클릭 이벤트 + document.getElementById('zzim-btn').addEventListener('click', async () => { + console.log('찜관리 버튼 클릭됨'); + + try { + // 현재 로그인된 토큰 확인 + const { access_token, user_id } = await chrome.storage.local.get(["access_token", "user_id"]); + if (!access_token) { + alert('로그인이 필요합니다.'); + return; + } + + // 토큰 유효성 재확인 + const isValid = await validateToken(access_token); + if (!isValid) { + alert('세션이 만료되었습니다. 다시 로그인해주세요.'); + await chrome.storage.local.remove("access_token"); + location.reload(); + return; + } + + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + // chrome.storage에 설정값 저장 (새 창에서 사용할 수 있도록) + await chrome.storage.local.set({ + 'zzim_config': { + SUPABASE_URL: SUPABASE_URL, + SUPABASE_ANON_KEY: SUPABASE_ANON_KEY, + DEBUG_MODE: DEBUG_MODE, + ACCESS_TOKEN: access_token, + USER_ID: user_id, + timestamp: Date.now() + } + }); + + await logger.log(LOG_LEVELS.INFO, '찜관리 창 설정 저장 완료', { + hasUrl: !!SUPABASE_URL, + hasKey: !!SUPABASE_ANON_KEY, + hasToken: !!access_token, + hasUserId: !!user_id, + debugMode: DEBUG_MODE + }); + + // 새 창에서 찜관리 페이지 열기 + chrome.windows.create({ + url: chrome.runtime.getURL('zzim.html'), + type: 'popup', + width: 1200, + height: 800 + }); + + } catch (error) { + console.error('찜관리 창 열기 실패:', error); + alert('찜관리 창을 열 수 없습니다: ' + error.message); + } + }); + + // 모달 초기화 + initializeModal('banned-words-modal'); + initializeModal('kipris-modal'); + initializeModal('sayings-modal'); + + // 🚀 자동 로그인 시도 (가장 중요한 부분) + updateDebugInfo('🚀 자동 로그인 시도 중...'); + const autoLoginSuccess = await attemptAutoLogin(); + + if (!autoLoginSuccess) { + updateDebugInfo('✅ 초기화 완료 - 수동 로그인이 필요합니다'); + } + + // API 키 로딩 + await loadApiKeysFromFile(); + + // 설정 변경 감지 시작 + setupStorageListener(); + watchSettingsChanges(); + console.log('설정 변경 감지 시스템 활성화됨'); + + await logger.log(LOG_LEVELS.INFO, '초기화 완료'); + + // 타이머 초기화 또는 동기화는 로그인 시점에서 처리됨 (중복 호출 방지) + + } catch (error) { + updateDebugInfo(`❌ 초기화 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '초기화 중 치명적 오류', { error: error.message }); + console.error('초기화 오류:', error); + } +}); + +// 로컬 파일에서 API 키 로드 +async function loadApiKeysFromFile() { + try { + await logger.log(LOG_LEVELS.INFO, 'API 키 로딩 시작'); + + // 로컬 파일에서 API 키 읽기 + const response = await fetch(chrome.runtime.getURL('api-keys.json')); + + if (!response.ok) { + throw new Error(`API 키 파일 로드 실패: ${response.status}`); + } + + const apiKeys = await response.json(); + await logger.log(LOG_LEVELS.DEBUG, 'API 키 파일 읽기 완료', { keys: Object.keys(apiKeys) }); + + // Chrome storage에 API 키 저장 (background.js에서 사용할 수 있도록) + // 각 서비스별로 정확한 구조로 저장 + const storageData = {}; + + if (apiKeys.papago && apiKeys.papago.clientId && apiKeys.papago.clientSecret) { + storageData.papago_api_key = { + clientId: apiKeys.papago.clientId, + clientSecret: apiKeys.papago.clientSecret + }; + await logger.log(LOG_LEVELS.DEBUG, 'Papago API 키 준비 완료'); + } else { + await logger.log(LOG_LEVELS.WARN, 'Papago API 키가 누락되었습니다'); + } + + if (apiKeys.deepl && apiKeys.deepl.authKey) { + storageData.deepl_api_key = { + authKey: apiKeys.deepl.authKey + }; + await logger.log(LOG_LEVELS.DEBUG, 'DeepL API 키 준비 완료'); + } else { + await logger.log(LOG_LEVELS.WARN, 'DeepL API 키가 누락되었습니다'); + } + + if (apiKeys.openai && apiKeys.openai.apiKey) { + storageData.openai_api_key = { + apiKey: apiKeys.openai.apiKey + }; + await logger.log(LOG_LEVELS.DEBUG, 'OpenAI API 키 준비 완료'); + } else { + await logger.log(LOG_LEVELS.WARN, 'OpenAI API 키가 누락되었습니다'); + } + + if (apiKeys.gemini && apiKeys.gemini.apiKey) { + storageData.gemini_api_key = { + apiKey: apiKeys.gemini.apiKey + }; + await logger.log(LOG_LEVELS.DEBUG, 'Gemini API 키 준비 완료'); + } else { + await logger.log(LOG_LEVELS.WARN, 'Gemini API 키가 누락되었습니다'); + } + + // Chrome storage에 저장 + await chrome.storage.local.set(storageData); + + // 저장 확인 + const saved = await chrome.storage.local.get(Object.keys(storageData)); + await logger.log(LOG_LEVELS.INFO, 'API 키 Chrome storage 저장 완료', { + savedKeys: Object.keys(saved), + totalKeys: Object.keys(storageData).length + }); + + console.log('✅ API 키 로드 및 저장 완료:', Object.keys(storageData)); + + } catch (error) { + console.error('❌ API 키 파일 로드 오류:', error); + await logger.log(LOG_LEVELS.ERROR, 'API 키 파일 로드 실패', { + error: error.message, + stack: error.stack + }); + + // 사용자에게 알림 + if (DEBUG_MODE) { + updateDebugInfo(`❌ API 키 로드 실패: ${error.message}`); + } + } +} + +// 로그인 함수 +async function doLogin() { + updateDebugInfo('🔄 로그인 프로세스 시작...'); + await logger.log(LOG_LEVELS.INFO, '로그인 프로세스 시작'); + + try { + const email = document.getElementById("email").value.trim(); + const password = document.getElementById("password").value.trim(); + + await logger.log(LOG_LEVELS.DEBUG, '로그인 입력값 검증 시작', { email }); + + // 오류 메시지 초기화 + document.getElementById("email-error").textContent = ""; + document.getElementById("password-error").textContent = ""; + document.getElementById("status").textContent = ""; + document.getElementById("membership-level").textContent = ""; + + // 유효성 검사 + let hasError = false; + + // 이메일 형식 검증 강화 + if (!email) { + document.getElementById("email-error").textContent = "이메일을 입력하세요."; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '이메일 입력 누락'); + } else if (!isValidEmail(email)) { + document.getElementById("email-error").textContent = "올바른 이메일 형식을 입력하세요. (예: user@example.com)"; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '잘못된 이메일 형식', { email }); + } + + // 비밀번호 검증 + if (!password) { + document.getElementById("password-error").textContent = "비밀번호를 입력하세요."; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '비밀번호 입력 누락'); + } else if (password.length < 6) { + document.getElementById("password-error").textContent = "비밀번호는 6자 이상이어야 합니다."; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '비밀번호 길이 부족', { length: password.length }); + } + + if (hasError) { + updateDebugInfo('❌ 입력 유효성 검사 실패'); + await logger.log(LOG_LEVELS.WARN, '입력 유효성 검사 실패'); + return; + } + + // UI 상태 변경 - 로딩 표시 + document.getElementById("loading").style.display = "block"; + document.getElementById("login-btn").disabled = true; + document.getElementById("login-btn").textContent = "로그인 중..."; + document.getElementById("status").textContent = "🔄 로그인 중입니다..."; + updateDebugInfo('🔄 로그인 API 호출 중...'); + await logger.log(LOG_LEVELS.INFO, '로그인 요청 전송 중...'); + + try { + // 직접 fetch를 사용한 로그인 + const authResult = await supabaseAuth(email, password); + + if (authResult.access_token) { + updateDebugInfo('✅ 로그인 성공! 사용자 정보 로드 중...'); + await logger.log(LOG_LEVELS.INFO, '로그인 성공, 토큰 저장'); + + const token = authResult.access_token; + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + await chrome.storage.local.set({ + access_token: token, + SUPABASE_URL, + SUPABASE_ANON_KEY + }); + + // 로그인 정보 저장 처리 + if (document.getElementById("save-login").checked) { + await chrome.storage.local.set({ + savedEmail: email, + savedPassword: password + }); + await logger.log(LOG_LEVELS.INFO, '로그인 정보 저장됨'); + } else { + await chrome.storage.local.remove(["savedEmail", "savedPassword"]); + } + + // 자동 로그인 설정 처리 + if (document.getElementById("auto-login").checked) { + await chrome.storage.local.set({ autoLogin: true }); + await logger.log(LOG_LEVELS.INFO, '자동 로그인 설정됨'); + } else { + await chrome.storage.local.remove("autoLogin"); + } + + document.getElementById("status").textContent = "✅ 로그인 성공!"; + await logger.log(LOG_LEVELS.INFO, '사용자 정보 로드 시작'); + await loadUserInfo(token); + + // 새로운 로그인이므로 타이머 초기화 + await handleTimerInitOrSync(true); + } else { + throw new Error('토큰을 받지 못했습니다'); + } + } catch (authError) { + updateDebugInfo(`❌ 로그인 실패: ${authError.message}`); + await logger.log(LOG_LEVELS.ERROR, '로그인 실패', { error: authError.message }); + document.getElementById("status").textContent = "❌ 로그인 실패: " + authError.message; + + // 입력 필드에 포커스 + if (authError.message.includes('email') || authError.message.includes('이메일')) { + document.getElementById("email").focus(); + } else { + document.getElementById("password").focus(); + } + } + } catch (err) { + updateDebugInfo(`❌ 로그인 오류: ${err.message}`); + await logger.log(LOG_LEVELS.ERROR, '로그인 중 예외', { error: err.message }); + document.getElementById("status").textContent = "❌ 알 수 없는 오류: " + err.message; + console.error('로그인 오류:', err); + } finally { + // UI 상태 복원 + document.getElementById("loading").style.display = "none"; + document.getElementById("login-btn").disabled = false; + document.getElementById("login-btn").textContent = "로그인"; + await logger.log(LOG_LEVELS.DEBUG, '로그인 프로세스 종료'); + } +} + +// 사용자 정보 로드 함수 +async function loadUserInfo(token) { + updateDebugInfo('🔄 사용자 정보 로드 중...'); + await logger.log(LOG_LEVELS.INFO, '사용자 정보 요청 중...'); + + try { + // 백엔드 설정 가져오기 + const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); + + // 먼저 Auth API로 사용자 기본 정보 가져오기 + const authUrl = `${SUPABASE_URL}/auth/v1/user`; + await logger.log(LOG_LEVELS.DEBUG, '사용자 Auth 정보 API 호출', { authUrl }); + + const authRes = await fetch(authUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!authRes.ok) { + // Auth API 응답 오류 처리 + let authErrorDetails; + try { + authErrorDetails = await authRes.json(); + } catch { + authErrorDetails = await authRes.text(); + } + + await logger.log(LOG_LEVELS.ERROR, 'Auth API 응답 오류', { + status: authRes.status, + statusText: authRes.statusText, + authErrorDetails + }); + + throw new Error(`Auth API 오류 - HTTP ${authRes.status}: ${authRes.statusText}`); + } + + const authUser = await authRes.json(); + await logger.log(LOG_LEVELS.DEBUG, 'Auth API 응답 데이터', { authUser }); + + // 사용자 상세 정보와 membership_levels 조인해서 가져오기 + let userDetails = null; + try { + // membership_levels 테이블과 조인하여 api_call_limit도 함께 가져오기 + const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*,membership_levels(level,api_call_limit)&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + await logger.log(LOG_LEVELS.DEBUG, '사용자 상세 정보 API 호출 (조인)', { detailsUrl }); + + const detailsRes = await fetch(detailsUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (detailsRes.ok) { + const detailsData = await detailsRes.json(); + await logger.log(LOG_LEVELS.DEBUG, '사용자 상세 정보 응답 (조인)', { detailsData }); + userDetails = detailsData[0]; + } else { + // 조인이 실패하면 기본 users 테이블만 조회 + await logger.log(LOG_LEVELS.WARN, '조인 쿼리 실패, 기본 사용자 정보만 조회', { + status: detailsRes.status + }); + + const fallbackUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + const fallbackRes = await fetch(fallbackUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (fallbackRes.ok) { + const fallbackData = await fallbackRes.json(); + await logger.log(LOG_LEVELS.DEBUG, '기본 사용자 정보 응답', { fallbackData }); + userDetails = fallbackData[0]; + } + } + } catch (detailsError) { + await logger.log(LOG_LEVELS.WARN, '사용자 상세 정보 조회 중 오류', { + error: detailsError.message + }); + } + + // UI 업데이트 + updateDebugInfo('✅ 사용자 정보 로드 완료'); + await logger.log(LOG_LEVELS.INFO, '사용자 정보 로드 성공', { + email: authUser.email, + hasDetails: !!userDetails, + membershipLevel: userDetails?.membership_level, + membershipLevels: userDetails?.membership_levels + }); + + // 사용자 ID를 storage에 저장 (금지어 추가 시 사용) + if (userDetails && userDetails.id) { + // API 한도 정보도 함께 저장 + let apiLimit = null; + if (userDetails.membership_levels) { + apiLimit = userDetails.membership_levels.api_call_limit; + } + + await chrome.storage.local.set({ + user_id: userDetails.id, + user_email: authUser.email, + user_membership_level: userDetails.membership_level, + user_api_limit: apiLimit, + user_current_api_calls: userDetails.current_api_calls || 0, + // 백엔드 설정도 저장 (다른 페이지에서 사용할 수 있도록) + SUPABASE_URL, + SUPABASE_ANON_KEY + }); + + await logger.log(LOG_LEVELS.INFO, '사용자 정보 저장 완료', { + user_id: userDetails.id, + email: authUser.email, + membership_level: userDetails.membership_level, + api_limit: apiLimit, + current_api_calls: userDetails.current_api_calls || 0 + }); + } + + document.getElementById("login-section").style.display = "none"; + document.getElementById("user-info-section").style.display = "block"; + document.getElementById("user-email").textContent = authUser.email; + + // 상세 정보 표시 + if (userDetails) { + // 회원등급 표시 (membership_levels 조인 데이터 또는 membership_level 필드) + let levelText = "기본"; + let apiLimit = null; + + if (userDetails.membership_levels) { + // 조인된 데이터가 있는 경우 + levelText = userDetails.membership_levels.level || "기본"; + apiLimit = userDetails.membership_levels.api_call_limit; + } else if (userDetails.membership_level) { + // 직접 필드가 있는 경우 + levelText = userDetails.membership_level; + } + + document.getElementById("user-level").textContent = levelText; + + // Chrome Storage에서 최신 호출량 정보 가져오기 (실시간 반영) + const { user_current_api_calls } = await chrome.storage.local.get(['user_current_api_calls']); + const currentCalls = user_current_api_calls || userDetails.current_api_calls || 0; + + // 오늘 호출량 표시 (current_api_calls/api_call_limit 형태) + let usageText = currentCalls.toString(); + + if (apiLimit !== null && apiLimit !== undefined) { + usageText = `${currentCalls}/${apiLimit}`; + } + + document.getElementById("user-usage").textContent = usageText; + + // 만료일 표시 + document.getElementById("user-expire").textContent = userDetails.payment_period_end + ? new Date(userDetails.payment_period_end).toLocaleDateString() + : "없음"; + + await logger.log(LOG_LEVELS.INFO, '사용자 정보 UI 업데이트 완료', { + level: levelText, + usage: usageText, + apiLimit: apiLimit, + currentCallsFromStorage: user_current_api_calls, + currentCallsFromDB: userDetails.current_api_calls + }); + } else { + // 상세 정보가 없는 경우 기본값 + document.getElementById("user-level").textContent = "기본"; + document.getElementById("user-usage").textContent = "0"; + document.getElementById("user-expire").textContent = "없음"; + + await logger.log(LOG_LEVELS.WARN, '사용자 상세 정보 없음, 기본값 사용'); + } + + // 타이머 초기화는 호출하는 쪽에서 별도로 처리됨 + + } catch (error) { + updateDebugInfo(`❌ 사용자 정보 로드 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '사용자 정보 로드 실패', { error: error.message }); + document.getElementById("status").textContent = "⚠️ 사용자 정보 로드 실패: " + error.message; + console.error('사용자 정보 로드 오류:', error); + } +} + +// 카운트다운 타이머 관련 변수 +let countdownInterval = null; +let settingsWatcher = null; // 설정 변경 감지용 +let isCountdownInitializing = false; // 중복 초기화 방지 + +// 타이머 초기화 상태를 Chrome Storage에서 관리하는 함수들 +async function getTimerInitializedStatus() { + try { + const result = await chrome.storage.local.get('isTimerInitialized'); + return result.isTimerInitialized || false; + } catch (error) { + console.error('타이머 초기화 상태 확인 실패:', error); + return false; + } +} + +async function setTimerInitializedStatus(status) { + try { + await chrome.storage.local.set({ isTimerInitialized: status }); + console.log('타이머 초기화 상태 저장:', status); + } catch (error) { + console.error('타이머 초기화 상태 저장 실패:', error); + } +} + +// 타이머 초기화 또는 동기화 함수 (loadUserInfo에서 분리) +async function handleTimerInitOrSync(isFirstLogin = false) { + try { + const isTimerInitialized = await getTimerInitializedStatus(); + + if (!isTimerInitialized || isFirstLogin) { + console.log('타이머 초기화 수행:', { isFirstLogin, isTimerInitialized }); + await setTimerInitializedStatus(true); + + // 잠시 대기 후 초기화 + setTimeout(() => { + if (!isCountdownInitializing) { + initBreakCountdown(); + } + }, 500); + + // Background 타이머 시작 요청 + try { + await logger.log(LOG_LEVELS.INFO, 'Background 타이머 시작 요청'); + const timerStartResponse = await chrome.runtime.sendMessage({ + action: 'startTimerAfterLogin' + }); + + if (timerStartResponse && timerStartResponse.success) { + await logger.log(LOG_LEVELS.INFO, 'Background 타이머 시작 성공'); + } else { + await logger.log(LOG_LEVELS.WARN, 'Background 타이머 시작 실패', { + response: timerStartResponse + }); + } + } catch (timerError) { + await logger.log(LOG_LEVELS.ERROR, 'Background 타이머 시작 요청 중 오류', { + error: timerError.message + }); + } + + } else { + console.log('이미 초기화됨 - 타이머 동기화만 수행'); + // 동기화만 수행 (리셋하지 않음) + if (!isCountdownInitializing) { + // Background 타이머 상태만 확인하고 UI 업데이트 + try { + const response = await chrome.runtime.sendMessage({ + action: 'getTimerStatus' + }); + + if (response && response.success && response.isRunning) { + const remainingMs = response.remainingTime; + console.log('Background 타이머 동기화 (UI만 업데이트):', { + 남은시간_분: Math.floor(remainingMs / (1000 * 60)), + 남은시간_초: Math.floor((remainingMs % (1000 * 60)) / 1000) + }); + + // 기존 카운트다운이 있으면 정리하고 새로 시작 + if (countdownInterval) { + clearInterval(countdownInterval); + } + + const nextBreakTime = new Date(Date.now() + remainingMs); + startCountdown(nextBreakTime); + + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + countdownEl.style.color = '#1e7e1e'; + } + } + } catch (error) { + console.error('타이머 동기화 실패:', error); + } + } + } + } catch (error) { + console.error('타이머 초기화/동기화 처리 실패:', error); + } +} + +// 카운트다운 타이머 초기화 함수 +async function initBreakCountdown() { + // 중복 실행 방지 + if (isCountdownInitializing) { + console.log('카운트다운 초기화가 이미 진행 중입니다.'); + return; + } + + isCountdownInitializing = true; + + try { + // 설정에서 시간 알림 설정 가져오기 + const settings = await chrome.storage.local.get(['time_alarm_settings']); + let timeAlarmSettings = settings.time_alarm_settings; + + // 시간 알림 설정이 없으면 기본값으로 활성화 + if (!timeAlarmSettings) { + timeAlarmSettings = { + enabled: true, // 기본값을 true로 변경 + workTime: 60, + restTime: 5 + }; + + // 기본 설정 저장 + await chrome.storage.local.set({ time_alarm_settings: timeAlarmSettings }); + console.log('시간 알림 설정이 없어서 기본값으로 활성화:', timeAlarmSettings); + } + + // 디버깅을 위한 로그 추가 + console.log('카운트다운 타이머 설정:', timeAlarmSettings); + + // 타이머 섹션 항상 표시 (로그인 시에는) + const timerSection = document.getElementById('break-timer-section'); + if (timerSection) { + timerSection.style.display = 'block'; + } + + // 시간 알림이 비활성화된 경우에만 메시지 표시 + if (!timeAlarmSettings.enabled) { + console.log('시간 알림이 비활성화됨 - 설정에서 활성화 필요'); + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + countdownEl.textContent = '⚙️ 설정에서 시간 알림을 활성화해주세요'; + countdownEl.style.color = '#f39c12'; + } + + // 기존 타이머 정리 + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + return; + } + + // Background에서 현재 타이머 상태 가져오기 (리셋하지 않고 동기화만) + try { + const response = await chrome.runtime.sendMessage({ + action: 'getTimerStatus' + }); + + if (response && response.success && response.isRunning) { + // Background 타이머가 실행 중인 경우 - 남은 시간으로 동기화 + const remainingMs = response.remainingTime; + + console.log('Background 타이머와 동기화 (리셋 없음):', { + 남은시간_분: Math.floor(remainingMs / (1000 * 60)), + 남은시간_초: Math.floor((remainingMs % (1000 * 60)) / 1000) + }); + + // 기존 카운트다운이 있으면 정리하고 새로 시작 + if (countdownInterval) { + clearInterval(countdownInterval); + } + + const nextBreakTime = new Date(Date.now() + remainingMs); + startCountdown(nextBreakTime); + + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + countdownEl.style.color = '#1e7e1e'; // 정상 색상 + } + + } else { + // Background 타이머가 실행 중이지 않은 경우 + console.log('Background 타이머가 실행 중이 아님'); + + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + countdownEl.textContent = '타이머가 시작되지 않았습니다'; + countdownEl.style.color = '#e74c3c'; + } + + // 기존 카운트다운 정리 + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + } + } catch (error) { + console.error('Background 타이머 상태 확인 실패:', error); + + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + countdownEl.textContent = '타이머 상태 확인 중...'; + countdownEl.style.color = '#3498db'; + } + } + + } catch (error) { + console.error('카운트다운 타이머 초기화 오류:', error); + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + countdownEl.textContent = '설정 오류'; + countdownEl.style.color = '#e74c3c'; + } + } finally { + isCountdownInitializing = false; + } +} + +// 설정 변경 감지 및 타이머 리셋 함수 +function watchSettingsChanges() { + // 기존 감시자가 있으면 정리 + if (settingsWatcher) { + clearInterval(settingsWatcher); + } + + let lastSettings = null; + + // 5초마다 설정 변경 확인 + settingsWatcher = setInterval(async () => { + try { + const settings = await chrome.storage.local.get(['time_alarm_settings']); + const currentSettings = settings.time_alarm_settings; + + // 설정이 변경되었는지 확인 + if (lastSettings && JSON.stringify(lastSettings) !== JSON.stringify(currentSettings)) { + console.log('시간 알림 설정 변경 감지:', { + 이전: lastSettings, + 현재: currentSettings + }); + + // 타이머 리셋 및 재시작 + console.log('타이머 리셋 및 재시작'); + await initBreakCountdown(); + + // 사용자에게 알림 + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl && currentSettings && currentSettings.enabled) { + // 잠시 리셋 메시지 표시 + const originalText = countdownEl.textContent; + countdownEl.textContent = '⚙️ 설정 변경됨 - 타이머 리셋'; + countdownEl.style.color = '#2196F3'; + + setTimeout(() => { + countdownEl.style.color = ''; + }, 2000); + } + } + + lastSettings = currentSettings ? JSON.parse(JSON.stringify(currentSettings)) : null; + + } catch (error) { + console.error('설정 변경 감지 오류:', error); + } + }, 5000); // 5초마다 확인 +} + +// Chrome storage 변경 이벤트 리스너 (더 즉각적인 반응) +function setupStorageListener() { + if (chrome.storage && chrome.storage.onChanged) { + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'local' && changes.time_alarm_settings) { + console.log('Chrome storage 변경 감지 - time_alarm_settings:', { + 이전값: changes.time_alarm_settings.oldValue, + 새값: changes.time_alarm_settings.newValue + }); + + // 즉시 타이머 리셋 + setTimeout(() => { + initBreakCountdown(); + + // 사용자에게 즉시 피드백 + const countdownEl = document.getElementById('break-countdown'); + if (countdownEl) { + const newSettings = changes.time_alarm_settings.newValue; + if (newSettings && newSettings.enabled) { + countdownEl.textContent = '⚙️ 설정 적용됨'; + countdownEl.style.color = '#4CAF50'; + + setTimeout(() => { + countdownEl.style.color = ''; + }, 1500); + } + } + }, 100); + } + + // API 호출량 변경 감지 및 UI 업데이트 + if (namespace === 'local' && changes.user_current_api_calls) { + console.log('Chrome storage 변경 감지 - user_current_api_calls:', { + 이전값: changes.user_current_api_calls.oldValue, + 새값: changes.user_current_api_calls.newValue + }); + + // 즉시 사용자 호출량 UI 업데이트 + updateUserUsageDisplay(); + } + }); + + console.log('Chrome storage 변경 리스너 설정 완료'); + } +} + +// 사용자 호출량 표시 업데이트 함수 +async function updateUserUsageDisplay() { + try { + const { user_current_api_calls, user_api_limit } = await chrome.storage.local.get([ + 'user_current_api_calls', + 'user_api_limit' + ]); + + const userUsageEl = document.getElementById('user-usage'); + if (userUsageEl) { + const currentCalls = user_current_api_calls || 0; + let usageText = currentCalls.toString(); + + if (user_api_limit !== null && user_api_limit !== undefined) { + usageText = `${currentCalls}/${user_api_limit}`; + } + + userUsageEl.textContent = usageText; + + // 시각적 피드백 제공 + userUsageEl.style.color = '#4CAF50'; + userUsageEl.style.fontWeight = 'bold'; + + setTimeout(() => { + userUsageEl.style.color = ''; + userUsageEl.style.fontWeight = ''; + }, 2000); + + console.log('사용자 호출량 UI 업데이트 완료:', { + currentCalls, + apiLimit: user_api_limit, + displayText: usageText + }); + } + } catch (error) { + console.error('사용자 호출량 UI 업데이트 실패:', error); + } +} + +// 카운트다운 시작 함수 +function startCountdown(targetTime) { + // 기존 타이머가 있으면 정리 + if (countdownInterval) { + clearInterval(countdownInterval); + } + + const countdownEl = document.getElementById('break-countdown'); + if (!countdownEl) return; + + countdownInterval = setInterval(() => { + const now = new Date(); + const timeLeft = targetTime.getTime() - now.getTime(); + + if (timeLeft <= 0) { + // 시간이 다 되었을 때 + countdownEl.textContent = '휴식 시간입니다! 🎉'; + clearInterval(countdownInterval); + + // 새로운 카운트다운 시작 (휴식 시간 후 다시 작업 시간) + setTimeout(() => { + initBreakCountdown(); + }, 1000); + + return; + } + + // 남은 시간을 시:분:초 형태로 표시 + const hours = Math.floor(timeLeft / (1000 * 60 * 60)); + const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000); + + let timeString = ''; + if (hours > 0) { + timeString = `${hours}시간 ${minutes}분 ${seconds}초`; + } else if (minutes > 0) { + timeString = `${minutes}분 ${seconds}초`; + } else { + timeString = `${seconds}초`; + } + + countdownEl.textContent = timeString; + }, 1000); +} + +// 페이지 언로드 시 타이머 정리 +window.addEventListener('beforeunload', () => { + // 카운트다운 타이머 정리 + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + + // 설정 감시 타이머 정리 + if (settingsWatcher) { + clearInterval(settingsWatcher); + settingsWatcher = null; + } + + console.log('모든 타이머 정리 완료'); +}); + +// 팝업 포커스 시 사용자 호출량 업데이트 +window.addEventListener('focus', () => { + console.log('팝업 포커스 - 사용자 호출량 업데이트'); + updateUserUsageDisplay(); +}); + +// 팝업 가시성 변경 시 사용자 호출량 업데이트 +document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + console.log('팝업 가시성 변경 - 사용자 호출량 업데이트'); + updateUserUsageDisplay(); + } +}); + +// 매뉴얼 버튼 이벤트 리스너 +document.getElementById('manual-btn').addEventListener('click', function() { + chrome.tabs.create({ url: chrome.runtime.getURL('manual.html') }); +}); \ No newline at end of file diff --git a/wrmc_ext/rest-modal.html b/wrmc_ext/rest-modal.html new file mode 100644 index 0000000..528675f --- /dev/null +++ b/wrmc_ext/rest-modal.html @@ -0,0 +1,296 @@ + + + + + + 휴식 시간 + + + + + + + + \ No newline at end of file diff --git a/wrmc_ext/rest-modal.js b/wrmc_ext/rest-modal.js new file mode 100644 index 0000000..85a5638 --- /dev/null +++ b/wrmc_ext/rest-modal.js @@ -0,0 +1,304 @@ +class RestModal { + constructor() { + this.restTime = 5; // 기본 5분 + this.currentTime = this.restTime * 60; // 초 단위 + this.timer = null; + this.config = null; + this.currentSaying = null; + this.autoZzim = false; + + // 기본 추천 활동 목록 (백엔드 연결 실패 시 사용) + this.defaultActivities = [ + "🚶‍♀️ 가벼운 산책을 해보세요", + "💧 물 한 잔을 마시며 수분을 보충하세요", + "🧘‍♀️ 심호흡을 하며 명상을 해보세요", + "🤸‍♀️ 간단한 스트레칭으로 몸을 풀어보세요", + "👀 눈 운동을 하며 눈의 피로를 풀어보세요", + "🚽 화장실을 다녀오세요", + "🌱 창밖을 보며 자연을 감상해보세요", + "📱 잠시 휴대폰을 내려놓고 마음을 비워보세요", + "☕ 따뜻한 차 한 잔을 마셔보세요", + "🎵 좋아하는 음악을 들으며 휴식하세요", + "📚 짧은 글이나 명언을 읽어보세요", + "🤝 동료나 가족과 간단한 대화를 나누세요", + "🧴 손 마사지나 목 마사지를 해보세요", + "🏃‍♀️ 제자리에서 가볍게 몸을 움직여보세요", + "🍎 건강한 간식을 드세요" + ]; + + this.activities = []; // 백엔드에서 가져온 활동 목록 + } + + async init() { + try { + await this.loadSettings(); + await this.loadConfig(); + this.setupEventListeners(); + await this.loadRestActivities(); + this.showRandomActivity(); + await this.loadRandomSaying(); + this.startTimer(); + } catch (error) { + console.error('[RestModal] 초기화 실패:', error); + } + } + + async loadSettings() { + try { + const result = await chrome.storage.local.get('time_alarm_settings'); + const settings = result.time_alarm_settings || {}; + + this.restTime = settings.restTime || 5; + this.autoZzim = settings.autoZzim || false; + this.currentTime = this.restTime * 60; + + console.log('[RestModal] 설정 로드:', { restTime: this.restTime, autoZzim: this.autoZzim }); + } catch (error) { + console.error('[RestModal] 설정 로드 실패:', error); + } + } + + async loadConfig() { + try { + const result = await chrome.storage.local.get('settings_config'); + this.config = result.settings_config || {}; + console.log('[RestModal] 설정 정보 로드:', this.config); + } catch (error) { + console.error('[RestModal] 설정 정보 로드 실패:', error); + } + } + + setupEventListeners() { + // 닫기 버튼 + document.getElementById('closeBtn').addEventListener('click', () => { + this.closeModal(); + }); + + // 건너뛰기 버튼 + document.getElementById('skipBtn').addEventListener('click', () => { + this.closeModal(); + }); + + // ESC 키로 닫기 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + this.closeModal(); + } + }); + } + + async loadRestActivities() { + try { + if (!this.config.ACCESS_TOKEN) { + throw new Error('Access token not found'); + } + + const SUPABASE_URL = this.config.SUPABASE_URL || "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + // public.events 테이블에서 event_type이 'rest_time'인 데이터 가져오기 + const apiUrl = `${SUPABASE_URL}/rest/v1/events?select=message&event_type=eq.rest_time&order=created_at.desc&limit=50`; + + console.log('[RestModal] 추천활동 API 호출:', apiUrl); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${this.config.ACCESS_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`API 호출 실패: ${response.status}`); + } + + const events = await response.json(); + + if (events && events.length > 0) { + // message 필드에서 JSON 파싱하여 활동 목록 생성 + this.activities = []; + + events.forEach(event => { + try { + // message 필드가 JSON 형식인 경우 파싱 + const messageData = JSON.parse(event.message); + + // 활동 텍스트 추출 (다양한 형식 지원) + if (messageData.activity) { + this.activities.push(messageData.activity); + } else if (messageData.text) { + this.activities.push(messageData.text); + } else if (typeof messageData === 'string') { + this.activities.push(messageData); + } + } catch (parseError) { + // JSON 파싱 실패 시 문자열 그대로 사용 + if (typeof event.message === 'string' && event.message.trim()) { + this.activities.push(event.message); + } + } + }); + + console.log('[RestModal] 백엔드에서 추천활동 로드 완료:', this.activities.length + '개'); + } else { + throw new Error('추천활동 데이터가 없습니다'); + } + + } catch (error) { + console.error('[RestModal] 추천활동 로드 실패:', error); + + // 기본 활동 목록 사용 + this.activities = [...this.defaultActivities]; + console.log('[RestModal] 기본 추천활동 사용:', this.activities.length + '개'); + } + } + + showRandomActivity() { + if (this.activities.length === 0) { + this.activities = [...this.defaultActivities]; + } + + const randomActivity = this.activities[Math.floor(Math.random() * this.activities.length)]; + document.getElementById('activitySuggestion').innerHTML = randomActivity; + } + + async loadRandomSaying() { + try { + if (!this.config.ACCESS_TOKEN) { + throw new Error('Access token not found'); + } + + const SUPABASE_URL = this.config.SUPABASE_URL || "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + // 최근 1달 이내의 승인된 어록 가져오기 + const oneMonthAgo = new Date(); + oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); + + const apiUrl = `${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${oneMonthAgo.toISOString()}&admin_approval=eq.true&order=created_at.desc&limit=50`; + + console.log('[RestModal] 어록 API 호출:', apiUrl); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${this.config.ACCESS_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`API 호출 실패: ${response.status}`); + } + + const sayings = await response.json(); + + if (sayings && sayings.length > 0) { + // 랜덤하게 하나 선택 + const randomSaying = sayings[Math.floor(Math.random() * sayings.length)]; + this.currentSaying = randomSaying; + + // 어록 표시 + const sayingContent = document.getElementById('sayingContent'); + const sayingAuthor = document.getElementById('sayingAuthor'); + + sayingContent.innerHTML = `"${randomSaying.saying}"`; + + // 카테고리와 대상 정보 표시 + const category = randomSaying.sayings_cat?.saying_cat || ''; + const target = randomSaying.sayings_target?.target || ''; + const dateStr = new Date(randomSaying.created_at).toLocaleDateString('ko-KR'); + + sayingAuthor.innerHTML = `${category} ${target ? `• ${target}` : ''} • ${dateStr}`; + + console.log('[RestModal] 어록 로드 완료:', randomSaying); + } else { + throw new Error('어록이 없습니다'); + } + + } catch (error) { + console.error('[RestModal] 어록 로드 실패:', error); + + // 기본 메시지 표시 + document.getElementById('sayingContent').innerHTML = '"열심히 일한 당신, 잠시 휴식을 취하며 에너지를 충전하세요!"'; + document.getElementById('sayingAuthor').innerHTML = '타냐 • 휴식 메시지'; + } + } + + startTimer() { + this.updateTimerDisplay(); + this.updateProgressBar(); + + this.timer = setInterval(() => { + this.currentTime--; + + this.updateTimerDisplay(); + this.updateProgressBar(); + + if (this.currentTime <= 0) { + this.onTimerComplete(); + } + }, 1000); + } + + updateTimerDisplay() { + const minutes = Math.floor(this.currentTime / 60); + const seconds = this.currentTime % 60; + const display = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + document.getElementById('timerDisplay').textContent = display; + } + + updateProgressBar() { + const totalTime = this.restTime * 60; + const elapsed = totalTime - this.currentTime; + const percentage = (elapsed / totalTime) * 100; + + document.getElementById('progressFill').style.width = `${percentage}%`; + } + + async onTimerComplete() { + clearInterval(this.timer); + + // 완료 메시지 표시 + this.showCompletionMessage(); + + // 3초 후 자동 닫기 + setTimeout(() => { + this.closeModal(); + }, 3000); + } + + showCompletionMessage() { + const modalContainer = document.querySelector('.modal-container'); + modalContainer.innerHTML = ` +
🚀
+

휴식 완료!

+

이제 다시 열심히 월매출 1억을 향해 달려가세요!

+
+ 💪 화이팅! 성공은 바로 앞에 있습니다! +
+
+ +
+ `; + } + + closeModal() { + if (this.timer) { + clearInterval(this.timer); + } + + // 창 닫기 + window.close(); + } +} + +// 페이지 로드 시 휴식 모달 초기화 +document.addEventListener('DOMContentLoaded', () => { + const restModal = new RestModal(); + restModal.init(); +}); \ No newline at end of file diff --git a/wrmc_ext/sayings.html b/wrmc_ext/sayings.html new file mode 100644 index 0000000..6d9b1d3 --- /dev/null +++ b/wrmc_ext/sayings.html @@ -0,0 +1,1195 @@ + + + + + 어록 관리 + + + +
+

📚 타냐대장경 관리

+
+
👤 로딩 중...
+ +
+
+ + +
+ 초기화 중... +
+ +
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/wrmc_ext/sayings.js b/wrmc_ext/sayings.js new file mode 100644 index 0000000..63f3ae5 --- /dev/null +++ b/wrmc_ext/sayings.js @@ -0,0 +1,2845 @@ +// 타냐대장경 관리 모듈 +class SayingsManager { + constructor() { + // 기본 설정 + this.config = null; + this.allSayings = []; + this.filteredSayings = []; + this.currentUser = null; + this.currentEditingSaying = null; // 현재 편집 중인 타냐대장경 + this.searchTimeout = null; + this.newSayingsCheckInterval = null; + this.debugLogs = []; + + // 디버그 모드 설정 + this.debugMode = false; + + // 백그라운드 연동 설정 + this.isBackgroundConnected = false; + this.lastSayingsCount = 0; + + this.debugLog('SayingsManager 생성자 호출'); + } + + // 설정 로드 함수 + async loadConfig() { + try { + this.debugLog('chrome.storage에서 설정 로드 시작'); + + // 1. sayings_config 우선 확인 + const configData = await chrome.storage.local.get('sayings_config'); + + if (configData.sayings_config) { + const config = configData.sayings_config; + this.debugLog('sayings_config에서 설정 로드', { + hasUrl: !!config.SUPABASE_URL, + hasKey: !!config.SUPABASE_ANON_KEY, + hasToken: !!config.ACCESS_TOKEN, + debugMode: config.DEBUG_MODE, + timestamp: config.timestamp, + age: Date.now() - config.timestamp + }); + + // 설정이 너무 오래된 경우 (5분 이상) 경고 + if (Date.now() - config.timestamp > 5 * 60 * 1000) { + this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp }); + } + + this.SUPABASE_URL = config.SUPABASE_URL; + this.SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY; + this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true; + this.ACCESS_TOKEN = config.ACCESS_TOKEN; + + } else { + // 2. 개별 설정 확인 (fallback) + this.debugLog('sayings_config가 없음, 개별 설정 확인 중'); + + const storageData = await chrome.storage.local.get([ + 'access_token', + 'SUPABASE_URL', + 'SUPABASE_ANON_KEY' + ]); + + this.debugLog('개별 설정 조회 결과', { + hasToken: !!storageData.access_token, + hasUrl: !!storageData.SUPABASE_URL, + hasKey: !!storageData.SUPABASE_ANON_KEY + }); + + // 기본값 설정 + this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000'; + this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'; + this.ACCESS_TOKEN = storageData.access_token; + } + + this.isConfigLoaded = true; + + this.debugLog('설정 로드 완료', { + SUPABASE_URL: this.SUPABASE_URL, + hasToken: !!this.ACCESS_TOKEN, + tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, + DEBUG_MODE: this.DEBUG_MODE, + isConfigLoaded: this.isConfigLoaded + }); + + // 설정 검증 + if (!this.SUPABASE_URL) { + throw new Error('SUPABASE_URL이 설정되지 않았습니다'); + } + if (!this.SUPABASE_ANON_KEY) { + throw new Error('SUPABASE_ANON_KEY가 설정되지 않았습니다'); + } + if (!this.ACCESS_TOKEN) { + throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다'); + } + + return true; + + } catch (error) { + this.debugLog('설정 로드 실패', { error: error.message }); + throw error; + } + } + + // 디버그 로깅 함수 + debugLog(message, data = null) { + if (this.DEBUG_MODE) { + console.log(`[Sayings] ${message}`, data || ''); + this.updateDebugUI(`[${new Date().toLocaleTimeString()}] ${message}`); + } + } + + // 디버그 UI 업데이트 + updateDebugUI(message) { + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + if (this.DEBUG_MODE) { + // 디버그 모드일 때만 표시 + const existingButton = debugElement.querySelector('button'); + const buttonHtml = existingButton ? existingButton.outerHTML : ''; + + debugElement.innerHTML = `${message}
${buttonHtml}`; + debugElement.style.display = "block"; + + // 버튼 이벤트 다시 등록 + if (existingButton) { + const newButton = debugElement.querySelector('button'); + if (newButton && !newButton.onclick) { + newButton.addEventListener('click', this.showDebugLogs.bind(this)); + } + } + } else { + // 디버그 모드가 아닐 때는 숨김 + debugElement.style.display = "none"; + debugElement.innerHTML = ""; + } + } + } + + // 초기화 및 데이터 로드 + async initialize() { + try { + this.debugLog('SayingsManager 초기화 시작'); + + // 1) 설정 로드 (chrome.storage에서) + this.debugLog('설정 로드 중...'); + await this.loadConfig(); + + // 2) 토큰 유효성 검증 + this.debugLog('토큰 유효성 검증 중...'); + await this.validateToken(); + + // 3) 현재 사용자 정보 로드 + this.debugLog('현재 사용자 정보 로드 중...'); + await this.loadCurrentUser(); + + // 4) 타냐대장경 등록 모달 이벤트 등록 + this.debugLog('타냐대장경 등록 모달 이벤트 등록 중...'); + this.attachAddSayingEvents(); + + // 5) 카테고리와 타겟 옵션 로드 + this.debugLog('카테고리와 타겟 옵션 로드 중...'); + await this.loadCategoryAndTargetOptions(); + + // 6) 타냐대장경 목록 로드 + this.debugLog('타냐대장경 목록 로드 시작'); + await this.loadSayings(); + + // 7) 새 타냐대장경 감지 시작 + this.debugLog('새 타냐대장경 감지 기능 시작'); + this.startNewSayingsMonitoring(); + + this.debugLog('SayingsManager 초기화 완료'); + + } catch (error) { + this.debugLog('초기화 실패', { error: error.message }); + this.renderError(error.message); + } + } + + // 토큰 유효성 검증 + async validateToken() { + try { + this.debugLog('토큰 검증 API 호출 준비', { + url: `${this.SUPABASE_URL}/auth/v1/user`, + hasToken: !!this.ACCESS_TOKEN, + tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0 + }); + + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + mode: 'cors', + credentials: 'omit' + }); + + this.debugLog('토큰 검증 API 응답', { + status: authRes.status, + statusText: authRes.statusText, + ok: authRes.ok + }); + + if (!authRes.ok) { + const errorText = await authRes.text(); + this.debugLog('토큰 검증 실패', { + status: authRes.status, + error: errorText + }); + throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})`); + } + + const userData = await authRes.json(); + this.debugLog('토큰 검증 성공', { + email: userData.email, + id: userData.id + }); + + return userData; + + } catch (error) { + this.debugLog('토큰 검증 중 오류', { + errorName: error.name, + errorMessage: error.message + }); + + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.`); + } + + throw error; + } + } + + // 타냐대장경 목록 로드 + async loadSayings() { + this.debugLog('타냐대장경 로딩 시작'); + + try { + // 로딩 표시 + document.getElementById('sayings-container').innerHTML = '
📚 타냐대장경을 불러오는 중...
'; + + // API 호출을 위한 헤더 설정 + const headers = { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + + // 승인된 타냐대장경만 가져오는 쿼리 + const query = `${this.SUPABASE_URL}/rest/v1/tanya_sayings?admin_approval=eq.true&order=created_at.desc`; + + this.debugLog('타냐대장경 API 호출', { + url: query, + headers: Object.keys(headers) + }); + + const response = await fetch(query, { + method: 'GET', + headers: headers + }); + + if (!response.ok) { + const errorText = await response.text(); + this.debugLog('타냐대장경 로딩 실패', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + throw new Error(`타냐대장경 로딩 실패: ${response.status} ${response.statusText}\n${errorText}`); + } + + const sayings = await response.json(); + + this.debugLog('타냐대장경 로딩 성공', { + count: sayings.length, + firstSaying: sayings.length > 0 ? sayings[0].saying_title : 'N/A' + }); + + // 데이터 저장 + this.allSayings = sayings; + this.filteredSayings = [...sayings]; + + // 필터 이벤트 연결 + this.attachFilterEvents(); + + // 타냐대장경 표시 + await this.displaySayings(); + + } catch (error) { + this.debugLog('타냐대장경 로딩 에러', { error: error.message }); + this.renderError('타냐대장경을 불러오는데 실패했습니다', error.message); + } + } + + // 타냐대장경 표시 + async displaySayings() { + this.debugLog('타냐대장경 표시 시작'); + + const container = document.getElementById('sayings-container'); + const stats = this.calculateStats(); + + // 통계 업데이트 + const statsText = `총 ${stats.total}개의 타냐대장경 (${Object.entries(stats.byCategory).map(([cat, count]) => `${cat}: ${count}개`).join(', ')})`; + document.getElementById('sayings-stats').textContent = statsText; + + this.debugLog('타냐대장경 통계 계산 완료', stats); + + if (this.filteredSayings.length === 0) { + container.innerHTML = '
📝 표시할 타냐대장경이 없습니다.
'; + this.debugLog('표시할 타냐대장경 없음'); + return; + } + + // 타냐대장경 카드 생성 (비동기 처리) + const sayingCards = await Promise.all(this.filteredSayings.map(async saying => { + // 콘텐츠 렌더링 (블록 데이터 우선) + let contentHtml = ''; + + if (saying.content_blocks && Array.isArray(saying.content_blocks) && saying.content_blocks.length > 0) { + // 블록 데이터가 있는 경우 + contentHtml = this.renderContentBlocks(saying.content_blocks); + } else { + // 기존 마크다운 방식 + contentHtml = await this.convertMarkdownToHtml(saying.saying); + } + + // 날짜 포맷팅 + const date = new Date(saying.created_at); + const formattedDate = date.toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + + // 등록자 정보 (register 필드 사용) + const authorName = saying.register || saying.user_name || '알 수 없음'; + + return ` +
+
+

${this.escapeHtml(saying.saying_title)}

+
+ ${this.escapeHtml(saying.cat)} + ${this.escapeHtml(saying.target)} +
+
+
+ ${contentHtml} +
+ +
+ `; + })); + + container.innerHTML = sayingCards.join(''); + + // 카드 클릭 이벤트 추가 + const cardElements = container.querySelectorAll('.saying-card'); + cardElements.forEach((cardElement, index) => { + cardElement.addEventListener('click', (e) => { + // 삭제 버튼이나 다른 버튼 클릭 시 모달이 열리지 않도록 함 + if (e.target.closest('.delete-btn') || e.target.closest('button')) { + return; + } + this.openDetailModal(this.filteredSayings[index]); + }); + }); + + this.debugLog('타냐대장경 표시 완료', { + filteredCount: this.filteredSayings.length, + cardsGenerated: sayingCards.length + }); + } + + // 블록 데이터 렌더링 함수 + renderContentBlocks(blocks) { + if (!blocks || !Array.isArray(blocks) || blocks.length === 0) { + return ''; + } + + return blocks.map(block => { + if (block.type === 'text') { + return `
${this.escapeHtml(block.content || '').replace(/\n/g, '
')}
`; + } else if (block.type === 'image') { + if (block.content) { + return ` +
+ ${this.escapeHtml(block.alt || '')} + ${block.alt ? `
${this.escapeHtml(block.alt)}
` : ''} +
+ `; + } + } + return ''; + }).join(''); + } + + // 전체 화면 이미지 표시 함수 + showFullImage(imageSrc) { + let modal = document.querySelector('.full-image-modal'); + if (!modal) { + modal = document.createElement('div'); + modal.className = 'full-image-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.95); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + cursor: zoom-out; + `; + modal.innerHTML = ` +
+ × + 전체 화면 이미지 +
+ + + +
+
+ `; + document.body.appendChild(modal); + + const img = modal.querySelector('img'); + const closeBtn = modal.querySelector('.close-btn'); + const zoomBtns = modal.querySelectorAll('.zoom-btn'); + + let currentZoom = 1; + let isDragging = false; + let startX, startY, translateX = 0, translateY = 0; + + // 닫기 버튼 이벤트 + closeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + modal.style.opacity = '0'; + modal.style.visibility = 'hidden'; + currentZoom = 1; + translateX = 0; + translateY = 0; + img.style.transform = 'scale(1) translate(0, 0)'; + }); + + // 배경 클릭으로 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.opacity = '0'; + modal.style.visibility = 'hidden'; + currentZoom = 1; + translateX = 0; + translateY = 0; + img.style.transform = 'scale(1) translate(0, 0)'; + } + }); + + // 확대/축소 버튼 이벤트 + zoomBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const action = btn.dataset.action; + + if (action === 'zoom-in') { + currentZoom = Math.min(currentZoom * 1.5, 5); + } else if (action === 'zoom-out') { + currentZoom = Math.max(currentZoom / 1.5, 0.5); + } else if (action === 'zoom-reset') { + currentZoom = 1; + translateX = 0; + translateY = 0; + } + + img.style.transform = `scale(${currentZoom}) translate(${translateX}px, ${translateY}px)`; + img.style.cursor = currentZoom > 1 ? 'move' : 'zoom-in'; + }); + + // 버튼 호버 효과 + btn.addEventListener('mouseenter', () => { + btn.style.background = 'rgba(255,255,255,0.4)'; + }); + + btn.addEventListener('mouseleave', () => { + btn.style.background = 'rgba(255,255,255,0.2)'; + }); + }); + + // 이미지 드래그 기능 + img.addEventListener('mousedown', (e) => { + if (currentZoom > 1) { + isDragging = true; + startX = e.clientX - translateX; + startY = e.clientY - translateY; + img.style.cursor = 'grabbing'; + e.preventDefault(); + } + }); + + document.addEventListener('mousemove', (e) => { + if (isDragging && currentZoom > 1) { + translateX = e.clientX - startX; + translateY = e.clientY - startY; + img.style.transform = `scale(${currentZoom}) translate(${translateX}px, ${translateY}px)`; + } + }); + + document.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + img.style.cursor = currentZoom > 1 ? 'move' : 'zoom-in'; + } + }); + + // 휠 확대/축소 + modal.addEventListener('wheel', (e) => { + e.preventDefault(); + if (e.deltaY < 0) { + currentZoom = Math.min(currentZoom * 1.2, 5); + } else { + currentZoom = Math.max(currentZoom / 1.2, 0.5); + } + img.style.transform = `scale(${currentZoom}) translate(${translateX}px, ${translateY}px)`; + img.style.cursor = currentZoom > 1 ? 'move' : 'zoom-in'; + }); + + // ESC 키로 닫기 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.style.visibility === 'visible') { + modal.style.opacity = '0'; + modal.style.visibility = 'hidden'; + currentZoom = 1; + translateX = 0; + translateY = 0; + img.style.transform = 'scale(1) translate(0, 0)'; + } + }); + } + + const img = modal.querySelector('img'); + img.src = imageSrc; + modal.style.opacity = '1'; + modal.style.visibility = 'visible'; + + // 이미지 로드 완료 시 최적화 적용 + img.onload = () => { + // 고해상도 이미지에 대한 렌더링 최적화 + img.style.imageRendering = 'auto'; + img.style.imageRendering = '-webkit-optimize-contrast'; + + // 이미지 크기에 따른 초기 스케일 조정 + const containerWidth = modal.offsetWidth * 0.9; + const containerHeight = modal.offsetHeight * 0.9; + const imgAspectRatio = img.naturalWidth / img.naturalHeight; + const containerAspectRatio = containerWidth / containerHeight; + + if (img.naturalWidth > containerWidth || img.naturalHeight > containerHeight) { + // 이미지가 컨테이너보다 큰 경우 적절한 크기로 조정 + if (imgAspectRatio > containerAspectRatio) { + img.style.maxWidth = '90%'; + img.style.maxHeight = 'auto'; + } else { + img.style.maxWidth = 'auto'; + img.style.maxHeight = '90%'; + } + } + }; + } + + // 마크다운을 HTML로 변환하는 함수 (비동기) + async convertMarkdownToHtml(content) { + // 빈 내용 체크 + if (!content || typeof content !== 'string') { + console.warn('⚠️ 변환할 마크다운 내용이 없습니다'); + return ''; + } + + try { + // 마크다운 라이브러리가 로드되었는지 확인 + if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') { + console.log('✅ 마크다운 라이브러리 사용 가능'); + + // marked로 마크다운을 HTML로 변환 + const rawHtml = marked.parse(content); + + // DOMPurify로 HTML 정화 + const cleanHtml = DOMPurify.sanitize(rawHtml); + + console.log('🎯 마크다운 변환 완료'); + return cleanHtml; + } else { + console.warn('⚠️ 마크다운 라이브러리가 로드되지 않음, 기본 처리 사용'); + return this.basicMarkdownToHtml(content); + } + } catch (error) { + console.error('❌ 마크다운 변환 중 오류:', error); + // 오류 발생 시 기본 마크다운 처리로 폴백 + return this.basicMarkdownToHtml(content); + } + } + + // 기본 마크다운 처리 (라이브러리 없이) - 개선된 버전 + basicMarkdownToHtml(markdown) { + let html = this.escapeHtml(markdown); + + // 기본적인 마크다운 문법 처리 (순서 중요) + html = html + // 코드 블록 (```) + .replace(/```([\s\S]*?)```/g, '
$1
') + // 인라인 코드 (`) + .replace(/`([^`]+)`/g, '$1') + // 볼드 텍스트 (**) + .replace(/\*\*([^*]+)\*\*/g, '$1') + // 이탤릭 텍스트 (*) + .replace(/\*([^*]+)\*/g, '$1') + // 헤더 (###, ##, #) + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + // 인용문 (>) + .replace(/^> (.+)$/gm, '
$1
') + // 링크 [텍스트](URL) + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '
$1') + // 리스트 (- 또는 *) + .replace(/^[*-] (.+)$/gm, '
  • $1
  • ') + // 줄바꿈 처리 + .replace(/\n\n/g, '

    ') + .replace(/\n/g, '
    '); + + // 리스트 태그로 감싸기 + html = html.replace(/(

  • .*<\/li>)/gs, '
      $1
    '); + + // 단락 태그로 감싸기 (이미 p 태그가 있는 경우 제외) + if (!html.includes('

    ') && !html.includes('

    ') && !html.includes('

    ') && !html.includes('

    ')) { + html = '

    ' + html + '

    '; + } + + this.debugLog('기본 마크다운 변환 완료', { + 원본길이: markdown.length, + 변환길이: html.length + }); + + return html; + } + + // HTML 이스케이프 + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // 에러 렌더링 + renderError(message) { + const container = document.getElementById("sayings-container"); + const statsDiv = document.getElementById("sayings-stats"); + + if (container) { + container.innerHTML = ` +
    +
    +
    오류가 발생했습니다
    +
    ${message}
    +
    + `; + } + + if (statsDiv) { + statsDiv.innerHTML = `
    오류: ${message}
    `; + } + + this.debugLog('에러 렌더링 완료', { message }); + } + + // 로그 확인 함수 + showDebugLogs() { + const debugElement = document.getElementById('debug-info'); + if (!debugElement) return; + + // 현재 상태 수집 + const stats = this.calculateStats(); + const currentUser = this.currentUser; + + // 디버그 정보 생성 + const debugInfo = ` +
    +

    🔧 디버그 정보

    + +
    📊 타냐대장경 통계:
    +
    + • 전체 타냐대장경: ${stats.total}개
    + • 필터링된 타냐대장경: ${this.filteredSayings.length}개
    + • 카테고리별: ${Object.entries(stats.byCategory).map(([cat, count]) => `${cat}(${count})`).join(', ')} +
    + +
    👤 사용자 정보:
    +
    + ${currentUser ? `• 이메일: ${currentUser.email}
    • 닉네임: ${currentUser.nickname}
    • 회원등급: ${currentUser.membership_level || '기본'}` : '• 로그인 필요'} +
    + +
    🔔 백그라운드 새 타냐대장경 감지:
    +
    + • 상태: 활성화 (1분 간격)
    + • 방식: 백그라운드 스크립트 연동
    + • 마지막 확인: 백그라운드에서 관리
    + • 브라우저 알림: 지원됨
    + • 로컬 스토리지: 사용 중 +
    + +
    ⚙️ 시스템 상태:
    +
    + • 설정 로드: ${this.isConfigLoaded ? '완료' : '실패'}
    + • API 연결: ${this.SUPABASE_URL ? '설정됨' : '미설정'}
    + • 토큰 상태: ${this.ACCESS_TOKEN ? '있음' : '없음'}
    + • 디버그 모드: ${this.DEBUG_MODE ? '활성화' : '비활성화'} +
    + +
    + + +
    +
    + `; + + debugElement.innerHTML = debugInfo; + debugElement.style.display = 'block'; + + // 테스트 버튼 이벤트 등록 + const testBtn = document.getElementById('test-new-sayings-btn'); + if (testBtn) { + testBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + if (confirm('새 타냐대장경 감지 기능을 테스트하시겠습니까?\n백그라운드에서 감지된 새 타냐대장경이 있으면 모달이 표시됩니다.')) { + await this.checkBackgroundNewSayings(); + } + }); + } + + // 스토리지 정리 버튼 이벤트 등록 + const clearBtn = document.getElementById('clear-storage-btn'); + if (clearBtn) { + clearBtn.addEventListener('click', async (e) => { + e.preventDefault(); + if (confirm('모든 저장된 데이터를 삭제하시겠습니까?\n(로그인 정보, 설정, 캐시 등이 모두 삭제됩니다)')) { + try { + // Chrome 확장 프로그램 API 접근 가능 여부 확인 + if (!chrome || !chrome.storage || !chrome.storage.local) { + alert('Chrome 확장 프로그램 API에 접근할 수 없습니다.'); + return; + } + + // 안전한 스토리지 정리 + await new Promise((resolve, reject) => { + try { + chrome.storage.local.clear(() => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + } catch (error) { + reject(error); + } + }); + + // 로컬 스토리지도 정리 + if (typeof localStorage !== 'undefined') { + localStorage.clear(); + } + + alert('모든 데이터가 삭제되었습니다. 페이지를 새로고침합니다.'); + location.reload(); + } catch (error) { + console.error('스토리지 정리 오류:', error); + alert('데이터 삭제 중 오류가 발생했습니다: ' + error.message); + } + } + }); + } + } + + // 현재 사용자 정보 로드 + async loadCurrentUser() { + try { + this.debugLog('현재 사용자 정보 로드 시작'); + + // 현재 사용자 정보 가져오기 + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + mode: 'cors', + credentials: 'omit' + }); + + if (!authRes.ok) { + throw new Error('사용자 인증 실패'); + } + + const authUser = await authRes.json(); + + // 사용자 닉네임 가져오기 + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id,nickname&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + mode: 'cors', + credentials: 'omit' + }); + + if (!userRes.ok) { + throw new Error('사용자 정보 조회 실패'); + } + + const userData = await userRes.json(); + if (!userData || userData.length === 0) { + throw new Error('사용자 데이터를 찾을 수 없습니다'); + } + + this.currentUser = { + id: userData[0].id, + nickname: userData[0].nickname || authUser.email, + email: authUser.email + }; + + this.debugLog('현재 사용자 정보 로드 완료', this.currentUser); + + // UI에 사용자 정보 표시 (헤더) + const currentUserElement = document.getElementById('current-user'); + if (currentUserElement) { + currentUserElement.innerHTML = `👤 ${this.currentUser.nickname}`; + } + + // UI에 사용자 정보 표시 (모달) + const modalCurrentUserElement = document.getElementById('modal-current-user'); + if (modalCurrentUserElement) { + modalCurrentUserElement.innerHTML = `👤 ${this.currentUser.nickname}`; + modalCurrentUserElement.style.color = '#2c3e50'; + modalCurrentUserElement.style.fontWeight = 'bold'; + } + + } catch (error) { + this.debugLog('현재 사용자 정보 로드 실패', { error: error.message }); + + // 에러 상태 표시 + const currentUserElement = document.getElementById('current-user'); + if (currentUserElement) { + currentUserElement.innerHTML = '👤 사용자 정보 로드 실패'; + currentUserElement.style.color = '#e74c3c'; + } + + const modalCurrentUserElement = document.getElementById('modal-current-user'); + if (modalCurrentUserElement) { + modalCurrentUserElement.innerHTML = '❌ 사용자 정보를 불러올 수 없습니다'; + modalCurrentUserElement.style.color = '#e74c3c'; + } + + throw error; + } + } + + // 타냐대장경 등록 모달 이벤트 등록 + attachAddSayingEvents() { + this.debugLog('타냐대장경 등록 모달 이벤트 등록 시작'); + + const addBtn = document.getElementById('add-saying-btn'); + const modal = document.getElementById('add-saying-modal'); + const closeBtn = document.getElementById('modal-close'); + const cancelBtn = document.getElementById('cancel-btn'); + const submitBtn = document.getElementById('submit-btn'); + const form = document.getElementById('saying-form'); + + // 모달 열기 + if (addBtn) { + addBtn.addEventListener('click', () => { + this.debugLog('타냐대장경 등록 모달 열기'); + modal.style.display = 'flex'; + document.getElementById('saying-title').focus(); + }); + } + + // 모달 닫기 + const closeModal = () => { + this.debugLog('타냐대장경 등록 모달 닫기'); + modal.style.display = 'none'; + form.reset(); + this.resetPreviewTabs(); + }; + + if (closeBtn) { + closeBtn.addEventListener('click', closeModal); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', closeModal); + } + + // 모달 외부 클릭 시 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeModal(); + } + }); + + // ESC 키로 모달 닫기 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.style.display === 'flex') { + closeModal(); + } + }); + + // 마크다운 미리보기 탭 이벤트 + this.attachPreviewTabEvents(); + + // 폼 제출 이벤트 + if (form) { + form.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleSayingSubmit(); + }); + } + + if (submitBtn) { + submitBtn.addEventListener('click', (e) => { + e.preventDefault(); + this.handleSayingSubmit(); + }); + } + + this.debugLog('타냐대장경 등록 모달 이벤트 등록 완료'); + } + + // 마크다운 미리보기 탭 이벤트 등록 + attachPreviewTabEvents() { + const previewTabs = document.querySelectorAll('.preview-tab'); + const contentTextarea = document.getElementById('saying-content'); + const previewDiv = document.getElementById('markdown-preview'); + + previewTabs.forEach(tab => { + tab.addEventListener('click', () => { + // 모든 탭에서 active 클래스 제거 + previewTabs.forEach(t => t.classList.remove('active')); + + // 클릭된 탭에 active 클래스 추가 + tab.classList.add('active'); + + const tabType = tab.getAttribute('data-tab'); + + if (tabType === 'edit') { + // 편집 모드 + contentTextarea.style.display = 'block'; + previewDiv.style.display = 'none'; + } else if (tabType === 'preview') { + // 미리보기 모드 + contentTextarea.style.display = 'none'; + previewDiv.style.display = 'block'; + + // 마크다운 렌더링 + this.renderMarkdownPreview(); + } + }); + }); + + // 실시간 미리보기 업데이트 + contentTextarea.addEventListener('input', () => { + const activeTab = document.querySelector('.preview-tab.active'); + if (activeTab && activeTab.getAttribute('data-tab') === 'preview') { + // 미리보기 탭이 활성화된 경우 실시간 업데이트 + this.renderMarkdownPreview(); + } + }); + } + + // 마크다운 미리보기 렌더링 (비동기) + async renderMarkdownPreview() { + const contentTextarea = document.getElementById('saying-content'); + const previewDiv = document.getElementById('markdown-preview'); + + if (!contentTextarea || !previewDiv) { + console.error('❌ 미리보기 요소를 찾을 수 없습니다'); + return; + } + + const content = contentTextarea.value.trim(); + + if (!content) { + previewDiv.innerHTML = '
    미리보기할 내용을 입력해주세요
    '; + return; + } + + // 로딩 상태 표시 + previewDiv.innerHTML = '
    미리보기 생성 중...
    '; + + try { + // 마크다운을 HTML로 변환 + const htmlContent = await this.convertMarkdownToHtml(content); + + // 변환된 HTML을 미리보기에 표시 + previewDiv.innerHTML = htmlContent || '
    내용을 변환할 수 없습니다
    '; + + console.log('✅ 마크다운 미리보기 렌더링 완료'); + } catch (error) { + console.error('❌ 마크다운 미리보기 렌더링 실패:', error); + previewDiv.innerHTML = '
    미리보기 생성 중 오류가 발생했습니다
    '; + } + } + + // 타냐대장경 제출 처리 + async handleSayingSubmit() { + this.debugLog('타냐대장경 제출 처리 시작'); + + const title = document.getElementById('saying-title').value.trim(); + const category = document.getElementById('saying-category').value; + const target = document.getElementById('saying-target').value; + const submitBtn = document.getElementById('submit-btn'); + + // 블록 에디터 데이터 가져오기 + let content = ''; + let contentBlocks = null; + + if (window.blockEditor && window.blockEditor.blocks.length > 0) { + // 블록 에디터 사용 시 + const blockData = window.blockEditor.getContentForSave(); + content = blockData.legacy_text; // 기존 텍스트 필드용 (하위 호환성) + contentBlocks = blockData.blocks; // JSONB 필드용 + + // 블록이 비어있는지 확인 + const hasContent = contentBlocks.some(block => { + if (block.type === 'text') { + return block.content && block.content.trim() !== ''; + } else if (block.type === 'image') { + return block.content !== null; + } + return false; + }); + + if (!hasContent) { + alert('❌ 타냐대장경 내용을 입력해주세요.'); + return; + } + } else { + // 기존 텍스트 에디터 사용 시 (하위 호환성) + content = document.getElementById('saying-content').value.trim(); + if (!content) { + alert('❌ 타냐대장경 내용을 입력해주세요.'); + document.getElementById('saying-content').focus(); + return; + } + } + + // 유효성 검사 + if (!title) { + alert('❌ 타냐대장경 제목을 입력해주세요.'); + document.getElementById('saying-title').focus(); + return; + } + + if (!category) { + alert('❌ 카테고리를 선택해주세요.'); + document.getElementById('saying-category').focus(); + return; + } + + if (!target) { + alert('❌ 타겟을 선택해주세요.'); + document.getElementById('saying-target').focus(); + return; + } + + if (!this.currentUser) { + alert('❌ 사용자 정보를 불러올 수 없습니다. 페이지를 새로고침해주세요.'); + return; + } + + try { + // 버튼 상태 변경 + const originalText = submitBtn.textContent; + submitBtn.textContent = '🔄 등록 중...'; + submitBtn.disabled = true; + + this.debugLog('타냐대장경 등록 API 호출', { + title, + contentLength: content.length, + hasBlocks: contentBlocks !== null, + blocksCount: contentBlocks ? contentBlocks.length : 0, + category, + target, + userId: this.currentUser.id, + userNickname: this.currentUser.nickname + }); + + // 타냐대장경 데이터 생성 (register 필드 사용) + const sayingData = { + saying_title: title, + saying: content, // 기존 텍스트 필드 (하위 호환성) + cat: category, + target: target, + register: this.currentUser.nickname, // 등록자 필드명 변경 + user_id: this.currentUser.id, // RLS 정책을 위한 사용자 ID 추가 + admin_approval: false, // 기본적으로 승인 대기 상태 + created_at: new Date().toISOString() + }; + + // 블록 데이터가 있으면 추가 + if (contentBlocks) { + sayingData.content_blocks = contentBlocks; + } + + // API 호출 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/tanya_sayings`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(sayingData) + }); + + this.debugLog('타냐대장경 등록 API 응답', { + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + + if (!response.ok) { + const errorDetail = await response.text(); + this.debugLog('타냐대장경 등록 실패', { + status: response.status, + error: errorDetail + }); + throw new Error(`타냐대장경 등록 실패 (${response.status}: ${response.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('타냐대장경 등록 성공'); + + // 성공 팝업 표시 + this.showSuccessPopup(title, category, target); + + // 모달 닫기 + document.getElementById('add-saying-modal').style.display = 'none'; + document.getElementById('saying-form').reset(); + this.resetPreviewTabs(); + + // 블록 에디터 초기화 + if (window.blockEditor) { + window.blockEditor.loadFromData([]); + } + + // 타냐대장경 목록 새로고침 (승인된 타냐대장경만 표시되므로 새로 등록한 타냐대장경은 보이지 않음) + await this.loadSayings(); + + } catch (error) { + this.debugLog('타냐대장경 등록 실패', { error: error.message }); + alert(`❌ 타냐대장경 등록 중 오류가 발생했습니다:\n${error.message}`); + } finally { + // 버튼 상태 복원 + submitBtn.textContent = '📝 등록하기'; + submitBtn.disabled = false; + } + } + + // 성공 팝업 표시 + showSuccessPopup(title, category, target) { + const popup = document.createElement('div'); + popup.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 30px; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + z-index: 2000; + text-align: center; + min-width: 400px; + border: 3px solid #28a745; + `; + + popup.innerHTML = ` +
    🎉
    +

    타냐대장경 등록 완료!

    +
    +
    제목: ${title}
    +
    카테고리: ${category}
    +
    타겟: ${target}
    +
    등록자: ${this.currentUser.nickname}
    +
    +
    + 📋 안내사항
    + 등록된 타냐대장경은 관리자 승인 후 목록에 표시됩니다. +
    + + `; + + // 팝업을 body에 추가 + document.body.appendChild(popup); + + // 확인 버튼 이벤트 + const closeBtn = popup.querySelector('#close-popup'); + closeBtn.addEventListener('click', () => { + document.body.removeChild(popup); + }); + + // 3초 후 자동으로 닫기 + setTimeout(() => { + if (document.body.contains(popup)) { + document.body.removeChild(popup); + } + }, 5000); + + this.debugLog('성공 팝업 표시 완료', { title, category, target }); + } + + // 카테고리와 타겟 옵션을 백엔드에서 로드 + async loadCategoryAndTargetOptions() { + this.debugLog('카테고리와 타겟 옵션 로드 시작'); + + try { + // 카테고리 데이터 가져오기 + this.debugLog('카테고리 데이터 요청 중...'); + const categoryResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/sayings_cat?select=saying_cat&order=saying_cat.asc`, { + headers: { + 'apikey': this.SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${this.SUPABASE_ANON_KEY}`, + 'Content-Type': 'application/json' + } + }); + + // 타겟 데이터 가져오기 + this.debugLog('타겟 데이터 요청 중...'); + const targetResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/sayings_target?select=target&order=target.asc`, { + headers: { + 'apikey': this.SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${this.SUPABASE_ANON_KEY}`, + 'Content-Type': 'application/json' + } + }); + + // 카테고리 응답 처리 + if (categoryResponse.ok) { + const categoryData = await categoryResponse.json(); + this.debugLog('카테고리 데이터 로드 성공', { count: categoryData.length }); + + if (categoryData.length === 0) { + throw new Error('카테고리 데이터가 비어있습니다. sayings_cat 테이블을 확인해주세요.'); + } + + // 카테고리 드롭다운 업데이트 + this.updateCategoryDropdowns(categoryData); + } else { + const errorText = await categoryResponse.text(); + this.debugLog('카테고리 데이터 로드 실패', { + status: categoryResponse.status, + statusText: categoryResponse.statusText, + error: errorText + }); + throw new Error(`카테고리 데이터 로드 실패: ${categoryResponse.status} ${categoryResponse.statusText}`); + } + + // 타겟 응답 처리 + if (targetResponse.ok) { + const targetData = await targetResponse.json(); + this.debugLog('타겟 데이터 로드 성공', { count: targetData.length }); + + if (targetData.length === 0) { + throw new Error('타겟 데이터가 비어있습니다. sayings_target 테이블을 확인해주세요.'); + } + + // 타겟 드롭다운 업데이트 + this.updateTargetDropdowns(targetData); + } else { + const errorText = await targetResponse.text(); + this.debugLog('타겟 데이터 로드 실패', { + status: targetResponse.status, + statusText: targetResponse.statusText, + error: errorText + }); + throw new Error(`타겟 데이터 로드 실패: ${targetResponse.status} ${targetResponse.statusText}`); + } + + this.debugLog('카테고리와 타겟 옵션 로드 완료'); + + } catch (error) { + this.debugLog('카테고리/타겟 옵션 로드 중 오류 발생', { error: error.message }); + + // 에러를 다시 던져서 앱 초기화를 중단 + throw new Error(`백엔드에서 드롭다운 데이터를 가져올 수 없습니다: ${error.message}`); + } + } + + // 카테고리 드롭다운 업데이트 + updateCategoryDropdowns(categoryData) { + const categorySelect = document.getElementById('saying-category'); + if (!categorySelect) return; + + // 기존 옵션 제거 (첫 번째 "선택하세요" 옵션 제외) + while (categorySelect.children.length > 1) { + categorySelect.removeChild(categorySelect.lastChild); + } + + // 새 카테고리 옵션 추가 + categoryData.forEach(item => { + const option = document.createElement('option'); + option.value = item.saying_cat; + option.textContent = item.saying_cat; + categorySelect.appendChild(option); + }); + + // "아무거나" 기본값 설정 + const defaultCategory = categoryData.find(item => item.saying_cat === '아무거나'); + if (defaultCategory) { + categorySelect.value = defaultCategory.saying_cat; + this.debugLog('카테고리 기본값 설정', { 기본값: defaultCategory.saying_cat }); + } + + // 필터 드롭다운도 업데이트 + this.updateFilterCategoryOptions(categoryData); + + this.debugLog('카테고리 드롭다운 업데이트 완료', { + 옵션개수: categorySelect.children.length - 1, + 기본값: categorySelect.value || '없음' + }); + } + + // 타겟 드롭다운 업데이트 + updateTargetDropdowns(targetData) { + const targetSelect = document.getElementById('saying-target'); + if (!targetSelect) return; + + // 기존 옵션 제거 (첫 번째 "선택하세요" 옵션 제외) + while (targetSelect.children.length > 1) { + targetSelect.removeChild(targetSelect.lastChild); + } + + // 새 타겟 옵션 추가 + targetData.forEach(item => { + const option = document.createElement('option'); + option.value = item.target; + option.textContent = item.target; + targetSelect.appendChild(option); + }); + + // "누구나" 기본값 설정 + const defaultTarget = targetData.find(item => item.target === '누구나'); + if (defaultTarget) { + targetSelect.value = defaultTarget.target; + this.debugLog('타겟 기본값 설정', { 기본값: defaultTarget.target }); + } + + // 필터 드롭다운도 업데이트 + this.updateFilterTargetOptions(targetData); + + this.debugLog('타겟 드롭다운 업데이트 완료', { + 옵션개수: targetSelect.children.length - 1, + 기본값: targetSelect.value || '없음' + }); + } + + // 필터용 카테고리 옵션 업데이트 + updateFilterCategoryOptions(categoryData) { + const categoryFilter = document.getElementById('category-filter'); + if (!categoryFilter) return; + + // 기존 옵션 제거 (첫 번째 "모든 카테고리" 옵션 제외) + while (categoryFilter.children.length > 1) { + categoryFilter.removeChild(categoryFilter.lastChild); + } + + // 새 카테고리 옵션 추가 + categoryData.forEach(item => { + const option = document.createElement('option'); + option.value = item.saying_cat; + option.textContent = item.saying_cat; + categoryFilter.appendChild(option); + }); + + this.debugLog('필터용 카테고리 옵션 업데이트 완료', { + 옵션개수: categoryFilter.children.length - 1 + }); + } + + // 필터용 타겟 옵션 업데이트 + updateFilterTargetOptions(targetData) { + const targetFilter = document.getElementById('target-filter'); + if (!targetFilter) return; + + // 기존 옵션 제거 (첫 번째 "모든 타겟" 옵션 제외) + while (targetFilter.children.length > 1) { + targetFilter.removeChild(targetFilter.lastChild); + } + + // 새 타겟 옵션 추가 + targetData.forEach(item => { + const option = document.createElement('option'); + option.value = item.target; + option.textContent = item.target; + targetFilter.appendChild(option); + }); + + this.debugLog('필터용 타겟 옵션 업데이트 완료', { + 옵션개수: targetFilter.children.length - 1 + }); + } + + // 통계 계산 + calculateStats() { + const stats = { + total: this.filteredSayings.length, + byCategory: {}, + byTarget: {} + }; + + this.filteredSayings.forEach(saying => { + // 카테고리별 통계 + const category = saying.cat || '기타'; + stats.byCategory[category] = (stats.byCategory[category] || 0) + 1; + + // 타겟별 통계 + const target = saying.target || '기타'; + stats.byTarget[target] = (stats.byTarget[target] || 0) + 1; + }); + + return stats; + } + + // 필터 적용 + async applyFilters() { + this.debugLog('필터 적용 시작'); + + const categoryFilter = document.getElementById('category-filter').value; + const targetFilter = document.getElementById('target-filter').value; + const dateFilter = document.getElementById('date-filter').value; + const searchText = document.getElementById('search-input').value.toLowerCase().trim(); + + this.debugLog('필터 조건', { + category: categoryFilter, + target: targetFilter, + date: dateFilter, + search: searchText + }); + + this.filteredSayings = this.allSayings.filter(saying => { + // 카테고리 필터 + if (categoryFilter && categoryFilter !== 'all') { + if (saying.cat !== categoryFilter) return false; + } + + // 타겟 필터 + if (targetFilter && targetFilter !== 'all') { + if (saying.target !== targetFilter) return false; + } + + // 날짜 필터 + if (dateFilter && dateFilter !== 'all') { + const sayingDate = new Date(saying.created_at); + const now = new Date(); + + switch (dateFilter) { + case 'today': + if (sayingDate.toDateString() !== now.toDateString()) return false; + break; + case 'week': + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + if (sayingDate < weekAgo) return false; + break; + case 'month': + const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + if (sayingDate < monthAgo) return false; + break; + } + } + + // 검색 텍스트 필터 + if (searchText) { + const searchableText = [ + saying.saying_title || '', + saying.saying || '', + saying.register || saying.user_name || '', + saying.cat || '', + saying.target || '' + ].join(' ').toLowerCase(); + + if (!searchableText.includes(searchText)) return false; + } + + return true; + }); + + this.debugLog('필터 적용 완료', { + 원본개수: this.allSayings.length, + 필터링된개수: this.filteredSayings.length + }); + + await this.displaySayings(); + } + + // 필터 이벤트 연결 + attachFilterEvents() { + this.debugLog('필터 이벤트 연결 시작'); + + // 카테고리 필터 + const categoryFilter = document.getElementById('category-filter'); + if (categoryFilter) { + categoryFilter.addEventListener('change', async () => await this.applyFilters()); + } + + // 타겟 필터 + const targetFilter = document.getElementById('target-filter'); + if (targetFilter) { + targetFilter.addEventListener('change', async () => await this.applyFilters()); + } + + // 날짜 필터 + const dateFilter = document.getElementById('date-filter'); + if (dateFilter) { + dateFilter.addEventListener('change', async () => await this.applyFilters()); + } + + // 검색 입력 + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.addEventListener('input', () => { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(async () => await this.applyFilters(), 300); + }); + } + + // 필터 초기화 버튼 + const resetButton = document.getElementById('reset-filters'); + if (resetButton) { + resetButton.addEventListener('click', async () => await this.resetFilters()); + } + + this.debugLog('필터 이벤트 연결 완료'); + } + + // 필터 초기화 + async resetFilters() { + this.debugLog('필터 초기화 시작'); + + // 모든 필터 초기화 + const categoryFilter = document.getElementById('category-filter'); + const targetFilter = document.getElementById('target-filter'); + const dateFilter = document.getElementById('date-filter'); + const searchInput = document.getElementById('search-input'); + + if (categoryFilter) categoryFilter.value = 'all'; + if (targetFilter) targetFilter.value = 'all'; + if (dateFilter) dateFilter.value = 'all'; + if (searchInput) searchInput.value = ''; + + // 필터 적용 + await this.applyFilters(); + + this.debugLog('필터 초기화 완료'); + } + + // 미리보기 탭 초기화 + resetPreviewTabs() { + const previewTabs = document.querySelectorAll('.preview-tab'); + const contentTextarea = document.getElementById('saying-content'); + const previewDiv = document.getElementById('markdown-preview'); + + // 모든 탭에서 active 클래스 제거 + previewTabs.forEach(tab => tab.classList.remove('active')); + + // 편집 탭을 활성화 + const editTab = document.querySelector('.preview-tab[data-tab="edit"]'); + if (editTab) { + editTab.classList.add('active'); + } + + // 편집 모드로 전환 + if (contentTextarea) { + contentTextarea.style.display = 'block'; + } + if (previewDiv) { + previewDiv.style.display = 'none'; + previewDiv.innerHTML = ''; + } + + this.debugLog('미리보기 탭 초기화 완료'); + } + + // 새 타냐대장경 감지 시작 (기존 코드 제거하고 백그라운드 연동 방식으로 교체) + startNewSayingsMonitoring() { + this.debugLog('새 타냐대장경 감지 시작'); + + // 기존 인터벌이 있으면 정리 + if (this.newSayingsCheckInterval) { + clearInterval(this.newSayingsCheckInterval); + } + + // 새 타냐대장경 감지 시작 (백그라운드 연동) + this.startNewSayingsDetection(); + } + + // 새 타냐대장경 감지 체크 (제거됨 - 백그라운드에서 처리) + // async checkForNewSayings() { ... } - 이 함수는 제거됨 + + // 새 타냐대장경 모달 표시 (카운트다운 포함) + showNewSayingsModal(newSayings) { + console.log(`[SayingsManager] 새 타냐대장경 모달 표시: ${newSayings.length}개`); + + // 기존 모달이 있으면 제거 + const existingModal = document.getElementById('newSayingsModal'); + if (existingModal) { + existingModal.remove(); + } + + // 모달 HTML 생성 + const modalHtml = ` + + + + `; + + // 모달을 body에 추가 + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // 카운트다운 시작 + let countdown = 10; + const countdownElement = document.getElementById('countdownTimer'); + const countdownInterval = setInterval(() => { + countdown--; + if (countdown > 0) { + countdownElement.textContent = `${countdown}초 후 자동 닫힘`; + } else { + clearInterval(countdownInterval); + this.closeNewSayingsModal(); + } + }, 1000); + + // 이벤트 리스너 추가 + const modal = document.getElementById('newSayingsModal'); + const dismissBtn = document.getElementById('dismissNewSayings'); + const viewBtn = document.getElementById('viewNewSayings'); + + // 오버레이 클릭으로 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + clearInterval(countdownInterval); + this.closeNewSayingsModal(); + } + }); + + // 나중에 버튼 + dismissBtn.addEventListener('click', () => { + clearInterval(countdownInterval); + this.closeNewSayingsModal(); + }); + + // 새 타냐대장경 보기 버튼 + viewBtn.addEventListener('click', () => { + clearInterval(countdownInterval); + this.closeNewSayingsModal(); + this.loadSayings(); // 타냐대장경 목록 새로고침 + }); + + // 모달 참조 저장 (카운트다운 정리용) + this.currentModalCountdown = countdownInterval; + } + + // 새 타냐대장경 모달 닫기 + closeNewSayingsModal() { + const modal = document.getElementById('newSayingsModal'); + if (modal) { + modal.style.animation = 'fadeOut 0.3s ease-out'; + setTimeout(() => { + modal.remove(); + }, 5000); + } + + // 카운트다운 정리 + if (this.currentModalCountdown) { + clearInterval(this.currentModalCountdown); + this.currentModalCountdown = null; + } + } + + // 새 타냐대장경 감지 정리 + cleanup() { + console.log('[SayingsManager] 새 타냐대장경 감지 정리'); + + if (this.newSayingsCheckInterval) { + clearInterval(this.newSayingsCheckInterval); + this.newSayingsCheckInterval = null; + } + + if (this.currentModalCountdown) { + clearInterval(this.currentModalCountdown); + this.currentModalCountdown = null; + } + + // 모달 제거 + const modal = document.getElementById('newSayingsModal'); + if (modal) { + modal.remove(); + } + } + + // 새 타냐대장경 감지 시작 + startNewSayingsDetection() { + console.log('[SayingsManager] 새 타냐대장경 감지 시작'); + + // 기존 타이머가 있으면 정리 + if (this.newSayingsTimer) { + clearInterval(this.newSayingsTimer); + } + + // 페이지 로드 시 백그라운드에서 감지된 새 타냐대장경 확인 + this.checkBackgroundNewSayings(); + + // 1분마다 새 타냐대장경 확인 (백그라운드와 동기화) + this.newSayingsTimer = setInterval(() => { + this.checkBackgroundNewSayings(); + }, 60000); // 1분 + + console.log('[SayingsManager] 새 타냐대장경 감지 타이머 설정 완료 (1분 간격)'); + } + + // 백그라운드에서 감지된 새 타냐대장경 확인 + async checkBackgroundNewSayings() { + try { + console.log('[SayingsManager] 백그라운드 새 타냐대장경 확인 시작'); + + // Chrome 확장 프로그램 API 접근 가능 여부 확인 + if (!chrome || !chrome.storage || !chrome.storage.local) { + console.error('[SayingsManager] Chrome 확장 프로그램 API에 접근할 수 없습니다.'); + return; + } + + // Promise 기반으로 안전하게 스토리지 접근 + const storageData = await new Promise((resolve, reject) => { + try { + chrome.storage.local.get(['hasNewSayings', 'pendingNewSayings'], (result) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(result); + } + }); + } catch (error) { + reject(error); + } + }); + + const { hasNewSayings, pendingNewSayings } = storageData; + + console.log('[SayingsManager] 스토리지 데이터 확인:', { + hasNewSayings: !!hasNewSayings, + pendingCount: pendingNewSayings ? pendingNewSayings.length : 0 + }); + + if (hasNewSayings && pendingNewSayings && pendingNewSayings.length > 0) { + console.log(`[SayingsManager] 백그라운드에서 감지된 새 타냐대장경: ${pendingNewSayings.length}개`); + this.showNewSayingsModal(pendingNewSayings); + + // 표시 후 플래그 제거 (안전한 방식) + try { + await new Promise((resolve, reject) => { + chrome.storage.local.remove(['hasNewSayings', 'pendingNewSayings'], () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + console.log('[SayingsManager] 새 타냐대장경 플래그 정리 완료'); + resolve(); + } + }); + }); + } catch (removeError) { + console.error('[SayingsManager] 새 타냐대장경 플래그 정리 실패:', removeError); + } + } else { + console.log('[SayingsManager] 백그라운드에서 감지된 새 타냐대장경이 없습니다.'); + } + + } catch (error) { + console.error('[SayingsManager] 백그라운드 새 타냐대장경 확인 오류:', error); + + // 오류 유형별 처리 + if (error.message.includes('Could not establish connection')) { + console.error('[SayingsManager] Chrome 확장 프로그램 연결 오류 - 확장 프로그램을 다시 로드하세요.'); + } else if (error.message.includes('Receiving end does not exist')) { + console.error('[SayingsManager] 메시지 수신자가 존재하지 않음 - 페이지를 새로고침하세요.'); + } else if (error.message.includes('Extension context invalidated')) { + console.error('[SayingsManager] 확장 프로그램 컨텍스트가 무효화됨 - 확장 프로그램을 다시 로드하세요.'); + } + } + } + + // 상세 보기 모달 열기 + openDetailModal(saying) { + this.debugLog('상세 보기 모달 열기', saying); + + const modal = document.getElementById('view-saying-modal'); + if (!modal) { + console.error('상세 보기 모달을 찾을 수 없습니다'); + return; + } + + this.currentEditingSaying = saying; + + // 현재 사용자와 작성자 비교 + const currentUser = this.currentUser?.user?.email || this.currentUser?.email; + const isAuthor = currentUser && (saying.register === currentUser || saying.user_name === currentUser); + + this.debugLog('권한 확인', { + currentUser, + sayingAuthor: saying.register || saying.user_name, + isAuthor + }); + + // 모달 데이터 설정 + this.populateDetailModal(saying, isAuthor); + + // 모달 표시 + modal.style.display = 'block'; + + // 모달 이벤트 바인딩 + this.attachDetailModalEvents(saying, isAuthor); + } + + // 상세 보기 모달 데이터 채우기 + populateDetailModal(saying, isAuthor) { + const viewMode = document.getElementById('view-mode'); + const editMode = document.getElementById('edit-mode'); + const editButton = document.getElementById('edit-mode-btn'); + const deleteButton = document.getElementById('delete-saying-btn'); + + // 보기 모드 데이터 설정 + document.getElementById('view-title').textContent = saying.saying_title || '제목 없음'; + + // 콘텐츠 렌더링 + const viewContent = document.getElementById('view-content'); + if (saying.content_blocks && Array.isArray(saying.content_blocks) && saying.content_blocks.length > 0) { + viewContent.innerHTML = this.renderContentBlocks(saying.content_blocks); + } else { + viewContent.innerHTML = this.basicMarkdownToHtml(saying.saying || ''); + } + + // 메타 정보 설정 + const viewMeta = document.getElementById('view-meta'); + viewMeta.innerHTML = ` + ${this.escapeHtml(saying.cat || '기타')} + ${this.escapeHtml(saying.target || '기타')} + `; + + // 작성자 정보 설정 + const authorName = saying.register || saying.user_name || '알 수 없음'; + const createdAt = new Date(saying.created_at).toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + + document.getElementById('view-author-info').innerHTML = ` +
    +
    ${authorName.charAt(0).toUpperCase()}
    +
    +
    ${this.escapeHtml(authorName)}
    +
    ${createdAt}
    +
    +
    + `; + + // 편집 모드 데이터 설정 (작성자인 경우에만) + if (isAuthor) { + document.getElementById('edit-saying-title').value = saying.saying_title || ''; + document.getElementById('edit-saying-content').value = saying.saying || ''; + document.getElementById('edit-saying-category').value = saying.cat || ''; + document.getElementById('edit-saying-target').value = saying.target || ''; + } + + // 버튼 표시/숨김 제어 + const viewModeButtons = document.getElementById('view-mode-buttons'); + const editModeButtons = document.getElementById('edit-mode-buttons'); + + if (isAuthor) { + editButton.style.display = 'inline-block'; + deleteButton.style.display = 'inline-block'; + viewMode.style.display = 'block'; + editMode.style.display = 'none'; + viewModeButtons.style.display = 'flex'; + editModeButtons.style.display = 'none'; + } else { + editButton.style.display = 'none'; + deleteButton.style.display = 'none'; + viewMode.style.display = 'block'; + editMode.style.display = 'none'; + viewModeButtons.style.display = 'flex'; + editModeButtons.style.display = 'none'; + } + } + + // 상세 보기 모달 이벤트 바인딩 + attachDetailModalEvents(saying, isAuthor) { + // 닫기 버튼 + const closeBtn = document.getElementById('view-modal-close'); + const viewCloseBtn = document.getElementById('view-close-btn'); + const modal = document.getElementById('view-saying-modal'); + + const closeModal = () => { + modal.style.display = 'none'; + this.currentEditingSaying = null; + }; + + // 기존 이벤트 리스너 제거 후 새로 추가 + if (closeBtn) { + closeBtn.replaceWith(closeBtn.cloneNode(true)); + document.getElementById('view-modal-close').addEventListener('click', closeModal); + } + + if (viewCloseBtn) { + viewCloseBtn.replaceWith(viewCloseBtn.cloneNode(true)); + document.getElementById('view-close-btn').addEventListener('click', closeModal); + } + + // 모달 배경 클릭 시 닫기 + modal.onclick = (e) => { + if (e.target === modal) { + closeModal(); + } + }; + + // ESC 키로 모달 닫기 + const handleEsc = (e) => { + if (e.key === 'Escape') { + closeModal(); + document.removeEventListener('keydown', handleEsc); + } + }; + document.addEventListener('keydown', handleEsc); + + if (isAuthor) { + // 편집 버튼 + const editBtn = document.getElementById('edit-mode-btn'); + if (editBtn) { + editBtn.replaceWith(editBtn.cloneNode(true)); + document.getElementById('edit-mode-btn').addEventListener('click', () => { + this.switchToEditMode(); + }); + } + + // 취소 버튼 + const cancelBtn = document.getElementById('edit-cancel-btn'); + if (cancelBtn) { + cancelBtn.replaceWith(cancelBtn.cloneNode(true)); + document.getElementById('edit-cancel-btn').addEventListener('click', () => { + this.switchToViewMode(); + }); + } + + // 저장 버튼 + const saveBtn = document.getElementById('update-saying-btn'); + if (saveBtn) { + saveBtn.replaceWith(saveBtn.cloneNode(true)); + document.getElementById('update-saying-btn').addEventListener('click', () => { + this.saveEditedSaying(); + }); + } + + // 삭제 버튼 + const deleteBtn = document.getElementById('delete-saying-btn'); + if (deleteBtn) { + deleteBtn.replaceWith(deleteBtn.cloneNode(true)); + document.getElementById('delete-saying-btn').addEventListener('click', () => { + this.deleteSaying(saying); + }); + } + } + + // 이미지 클릭 이벤트 추가 (보기 모드에서) - 원본 이미지 사용 + const viewContent = document.getElementById('view-content'); + const images = viewContent.querySelectorAll('.content-image'); + images.forEach(img => { + img.addEventListener('click', () => { + // data-original 속성에서 원본 이미지 URL 가져오기 + const originalSrc = img.dataset.original || img.src; + this.showFullImage(originalSrc); + }); + }); + } + + // 편집 모드로 전환 + switchToEditMode() { + const viewMode = document.getElementById('view-mode'); + const editMode = document.getElementById('edit-mode'); + const viewModeButtons = document.getElementById('view-mode-buttons'); + const editModeButtons = document.getElementById('edit-mode-buttons'); + + viewMode.style.display = 'none'; + editMode.style.display = 'block'; + viewModeButtons.style.display = 'none'; + editModeButtons.style.display = 'flex'; + + this.debugLog('편집 모드로 전환'); + } + + // 보기 모드로 전환 + switchToViewMode() { + const viewMode = document.getElementById('view-mode'); + const editMode = document.getElementById('edit-mode'); + const viewModeButtons = document.getElementById('view-mode-buttons'); + const editModeButtons = document.getElementById('edit-mode-buttons'); + + viewMode.style.display = 'block'; + editMode.style.display = 'none'; + viewModeButtons.style.display = 'flex'; + editModeButtons.style.display = 'none'; + + this.debugLog('보기 모드로 전환'); + } + + // 편집된 타냐대장경 저장 + async saveEditedSaying() { + if (!this.currentEditingSaying) { + console.error('편집 중인 타냐대장경이 없습니다'); + return; + } + + const title = document.getElementById('edit-saying-title').value.trim(); + const content = document.getElementById('edit-saying-content').value.trim(); + const category = document.getElementById('edit-saying-category').value.trim(); + const target = document.getElementById('edit-saying-target').value.trim(); + + if (!title || !content) { + alert('제목과 내용을 모두 입력해주세요.'); + return; + } + + try { + const response = await fetch(`${this.config.supabaseUrl}/rest/v1/tanya_sayings?id=eq.${this.currentEditingSaying.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'apikey': this.config.supabaseKey, + 'Authorization': `Bearer ${this.config.supabaseKey}`, + 'Prefer': 'return=representation' + }, + body: JSON.stringify({ + saying_title: title, + saying: content, + cat: category, + target: target + }) + }); + + if (response.ok) { + alert('타냐대장경이 성공적으로 수정되었습니다.'); + + // 모달 닫기 + document.getElementById('view-saying-modal').style.display = 'none'; + this.currentEditingSaying = null; + + // 타냐대장경 목록 새로고침 + await this.loadSayings(); + } else { + throw new Error('타냐대장경 수정에 실패했습니다.'); + } + } catch (error) { + console.error('타냐대장경 수정 오류:', error); + alert('타냐대장경 수정 중 오류가 발생했습니다.'); + } + } + + // 타냐대장경 삭제 + async deleteSaying(saying) { + if (!confirm('정말로 이 타냐대장경을 삭제하시겠습니까?')) { + return; + } + + try { + const response = await fetch(`${this.config.supabaseUrl}/rest/v1/tanya_sayings?id=eq.${saying.id}`, { + method: 'DELETE', + headers: { + 'apikey': this.config.supabaseKey, + 'Authorization': `Bearer ${this.config.supabaseKey}` + } + }); + + if (response.ok) { + alert('타냐대장경이 성공적으로 삭제되었습니다.'); + + // 모달 닫기 + document.getElementById('view-saying-modal').style.display = 'none'; + this.currentEditingSaying = null; + + // 타냐대장경 목록 새로고침 + await this.loadSayings(); + } else { + throw new Error('타냐대장경 삭제에 실패했습니다.'); + } + } catch (error) { + console.error('타냐대장경 삭제 오류:', error); + alert('타냐대장경 삭제 중 오류가 발생했습니다.'); + } + } +} + +// 페이지 로드 시 초기화 +document.addEventListener('DOMContentLoaded', async () => { + console.log('=== 타냐대장경 관리 페이지 초기화 시작 ==='); + console.log('현재 시간:', new Date().toLocaleString()); + + // chrome.storage에서 설정 확인 + let debugMode = false; // 기본값 + try { + const configData = await chrome.storage.local.get('sayings_config'); + console.log('chrome.storage 설정 확인:', { + hasSayingsConfig: !!configData.sayings_config, + config: configData.sayings_config ? { + hasUrl: !!configData.sayings_config.SUPABASE_URL, + hasKey: !!configData.sayings_config.SUPABASE_ANON_KEY, + hasToken: !!configData.sayings_config.ACCESS_TOKEN, + debugMode: configData.sayings_config.DEBUG_MODE, + timestamp: configData.sayings_config.timestamp + } : null + }); + + // DEBUG_MODE 설정 확인 + if (configData.sayings_config && configData.sayings_config.DEBUG_MODE !== undefined) { + debugMode = configData.sayings_config.DEBUG_MODE; + } + + } catch (error) { + console.error('chrome.storage 설정 확인 실패:', error); + } + + // 디버그 요소 표시/숨김 처리 + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + if (debugMode) { + debugElement.style.display = "block"; + console.log('디버그 모드 활성화 - 디버그 정보 표시'); + } else { + debugElement.style.display = "none"; + console.log('디버그 모드 비활성화 - 디버그 정보 숨김'); + } + } + + // 로그 확인 버튼 이벤트 등록 (DEBUG_MODE일 때만) + const logCheckBtn = document.getElementById('log-check-btn'); + if (logCheckBtn) { + if (debugMode) { + console.log('디버그 모드: 로그 확인 버튼 이벤트 등록'); + logCheckBtn.style.display = "inline-block"; + + logCheckBtn.addEventListener('click', async (e) => { + e.preventDefault(); + console.log('로그 확인 버튼 클릭됨'); + + // 버튼 상태 변경 + const originalText = logCheckBtn.textContent; + logCheckBtn.textContent = '🔄 확인 중...'; + logCheckBtn.disabled = true; + + try { + // SayingsManager가 초기화되었는지 확인 + if (window.sayingsManager && typeof window.sayingsManager.showDebugLogs === 'function') { + await window.sayingsManager.showDebugLogs(); + } else { + console.error('SayingsManager가 초기화되지 않았습니다'); + alert('❌ 관리자가 초기화되지 않았습니다. 페이지를 새로고침해주세요.'); + } + } finally { + // 버튼 상태 복원 + logCheckBtn.textContent = originalText; + logCheckBtn.disabled = false; + } + }); + console.log('로그 확인 버튼 이벤트 등록 완료'); + } else { + console.log('디버그 모드 비활성화: 로그 확인 버튼 숨김'); + logCheckBtn.style.display = "none"; + } + } + + try { + // SayingsManager 초기화 + console.log('SayingsManager 인스턴스 생성 중...'); + window.sayingsManager = new SayingsManager(); + + console.log('SayingsManager 초기화 시작...'); + await window.sayingsManager.initialize(); + + console.log('✅ 타냐대장경 관리 페이지 초기화 완료'); + + } catch (error) { + console.error('❌ 초기화 실패:', error); + + // UI에 오류 표시 + const statsElement = document.getElementById('sayings-stats'); + if (statsElement) { + statsElement.innerHTML = ` +
    + ❌ 초기화 실패
    + ${error.message}
    + 로그인 후 다시 시도해주세요. +
    + `; + } + + const container = document.getElementById("sayings-container"); + if (container) { + container.innerHTML = ` +
    +
    +
    초기화 실패
    +
    ${error.message}
    +
    + `; + } + + // 디버그 정보에도 표시 (DEBUG_MODE일 때만) + if (debugMode) { + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + debugElement.innerHTML = `❌ 초기화 실패: ${error.message}
    `; + debugElement.style.display = "block"; + + // 버튼 이벤트 다시 등록 + const newBtn = document.getElementById('log-check-btn'); + if (newBtn) { + newBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + // 간단한 로그 정보 표시 + const logInfo = `❌ 초기화 실패 상태\n\n오류: ${error.message}\n\n시간: ${new Date().toLocaleString()}`; + alert(logInfo); + }); + } + } + } + } + + console.log('=== DOMContentLoaded 이벤트 처리 완료 ==='); + + // 블록 에디터 초기화 + initializeBlockEditor(); +}); + +// 페이지 언로드 시 정리 +window.addEventListener('beforeunload', () => { + console.log('페이지 언로드 - 새 타냐대장경 감지 기능 정리'); + if (window.sayingsManager) { + window.sayingsManager.cleanup(); + } +}); + +// 전역 함수로 등록 +window.sayingsManager = null; + +// 블록 에디터 클래스 +class BlockEditor { + constructor() { + this.blocks = []; + this.draggedBlock = null; + this.init(); + } + + init() { + this.bindEvents(); + this.addInitialTextBlock(); + } + + bindEvents() { + // 툴바 버튼 이벤트 + document.getElementById('add-text-block')?.addEventListener('click', () => { + this.addTextBlock(); + }); + + document.getElementById('add-image-block')?.addEventListener('click', () => { + this.addImageBlock(); + }); + } + + addInitialTextBlock() { + this.addTextBlock(''); + } + + addTextBlock(content = '') { + const blockId = 'block_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + const block = { + id: blockId, + type: 'text', + content: content + }; + + this.blocks.push(block); + this.renderBlock(block); + this.updateContentBlocksData(); + } + + addImageBlock() { + const blockId = 'block_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + const block = { + id: blockId, + type: 'image', + content: null, + alt: '', + thumbnail: null + }; + + this.blocks.push(block); + this.renderBlock(block); + this.updateContentBlocksData(); + } + + renderBlock(block) { + const container = document.getElementById('content-blocks'); + if (!container) return; + + const blockElement = document.createElement('div'); + blockElement.className = 'content-block'; + blockElement.setAttribute('data-block-id', block.id); + blockElement.draggable = true; + + if (block.type === 'text') { + blockElement.innerHTML = this.getTextBlockHTML(block); + this.bindTextBlockEvents(blockElement, block); + } else if (block.type === 'image') { + blockElement.innerHTML = this.getImageBlockHTML(block); + this.bindImageBlockEvents(blockElement, block); + } + + container.appendChild(blockElement); + } + + getTextBlockHTML(block) { + return ` +
    + + +
    +
    + +
    + `; + } + + getImageBlockHTML(block) { + if (block.content) { + return ` +
    + + +
    +
    + ${block.alt || ''} +
    + 크기: ${this.getImageSize(block.content)} + +
    + +
    + `; + } else { + return ` +
    + + +
    +
    +
    +
    📷
    +
    이미지를 드래그하거나 클릭하여 업로드
    +
    지원 형식: JPG, PNG, GIF (최대 2MB)
    +
    + +
    + `; + } + } + + bindTextBlockEvents(blockElement, block) { + const textarea = blockElement.querySelector('textarea'); + const deleteBtn = blockElement.querySelector('.delete'); + + textarea.addEventListener('input', () => { + block.content = textarea.value; + this.updateContentBlocksData(); + }); + + deleteBtn.addEventListener('click', () => { + this.deleteBlock(block.id); + }); + } + + bindImageBlockEvents(blockElement, block) { + const uploadArea = blockElement.querySelector('.image-upload-area'); + const fileInput = blockElement.querySelector('input[type="file"]'); + const deleteBtn = blockElement.querySelector('.delete'); + const altInput = blockElement.querySelector('.image-alt-input'); + + if (uploadArea) { + uploadArea.addEventListener('click', () => fileInput.click()); + + uploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + uploadArea.classList.add('dragover'); + }); + + uploadArea.addEventListener('dragleave', () => { + uploadArea.classList.remove('dragover'); + }); + + uploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + const files = e.dataTransfer.files; + if (files.length > 0) { + this.handleImageUpload(files[0], block); + } + }); + } + + if (fileInput) { + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + this.handleImageUpload(e.target.files[0], block); + } + }); + } + + if (altInput) { + altInput.addEventListener('input', () => { + block.alt = altInput.value; + this.updateContentBlocksData(); + }); + } + + deleteBtn.addEventListener('click', () => { + this.deleteBlock(block.id); + }); + + // 이미지 클릭 시 전체 화면으로 보기 - 원본 이미지 사용 + const uploadedImage = blockElement.querySelector('.uploaded-image'); + if (uploadedImage) { + uploadedImage.addEventListener('click', () => { + // 원본 이미지(block.content) 사용 + this.showImageModal(block.content); + }); + } + } + + async handleImageUpload(file, block) { + if (!file.type.startsWith('image/')) { + alert('이미지 파일만 업로드 가능합니다.'); + return; + } + + if (file.size > 2 * 1024 * 1024) { + alert('이미지 크기는 2MB 이하여야 합니다.'); + return; + } + + try { + // 원본 이미지 Base64 변환 + const originalBase64 = await this.fileToBase64(file); + + // 썸네일 생성 (표시용) + const thumbnailBase64 = await this.createThumbnail(file, 300, 200); + + block.content = originalBase64; + block.thumbnail = thumbnailBase64; + + // 블록 다시 렌더링 + const blockElement = document.querySelector(`[data-block-id="${block.id}"]`); + blockElement.innerHTML = this.getImageBlockHTML(block); + this.bindImageBlockEvents(blockElement, block); + + this.updateContentBlocksData(); + } catch (error) { + console.error('이미지 업로드 오류:', error); + alert('이미지 업로드에 실패했습니다.'); + } + } + + fileToBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + createThumbnail(file, maxWidth, maxHeight) { + return new Promise((resolve) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.onload = () => { + // 비율 계산 + const ratio = Math.min(maxWidth / img.width, maxHeight / img.height); + const width = img.width * ratio; + const height = img.height * ratio; + + canvas.width = width; + canvas.height = height; + + // 이미지 그리기 + ctx.drawImage(img, 0, 0, width, height); + + // Base64로 변환 (품질 0.7) + resolve(canvas.toDataURL('image/jpeg', 0.7)); + }; + + img.src = URL.createObjectURL(file); + }); + } + + getImageSize(base64) { + const sizeInBytes = Math.round((base64.length * 3) / 4); + if (sizeInBytes < 1024) { + return sizeInBytes + ' B'; + } else if (sizeInBytes < 1024 * 1024) { + return Math.round(sizeInBytes / 1024) + ' KB'; + } else { + return Math.round(sizeInBytes / (1024 * 1024)) + ' MB'; + } + } + + changeImage(blockId) { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + if (e.target.files.length > 0) { + const block = this.blocks.find(b => b.id === blockId); + if (block) { + this.handleImageUpload(e.target.files[0], block); + } + } + }; + input.click(); + } + + showImageModal(imageSrc) { + let modal = document.querySelector('.image-modal'); + if (!modal) { + modal = document.createElement('div'); + modal.className = 'image-modal'; + modal.innerHTML = ` + × + 전체 화면 이미지 + `; + document.body.appendChild(modal); + + modal.querySelector('.image-modal-close').addEventListener('click', () => { + modal.classList.remove('active'); + }); + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.classList.remove('active'); + } + }); + } + + modal.querySelector('img').src = imageSrc; + modal.classList.add('active'); + } + + deleteBlock(blockId) { + this.blocks = this.blocks.filter(block => block.id !== blockId); + const blockElement = document.querySelector(`[data-block-id="${blockId}"]`); + if (blockElement) { + blockElement.remove(); + } + this.updateContentBlocksData(); + } + + updateContentBlocksData() { + // 전역 변수에 저장 + window.currentContentBlocks = this.blocks; + } + + getContentForSave() { + return { + blocks: this.blocks, + legacy_text: this.blocks + .filter(block => block.type === 'text') + .map(block => block.content) + .join('\n\n') + }; + } + + loadFromData(contentBlocks) { + this.blocks = contentBlocks || []; + const container = document.getElementById('content-blocks'); + if (container) { + container.innerHTML = ''; + } + + if (this.blocks.length === 0) { + this.addInitialTextBlock(); + } else { + this.blocks.forEach(block => this.renderBlock(block)); + } + } +} + +// 블록 에디터 초기화 함수 +function initializeBlockEditor() { + if (document.getElementById('content-blocks-editor')) { + window.blockEditor = new BlockEditor(); + } +} \ No newline at end of file diff --git a/wrmc_ext/settings.html b/wrmc_ext/settings.html new file mode 100644 index 0000000..f801582 --- /dev/null +++ b/wrmc_ext/settings.html @@ -0,0 +1,582 @@ + + + + + + 설정 - 내차는언제타냐 통합확장기 + + + +
    +
    +

    ⚙️ 설정

    +

    번역 엔진 및 시간 알람 설정을 관리합니다

    +
    + + +
    + + +
    + +
    + +
    + +
    +
    현재 회원등급: 로딩 중...
    +
    회원 정보를 불러오는 중입니다...
    +
    + + +
    +
    + 📋 + 노출할 번역 엔진 선택 +
    +
    + 체크된 엔진만 멀티번역 결과에 표시됩니다. 회원등급에 따라 일부 엔진은 사용할 수 없습니다. +
    + +
    +
    +
    +
    + 🌐 + Google 번역 + 무료 +
    +
    + 빠른 속도와 다양한 언어 지원을 제공하는 구글의 무료 번역 서비스 +
    +
    + ✓ 100개 이상 언어 지원 ✓ 실시간 번역 ✓ 무료 사용 +
    +
    +
    +
    + +
    +
    +
    + 💾 + MyMemory 번역 + 무료 +
    +
    + 번역 메모리 기반으로 높은 품질의 번역을 제공하는 무료 서비스 +
    +
    + ✓ 번역 메모리 활용 ✓ 높은 번역 품질 ✓ 무료 사용 +
    +
    +
    +
    + +
    +
    +
    + 🎯 + DeepL 번역 + 프리미엄 +
    +
    + 높은 번역 품질로 유명한 AI 번역 서비스 +
    +
    + ✓ 최고 품질 번역 ✓ 문맥 이해 ✓ 프리미엄 이상 사용 가능 +
    +
    +
    +
    + +
    +
    +
    + 🤖 + OpenAI ChatGPT + VIP +
    +
    + 문맥을 이해하는 고품질 AI 번역, 의역 및 설명 포함 +
    +
    + ✓ AI 기반 번역 ✓ 문맥 이해 ✓ 의역 제공 ✓ VIP 전용 +
    +
    +
    +
    + +
    +
    +
    + 💎 + Google Gemini + 비활성화 +
    +
    + 현재 사용할 수 없습니다 (서비스 점검 중) +
    +
    + ✗ 현재 사용 불가 ✗ 서비스 점검 중 +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + 시간 알람 설정 +
    +
    + 작업 시간과 휴식 시간을 설정하여 건강한 작업 패턴을 유지하세요. +
    + +
    +
    + +
    + 알람 활성화/비활성화 +
    + +
    + + + 분 (작업 후 휴식 알림) +
    + +
    + + + 분 (휴식 시간 길이) +
    + +
    + + +
    +
    +
    +
    + +
    +
    ⏳ 설정을 저장하는 중...
    + +
    + + +
    +
    + + +
    + + + + \ No newline at end of file diff --git a/wrmc_ext/settings.js b/wrmc_ext/settings.js new file mode 100644 index 0000000..8a7288a --- /dev/null +++ b/wrmc_ext/settings.js @@ -0,0 +1,546 @@ +// 설정 페이지 JavaScript +class SettingsManager { + constructor() { + this.config = null; + this.userInfo = null; + this.currentSettings = {}; + this.timeAlarmSettings = {}; + + // 회원등급별 사용 가능한 엔진 정의 + this.availableEngines = { + 'basic': ['google', 'mymemory'], + 'premium': ['google', 'mymemory', 'deepl'], + 'vip': ['google', 'mymemory', 'deepl', 'openai'] + }; + + // 엔진별 상세 정보 + this.engineInfo = { + 'google': { + name: 'Google 번역', + description: '빠른 속도와 다양한 언어 지원을 제공하는 구글의 무료 번역 서비스', + level: 'basic' + }, + 'mymemory': { + name: 'MyMemory 번역', + description: '번역 메모리 기반으로 높은 품질의 번역을 제공하는 무료 서비스', + level: 'basic' + }, + 'deepl': { + name: 'DeepL 번역', + description: '높은 번역 품질로 유명한 AI 번역 서비스', + level: 'premium' + }, + 'openai': { + name: 'OpenAI ChatGPT', + description: '문맥을 이해하는 고품질 AI 번역, 의역 및 설명 포함', + level: 'vip' + }, + 'gemini': { + name: 'Google Gemini', + description: '구글의 최신 AI 모델, 자연스러운 번역과 의역 제공', + level: 'vip' + } + }; + + this.init(); + } + + async init() { + try { + console.log('설정 페이지 초기화 시작'); + + // 탭 기능 초기화 + this.initTabs(); + + await this.loadConfig(); + await this.loadUserInfo(); + await this.loadCurrentSettings(); + await this.loadTimeAlarmSettings(); + + // UI 초기화 + this.initializeUI(); + this.initializeTimeAlarmUI(); + this.setupEventListeners(); + this.updateUI(); + + console.log('설정 페이지 초기화 완료'); + } catch (error) { + console.error('설정 페이지 초기화 실패:', error); + this.showMessage('설정 페이지를 초기화하는 중 오류가 발생했습니다.', 'error'); + } + } + + // 탭 기능 초기화 + initTabs() { + const tabButtons = document.querySelectorAll('.tab-button'); + const tabContents = document.querySelectorAll('.tab-content'); + + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const targetTab = button.dataset.tab; + + // 모든 탭 버튼과 컨텐츠 비활성화 + tabButtons.forEach(btn => btn.classList.remove('active')); + tabContents.forEach(content => content.classList.remove('active')); + + // 선택된 탭 활성화 + button.classList.add('active'); + const targetContent = document.getElementById(`${targetTab}-tab`); + if (targetContent) { + targetContent.classList.add('active'); + } + + console.log(`탭 전환: ${targetTab}`); + }); + }); + } + + async loadConfig() { + try { + const result = await chrome.storage.local.get('settings_config'); + this.config = result.settings_config || {}; + console.log('설정을 settings_config에서 로드함:', this.config); + } catch (error) { + console.error('설정 로드 실패:', error); + this.config = {}; + } + } + + async loadUserInfo() { + try { + console.log('사용자 정보 로드 시작...'); + + if (!this.config || !this.config.ACCESS_TOKEN) { + console.warn('액세스 토큰이 없습니다. 기본 회원으로 설정합니다.'); + this.userInfo = { membership_level: 'basic' }; + return; + } + + const SUPABASE_URL = this.config.SUPABASE_URL || "http://146.56.101.199:8000"; + const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + + console.log('Supabase URL:', SUPABASE_URL); + console.log('토큰 존재 여부:', !!this.config.ACCESS_TOKEN); + + // 사용자 기본 정보 가져오기 + const authUrl = `${SUPABASE_URL}/auth/v1/user`; + console.log('사용자 인증 요청:', authUrl); + + const authRes = await fetch(authUrl, { + headers: { + Authorization: `Bearer ${this.config.ACCESS_TOKEN}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + console.log('인증 응답 상태:', authRes.status, authRes.statusText); + + if (!authRes.ok) { + const errorText = await authRes.text(); + console.error('사용자 인증 실패:', errorText); + throw new Error(`사용자 인증 실패: ${authRes.status}`); + } + + const authUser = await authRes.json(); + console.log('인증된 사용자:', authUser); + + if (!authUser || !authUser.email) { + throw new Error('사용자 이메일 정보가 없습니다.'); + } + + // 사용자 상세 정보 가져오기 + const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + console.log('사용자 상세 정보 요청:', detailsUrl); + + const detailsRes = await fetch(detailsUrl, { + headers: { + Authorization: `Bearer ${this.config.ACCESS_TOKEN}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + console.log('상세 정보 응답 상태:', detailsRes.status, detailsRes.statusText); + + if (!detailsRes.ok) { + const errorText = await detailsRes.text(); + console.error('사용자 정보 조회 실패:', errorText); + throw new Error(`사용자 정보 조회 실패: ${detailsRes.status}`); + } + + const detailsData = await detailsRes.json(); + console.log('사용자 상세 정보:', detailsData); + + if (!detailsData || !Array.isArray(detailsData) || detailsData.length === 0) { + console.warn('사용자 상세 정보가 없습니다. 기본 회원으로 설정합니다.'); + this.userInfo = { + email: authUser.email, + membership_level: 'basic' + }; + } else { + this.userInfo = detailsData[0]; + // membership_level이 없으면 기본값 설정 + if (!this.userInfo.membership_level) { + this.userInfo.membership_level = 'basic'; + } + } + + console.log('사용자 정보 로드 완료:', this.userInfo); + } catch (error) { + console.error('사용자 정보 로드 실패:', error); + this.userInfo = { membership_level: 'basic' }; + console.log('기본 회원으로 설정됨'); + } + } + + async loadCurrentSettings() { + try { + const result = await chrome.storage.local.get('translation_engine_settings'); + this.currentSettings = result.translation_engine_settings || {}; + + // 기본값 설정 (구글만 활성화, 나머지는 비활성화) + const defaultSettings = { + google: true, // 구글만 기본 활성화 + mymemory: false, // MyMemory 비활성화 + deepl: false, // DeepL 비활성화 + openai: false, // OpenAI 비활성화 + gemini: false // Gemini 비활성화 + }; + + // 기존 설정이 없으면 기본값 사용 + Object.keys(defaultSettings).forEach(engine => { + if (this.currentSettings[engine] === undefined) { + this.currentSettings[engine] = defaultSettings[engine]; + } + }); + + console.log('현재 설정 로드:', this.currentSettings); + } catch (error) { + console.error('현재 설정 로드 실패:', error); + // 기본값으로 구글만 활성화 + this.currentSettings = { + google: true, + mymemory: false, + deepl: false, + openai: false, + gemini: false + }; + } + } + + async loadTimeAlarmSettings() { + try { + const result = await chrome.storage.local.get('time_alarm_settings'); + this.timeAlarmSettings = result.time_alarm_settings || { + enabled: true, + workTime: 60, // 분 + restTime: 5, // 분 + autoZzim: false + }; + console.log('시간 알람 설정 로드 완료:', this.timeAlarmSettings); + } catch (error) { + console.error('시간 알람 설정 로드 실패:', error); + this.timeAlarmSettings = { + enabled: true, + workTime: 60, + restTime: 5, + autoZzim: false + }; + } + } + + initializeUI() { + // 회원등급 정보 표시 + const membershipElement = document.getElementById('current-membership'); + const membershipInfoElement = document.getElementById('membership-engines-info'); + + if (membershipElement && this.userInfo) { + const level = this.userInfo.membership_level || 'basic'; + const levelNames = { + 'basic': '기본 회원', + 'premium': '프리미엄 회원', + 'vip': 'VIP 회원' + }; + + membershipElement.textContent = levelNames[level.toLowerCase()] || '기본 회원'; + + const availableEngines = this.availableEngines[level.toLowerCase()] || this.availableEngines.basic; + const engineNames = availableEngines.map(engine => this.engineInfo[engine].name); + + membershipInfoElement.innerHTML = ` + 사용 가능한 번역 엔진:
    + ${engineNames.join(', ')}

    + 등급별 혜택:
    + • 기본: Google, MyMemory (무료 엔진)
    + • 프리미엄: + DeepL (고품질 엔진)
    + • VIP: + ChatGPT + `; + + console.log('회원등급 UI 업데이트 완료:', level); + } else { + console.error('회원등급 표시 요소를 찾을 수 없습니다.'); + } + } + + // 시간 알람 UI 초기화 + initializeTimeAlarmUI() { + this.updateTimeAlarmUI(); + } + + setupEventListeners() { + // 토글 스위치 이벤트 + const toggleSwitches = document.querySelectorAll('.toggle-switch'); + toggleSwitches.forEach(toggle => { + toggle.addEventListener('click', (e) => { + const engine = e.target.dataset.engine; + if (engine && !e.target.classList.contains('disabled')) { + this.toggleEngine(engine); + } else if (e.target.id === 'timeAlarmToggle') { + // 시간 알람 토글 + this.timeAlarmSettings.enabled = !this.timeAlarmSettings.enabled; + this.updateTimeAlarmUI(); + console.log('시간 알람 토글:', this.timeAlarmSettings.enabled); + } + }); + }); + + // 저장 버튼 + const saveButton = document.getElementById('save-settings'); + if (saveButton) { + saveButton.addEventListener('click', () => this.saveSettings()); + } + + // 기본값 복원 버튼 + const resetButton = document.getElementById('reset-settings'); + if (resetButton) { + resetButton.addEventListener('click', () => this.resetToDefaults()); + } + + // 시간 입력 필드 이벤트 + const workTimeInput = document.getElementById('workTimeInput'); + const restTimeInput = document.getElementById('restTimeInput'); + const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); + + if (workTimeInput) { + workTimeInput.addEventListener('change', (e) => { + this.timeAlarmSettings.workTime = parseInt(e.target.value) || 60; + }); + } + + if (restTimeInput) { + restTimeInput.addEventListener('change', (e) => { + this.timeAlarmSettings.restTime = parseInt(e.target.value) || 5; + }); + } + + if (autoZzimCheckbox) { + autoZzimCheckbox.addEventListener('change', (e) => { + this.timeAlarmSettings.autoZzim = e.target.checked; + }); + } + } + + updateUI() { + const userLevel = (this.userInfo?.membership_level || 'basic').toLowerCase(); + const availableEngines = this.availableEngines[userLevel] || this.availableEngines.basic; + + // 각 엔진 아이템 업데이트 + const engineItems = document.querySelectorAll('.engine-item'); + engineItems.forEach(item => { + const engine = item.dataset.engine; + const toggle = item.querySelector('.toggle-switch'); + + if (availableEngines.includes(engine)) { + // 사용 가능한 엔진 + item.classList.remove('disabled'); + toggle.classList.remove('disabled'); + + // 현재 설정에 따라 토글 상태 설정 + if (this.currentSettings[engine]) { + toggle.classList.add('active'); + } else { + toggle.classList.remove('active'); + } + } else { + // 사용 불가능한 엔진 + item.classList.add('disabled'); + toggle.classList.add('disabled'); + toggle.classList.remove('active'); + } + }); + + // 시간 알람 UI 업데이트 + this.updateTimeAlarmUI(); + } + + updateTimeAlarmUI() { + const timeAlarmToggle = document.getElementById('timeAlarmToggle'); + const workTimeInput = document.getElementById('workTimeInput'); + const restTimeInput = document.getElementById('restTimeInput'); + const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); + + if (timeAlarmToggle) { + if (this.timeAlarmSettings.enabled) { + timeAlarmToggle.classList.add('active'); + } else { + timeAlarmToggle.classList.remove('active'); + } + } + + if (workTimeInput) { + workTimeInput.value = this.timeAlarmSettings.workTime; + } + + if (restTimeInput) { + restTimeInput.value = this.timeAlarmSettings.restTime; + } + + if (autoZzimCheckbox) { + autoZzimCheckbox.checked = this.timeAlarmSettings.autoZzim; + } + } + + toggleEngine(engine) { + this.currentSettings[engine] = !this.currentSettings[engine]; + this.updateUI(); + console.log(`${engine} 엔진 토글:`, this.currentSettings[engine]); + } + + async saveSettings() { + try { + this.showLoading(true); + + // 시간 알람 설정 업데이트 + const workTimeInput = document.getElementById('workTimeInput'); + const restTimeInput = document.getElementById('restTimeInput'); + const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); + + if (workTimeInput) { + this.timeAlarmSettings.workTime = parseInt(workTimeInput.value) || 60; + } + if (restTimeInput) { + this.timeAlarmSettings.restTime = parseInt(restTimeInput.value) || 5; + } + if (autoZzimCheckbox) { + this.timeAlarmSettings.autoZzim = autoZzimCheckbox.checked; + } + + // 번역 엔진 설정 저장 + await chrome.storage.local.set({ + 'translation_engine_settings': this.currentSettings + }); + + // 시간 알람 설정 저장 + await chrome.storage.local.set({ + 'time_alarm_settings': this.timeAlarmSettings + }); + + // Background script에 시간 알람 설정 변경 알림 + chrome.runtime.sendMessage({ + action: 'updateTimeAlarmSettings', + settings: this.timeAlarmSettings + }); + + console.log('설정 저장 완료:', { + translation: this.currentSettings, + timeAlarm: this.timeAlarmSettings + }); + + this.showMessage('설정이 성공적으로 저장되었습니다.', 'success'); + + } catch (error) { + console.error('설정 저장 실패:', error); + this.showMessage('설정 저장 중 오류가 발생했습니다.', 'error'); + } finally { + this.showLoading(false); + } + } + + async resetToDefaults() { + if (!confirm('모든 설정을 기본값으로 복원하시겠습니까?')) { + return; + } + + try { + this.showLoading(true); + + // 기본값으로 재설정 (구글만 활성화) + this.currentSettings = { + google: true, + mymemory: false, + deepl: false, + openai: false, + gemini: false + }; + + // 저장 + await chrome.storage.local.set({ + 'translation_engine_settings': this.currentSettings + }); + + // UI 업데이트 + this.updateUI(); + + // 시간 알람 설정 초기화 + this.timeAlarmSettings = { + enabled: false, + workTime: 60, + restTime: 5, + autoZzim: false + }; + + // 저장소에서 시간 알람 설정 제거 + await chrome.storage.local.remove('time_alarm_settings'); + + // Background script에 알림 + chrome.runtime.sendMessage({ + action: 'updateTimeAlarmSettings', + settings: this.timeAlarmSettings + }); + + console.log('기본값 복원 완료'); + this.showMessage('설정이 기본값으로 복원되었습니다.', 'success'); + + } catch (error) { + console.error('기본값 복원 실패:', error); + this.showMessage('기본값 복원 중 오류가 발생했습니다.', 'error'); + } finally { + this.showLoading(false); + } + } + + showMessage(text, type = 'info') { + const messageElement = document.getElementById('message'); + if (messageElement) { + messageElement.textContent = text; + messageElement.className = `message ${type}`; + messageElement.style.display = 'block'; + + // 3초 후 자동 숨김 + setTimeout(() => { + messageElement.style.display = 'none'; + }, 3000); + } + } + + showLoading(show) { + const loadingElement = document.getElementById('loading'); + if (loadingElement) { + loadingElement.style.display = show ? 'block' : 'none'; + } + } +} + +// 페이지 로드 시 초기화 +document.addEventListener('DOMContentLoaded', () => { + console.log('설정 페이지 DOM 로드 완료'); + new SettingsManager(); +}); + +// 창 닫기 전 확인 +window.addEventListener('beforeunload', (e) => { + // 설정이 변경되었는지 확인하는 로직을 추가할 수 있음 + // 현재는 단순히 로그만 출력 + console.log('설정 페이지 종료'); +}); \ No newline at end of file diff --git a/wrmc_ext/zzim.html b/wrmc_ext/zzim.html new file mode 100644 index 0000000..e9d39ae --- /dev/null +++ b/wrmc_ext/zzim.html @@ -0,0 +1,288 @@ + + + + + + 찜관리 + + + +
    +
    +
    오늘 찜한 갯수
    +
    0
    +
    / 100
    +
    +
    +
    찜 마일리지
    +
    0
    +
    / 1000
    +
    +
    + +
    +
    + 🏪 내 마켓 등록 +
    +
    + https://smartstore.naver.com/ + + +
    +
    + + + +
    +
    + + + +
    + +
    +

    내 마켓 목록

    +
    + +
    +
    +
    + +
    +
    + 🎯 찜하기 작업 +
    +
    + + +
    + + + +
    + + + + \ No newline at end of file diff --git a/wrmc_ext/zzim.js b/wrmc_ext/zzim.js new file mode 100644 index 0000000..b800658 --- /dev/null +++ b/wrmc_ext/zzim.js @@ -0,0 +1,616 @@ +// zzim.js - 찜관리 모듈 + +class ZzimManager { + constructor() { + this.SUPABASE_URL = null; + this.SUPABASE_ANON_KEY = null; + this.access_token = null; + this.user_id = null; + this.zzimInProgress = false; + } + + async init() { + console.log('ZzimManager 초기화 중...'); + + try { + // Chrome Extension 환경에서 설정값 가져오기 + if (typeof chrome !== 'undefined' && chrome.storage) { + const config = await chrome.storage.local.get(['zzim_config']); + + if (config.zzim_config) { + this.access_token = config.zzim_config.ACCESS_TOKEN; + this.user_id = config.zzim_config.USER_ID; + this.SUPABASE_URL = config.zzim_config.SUPABASE_URL || "http://146.56.101.199:8000"; + this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + } else { + // 기존 방식으로 폴백 + const fallbackConfig = await chrome.storage.local.get(['access_token', 'user_id', 'SUPABASE_URL', 'SUPABASE_ANON_KEY']); + this.access_token = fallbackConfig.access_token; + this.user_id = fallbackConfig.user_id; + this.SUPABASE_URL = fallbackConfig.SUPABASE_URL || "http://146.56.101.199:8000"; + this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + } + + if (!this.access_token || !this.user_id) { + throw new Error('로그인이 필요합니다.'); + } + + console.log('Chrome Extension 환경에서 설정 로드 완료', { + hasToken: !!this.access_token, + hasUserId: !!this.user_id, + hasUrl: !!this.SUPABASE_URL, + hasKey: !!this.SUPABASE_ANON_KEY + }); + } else { + throw new Error('Chrome Extension 환경이 아닙니다.'); + } + + // 사용자 찜 설정 초기화 + await this.initializeUserZzimSettings(); + + // 통계 및 마켓 정보 로드 + await Promise.all([ + this.loadZzimStats(), + this.loadMyMarkets() + ]); + + // 이벤트 바인딩 + this.bindEvents(); + + console.log('ZzimManager 초기화 완료'); + } catch (error) { + console.error('ZzimManager 초기화 실패:', error); + this.showError('초기화 실패: ' + error.message); + } + } + + async initializeUserZzimSettings() { + try { + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/rpc/initialize_user_zzim_settings`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ user_uuid: this.user_id }) + }); + + if (!response.ok) { + console.error('찜 설정 초기화 실패:', response.status); + } + } catch (error) { + console.error('찜 설정 초기화 오류:', error); + } + } + + bindEvents() { + // 마켓 추가 버튼 + const addMarketBtn = document.getElementById('add-market-btn'); + if (addMarketBtn) { + addMarketBtn.addEventListener('click', () => this.addMarket()); + } + + // 내 마켓 찜하기 버튼 + const myMarketZzimBtn = document.getElementById('my-market-zzim-btn'); + if (myMarketZzimBtn) { + myMarketZzimBtn.addEventListener('click', () => this.startMyMarketZzim()); + } + + // 품앗이 찜하기 버튼 + const mutualZzimBtn = document.getElementById('mutual-zzim-btn'); + if (mutualZzimBtn) { + mutualZzimBtn.addEventListener('click', () => this.startMutualZzim()); + } + + // 마켓 URL 입력 시 자동 변환 + const marketUrlInput = document.getElementById('market-url'); + if (marketUrlInput) { + marketUrlInput.addEventListener('input', (e) => { + let url = e.target.value; + if (url && !url.startsWith('https://smartstore.naver.com/')) { + // URL이 smartstore로 시작하지 않으면 자동으로 앞에 추가 + if (!url.startsWith('http')) { + url = 'https://smartstore.naver.com/' + url; + e.target.value = url; + } + } + }); + } + } + + async loadZzimStats() { + try { + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`찜 통계 로드 실패: ${response.status}`); + } + + const data = await response.json(); + const stats = data[0] || { + current_daily_count: 0, + daily_zzim_limit: 100, + current_mileage: 0, + zzim_mileage_limit: 1000 + }; + + // UI 업데이트 - HTML의 실제 ID와 일치하도록 수정 + const todayCountEl = document.getElementById('today-zzim-count'); + const todayLimitEl = document.getElementById('today-zzim-limit'); + const mileageCountEl = document.getElementById('zzim-mileage'); + const mileageLimitEl = document.getElementById('zzim-mileage-limit'); + + if (todayCountEl) todayCountEl.textContent = stats.current_daily_count; + if (todayLimitEl) todayLimitEl.textContent = stats.daily_zzim_limit; + if (mileageCountEl) mileageCountEl.textContent = stats.current_mileage; + if (mileageLimitEl) mileageLimitEl.textContent = stats.zzim_mileage_limit; + + } catch (error) { + console.error('찜 통계 로드 오류:', error); + this.showError('찜 통계를 불러올 수 없습니다.'); + } + } + + async loadMyMarkets() { + try { + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true&order=created_at.desc`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`마켓 목록 로드 실패: ${response.status}`); + } + + const markets = await response.json(); + this.renderMarketsList(markets); + + } catch (error) { + console.error('마켓 목록 로드 오류:', error); + this.showError('마켓 목록을 불러올 수 없습니다.'); + } + } + + renderMarketsList(markets) { + const marketsList = document.getElementById('my-markets-list'); + if (!marketsList) return; + + if (markets.length === 0) { + marketsList.innerHTML = '

    등록된 마켓이 없습니다.

    '; + return; + } + + marketsList.innerHTML = markets.map(market => ` +
    +
    +
    +
    ${market.market_name}
    +
    ${market.market_nickname}
    +
    ${market.market_url}
    +
    +
    + + +
    +
    +
    + `).join(''); + } + + async addMarket() { + const marketUrl = document.getElementById('market-url').value.trim(); + const marketName = document.getElementById('market-name').value.trim(); + const marketNickname = document.getElementById('market-nickname').value.trim(); + + if (!marketUrl || !marketName || !marketNickname) { + this.showError('모든 필드를 입력해주세요.'); + return; + } + + if (!marketUrl.startsWith('https://smartstore.naver.com/')) { + this.showError('올바른 네이버 스마트스토어 URL을 입력해주세요.'); + return; + } + + try { + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + user_id: this.user_id, + market_url: marketUrl, + market_name: marketName, + market_nickname: marketNickname + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || '마켓 추가 실패'); + } + + // 입력 필드 초기화 + document.getElementById('market-url').value = ''; + document.getElementById('market-name').value = ''; + document.getElementById('market-nickname').value = ''; + + // 마켓 목록 다시 로드 + await this.loadMyMarkets(); + this.showSuccess('마켓이 추가되었습니다.'); + + } catch (error) { + console.error('마켓 추가 오류:', error); + this.showError('마켓 추가 중 오류가 발생했습니다: ' + error.message); + } + } + + async editMarket(marketId) { + // 편집 모달을 표시하거나 인라인 편집 구현 + const newName = prompt('새로운 마켓 이름을 입력하세요:'); + if (!newName) return; + + const newNickname = prompt('새로운 마켓 별명을 입력하세요:'); + if (!newNickname) return; + + try { + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + market_name: newName, + market_nickname: newNickname + }) + }); + + if (!response.ok) { + throw new Error('마켓 수정 실패'); + } + + await this.loadMyMarkets(); + this.showSuccess('마켓이 수정되었습니다.'); + + } catch (error) { + console.error('마켓 수정 오류:', error); + this.showError('마켓 수정 중 오류가 발생했습니다.'); + } + } + + async deleteMarket(marketId) { + if (!confirm('정말로 이 마켓을 삭제하시겠습니까?')) { + return; + } + + try { + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + is_active: false + }) + }); + + if (!response.ok) { + throw new Error('마켓 삭제 실패'); + } + + await this.loadMyMarkets(); + this.showSuccess('마켓이 삭제되었습니다.'); + + } catch (error) { + console.error('마켓 삭제 오류:', error); + this.showError('마켓 삭제 중 오류가 발생했습니다.'); + } + } + + async startMyMarketZzim() { + if (this.zzimInProgress) { + this.showError('찜하기가 이미 진행 중입니다.'); + return; + } + + try { + // 내 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('마켓 목록을 가져올 수 없습니다.'); + } + + const markets = await response.json(); + if (markets.length === 0) { + this.showError('등록된 마켓이 없습니다.'); + return; + } + + // 랜덤 마켓 선택 + const randomMarket = markets[Math.floor(Math.random() * markets.length)]; + this.showStatus(`"${randomMarket.market_nickname}" 마켓에서 찜하기를 시작합니다...`); + + await this.executeZzim(randomMarket, 'my_market', false); // 포그라운드에서 실행 + + } catch (error) { + console.error('내 마켓 찜하기 오류:', error); + this.showError('내 마켓 찜하기 중 오류가 발생했습니다.'); + } + } + + async startMutualZzim() { + if (this.zzimInProgress) { + this.showError('찜하기가 이미 진행 중입니다.'); + return; + } + + try { + // 다른 사용자의 마켓 목록 가져오기 (품앗이용) + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=neq.${this.user_id}&is_active=eq.true`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('품앗이 마켓 목록을 가져올 수 없습니다.'); + } + + const markets = await response.json(); + if (markets.length === 0) { + this.showError('품앗이할 마켓이 없습니다.'); + return; + } + + // 랜덤 마켓 선택 + const randomMarket = markets[Math.floor(Math.random() * markets.length)]; + this.showStatus(`품앗이 마켓 "${randomMarket.market_nickname}"에서 찜하기를 시작합니다...`); + + await this.executeZzim(randomMarket, 'mutual', true); // 백그라운드에서 실행 + + } catch (error) { + console.error('품앗이 찜하기 오류:', error); + this.showError('품앗이 찜하기 중 오류가 발생했습니다.'); + } + } + + async executeZzim(market, zzimType, inBackground = false) { + this.zzimInProgress = true; + + try { + // 찜하기 스크립트 + const zzimScript = ` + (function() { + let zzimCount = 0; + const maxZzim = 50; // 최대 찜할 개수 + + function clickZzimButtons() { + const zzimButtons = document.querySelectorAll('.zzim_button:not(.active)'); + console.log('찾은 찜 버튼 개수:', zzimButtons.length); + + if (zzimButtons.length === 0) { + console.log('더 이상 찜할 상품이 없습니다.'); + return false; + } + + // 최대 10개씩 찜하기 + const buttonsToClick = Array.from(zzimButtons).slice(0, Math.min(10, maxZzim - zzimCount)); + + buttonsToClick.forEach((btn, index) => { + setTimeout(() => { + try { + btn.click(); + zzimCount++; + console.log(\`찜 버튼 클릭: \${zzimCount}개\`); + } catch (e) { + console.error('찜 버튼 클릭 오류:', e); + } + }, index * 500); // 0.5초 간격 + }); + + return buttonsToClick.length > 0 && zzimCount < maxZzim; + } + + // 페이지 스크롤 및 찜하기 반복 + function scrollAndZzim() { + if (zzimCount >= maxZzim) { + console.log('최대 찜하기 완료:', zzimCount); + return; + } + + // 페이지 하단으로 스크롤 + window.scrollTo(0, document.body.scrollHeight); + + setTimeout(() => { + if (clickZzimButtons()) { + setTimeout(scrollAndZzim, 3000); // 3초 후 다시 시도 + } + }, 1000); + } + + // 시작 + scrollAndZzim(); + + return zzimCount; + })(); + `; + + if (inBackground) { + // 백그라운드에서 실행 (새 탭에서 백그라운드로) + const tab = await chrome.tabs.create({ + url: market.market_url, + active: false + }); + + // 페이지 로드 완료 후 스크립트 실행 + chrome.tabs.onUpdated.addListener(function listener(tabId, info) { + if (tabId === tab.id && info.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: eval(`(${zzimScript})`) + }).then(() => { + // 일정 시간 후 탭 닫기 + setTimeout(() => { + chrome.tabs.remove(tab.id); + }, 30000); // 30초 후 + }); + } + }); + } else { + // 포그라운드에서 실행 (현재 창에서) + window.open(market.market_url, '_blank'); + } + + // 찜 기록 저장 + await this.recordZzim(market, zzimType); + + this.showSuccess(`찜하기가 시작되었습니다. (${market.market_nickname})`); + + } catch (error) { + console.error('찜하기 실행 오류:', error); + this.showError('찜하기 실행 중 오류가 발생했습니다.'); + } finally { + this.zzimInProgress = false; + } + } + + async recordZzim(market, zzimType) { + try { + // 찜 기록 저장 + await fetch(`${this.SUPABASE_URL}/rest/v1/jjim`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + user_id: this.user_id, + market_url: market.market_url, + market_name: market.market_name, + market_nickname: market.market_nickname, + zzim_type: zzimType, + product_count: 50 // 예상 찜 개수 (실제로는 스크립트 결과를 받아야 함) + }) + }); + + // 통계 업데이트 + await this.updateZzimStats(50, zzimType); + + // UI 새로고침 + await this.loadZzimStats(); + + } catch (error) { + console.error('찜 기록 저장 오류:', error); + } + } + + async updateZzimStats(count, zzimType) { + try { + const mileageIncrease = zzimType === 'mutual' ? count * 2 : count; // 품앗이는 마일리지 2배 + + await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + current_daily_count: `current_daily_count + ${count}`, + current_mileage: `current_mileage + ${mileageIncrease}` + }) + }); + } catch (error) { + console.error('찜 통계 업데이트 오류:', error); + } + } + + showStatus(message) { + const statusEl = document.getElementById('zzim-status'); + const errorEl = document.getElementById('zzim-error'); + const successEl = document.getElementById('zzim-success'); + + // 모든 상태 메시지 숨기기 + if (statusEl) statusEl.style.display = 'none'; + if (errorEl) errorEl.style.display = 'none'; + if (successEl) successEl.style.display = 'none'; + + // 상태 메시지 표시 + if (statusEl) { + statusEl.textContent = message; + statusEl.style.display = 'block'; + } + } + + showError(message) { + const statusEl = document.getElementById('zzim-status'); + const errorEl = document.getElementById('zzim-error'); + const successEl = document.getElementById('zzim-success'); + + // 모든 상태 메시지 숨기기 + if (statusEl) statusEl.style.display = 'none'; + if (errorEl) errorEl.style.display = 'none'; + if (successEl) successEl.style.display = 'none'; + + // 오류 메시지 표시 + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + } + + showSuccess(message) { + const statusEl = document.getElementById('zzim-status'); + const errorEl = document.getElementById('zzim-error'); + const successEl = document.getElementById('zzim-success'); + + // 모든 상태 메시지 숨기기 + if (statusEl) statusEl.style.display = 'none'; + if (errorEl) errorEl.style.display = 'none'; + if (successEl) successEl.style.display = 'none'; + + // 성공 메시지 표시 + if (successEl) { + successEl.textContent = message; + successEl.style.display = 'block'; + } + } +} + +// 전역 인스턴스 생성 +let zzimManager; + +// DOM 로드 완료 후 초기화 +document.addEventListener('DOMContentLoaded', async () => { + zzimManager = new ZzimManager(); + await zzimManager.init(); +}); \ No newline at end of file diff --git a/확장프로그램설치도구.spec b/확장프로그램설치도구.spec new file mode 100644 index 0000000..b2e96c9 --- /dev/null +++ b/확장프로그램설치도구.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[('wrmc_ext', 'wrmc_ext')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='확장프로그램설치도구', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +)