first commit

This commit is contained in:
9700X_PC 2025-06-26 18:33:57 +09:00
commit 62a95c6fcc
24 changed files with 15616 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
build/
dist/
*.pyc
*.pyo
*.pyd
include/
lib/
scripts/
pyvenv.cfg

38
ExtensionInstaller.spec Normal file
View File

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

195
log.txt Normal file
View File

@ -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/'}]

343
main.py Normal file
View File

@ -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(
"<b>설치 안내:</b><br>"
"<span style='color:#e1ffbb;'>1. main.py와 같은 경로의 <b>wrmc_ext</b> 폴더가 자동 복사됩니다.<br>"
"2. 설치할 브라우저를 모두 체크 후 <b>설치 시작</b>을 누르세요.<br>"
"3. 설치 후 브라우저별 단계별 안내를 참고하세요.</span>")
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": "② 오른쪽 상단의 <b>[개발자 모드]</b>를 ON(체크)로 변경하세요.",
"Edge": "② 오른쪽 상단의 <b>[개발자 모드]</b>를 ON(체크)로 변경하세요.",
"Whale": "② <span style='color:#ffc'><b>제일 하단의 [개발자 모드]</b>를 ON(체크)로 변경하세요.</span>"
}
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("③ <b>[압축해제된 확장 프로그램 로드]</b> 버튼 클릭")
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("⑤ <b>주소창 옆 확장프로그램 아이콘을 클릭하여 로그인</b>")
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

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pyside6
psutil

2427
wrmc_ext/background.js Normal file

File diff suppressed because it is too large Load Diff

244
wrmc_ext/bannedWords.html Normal file
View File

@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>금지어 관리</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', sans-serif;
background: #f4f6f9;
padding: 20px;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #2c3e50;
}
/* 통계 정보 스타일 */
.stats-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
font-size: 14px;
font-weight: bold;
color: #495057;
text-align: center;
}
/* 테이블 스타일 */
.banned-words-table-container {
overflow-x: auto;
margin-top: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#banned-words-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
#banned-words-table th,
#banned-words-table td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: left;
white-space: nowrap;
}
#banned-words-table th {
background-color: #f8f9fa;
font-weight: bold;
position: sticky;
top: 0;
z-index: 1;
}
#banned-words-table tr:nth-child(even) {
background-color: #f9f9f9;
}
/* 순번 열 스타일 */
#banned-words-table th:first-child,
#banned-words-table td:first-child {
width: 60px;
text-align: center;
}
/* 액션 버튼 스타일 */
.action-btn {
padding: 6px 10px;
margin: 2px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
display: inline-block;
min-width: 40px;
}
.view-btn {
background: #3498db;
color: white;
}
.edit-btn {
background: #f39c12;
color: white;
}
.delete-btn {
background: #e74c3c;
color: white;
}
.action-btn:hover {
opacity: 0.8;
}
/* 등급 표시 스타일 */
.grade-display {
display: inline-block;
padding: 4px 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #495057;
min-width: 60px;
text-align: center;
}
/* 로딩 표시 */
.loading {
text-align: center;
padding: 20px;
font-size: 14px;
color: #3498db;
}
/* 키프리스 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: 15px auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 800px;
border-radius: 8px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.modal-header h3 {
margin: 0;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
color: #666;
}
.close:hover {
color: #000;
}
/* 디버그 정보 */
.debug-info {
margin-top: 20px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
color: #6c757d;
}
</style>
</head>
<body>
<h2>🚫 금지어 관리</h2>
<!-- 디버그 정보 -->
<div class="debug-info" id="debug-info" style="display: block;">
초기화 중...
<br>
<button id="token-check-btn" style="margin-top: 10px; padding: 5px 10px; font-size: 12px;">🔍 토큰 상태 확인</button>
</div>
<!-- 통계 정보 -->
<div id="banned-words-stats" class="stats-info">
<!-- 통계 정보가 여기에 표시됩니다 -->
</div>
<!-- 금지어 테이블 -->
<div class="banned-words-table-container">
<table id="banned-words-table">
<thead>
<tr>
<th>순번</th>
<th>금지어</th>
<th>등급</th>
<th>작업</th>
</tr>
</thead>
<tbody id="banned-words-tbody">
<!-- 동적으로 생성됨 -->
</tbody>
</table>
</div>
<div class="loading" id="banned-words-loading" style="display: none;">
🔄 금지어 목록을 불러오는 중...
</div>
<!-- 키프리스 결과 모달 -->
<div id="kipris-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>🔍 키프리스 검색 결과</h3>
<span class="close" id="close-kipris">&times;</span>
</div>
<div id="kipris-word-title"></div>
<div id="kipris-results">
<!-- 동적으로 생성됨 -->
</div>
<div class="loading" id="kipris-loading" style="display: none;">
🔄 키프리스 결과를 불러오는 중...
</div>
</div>
</div>
<script src="bannedWords.js"></script>
</body>
</html>

1652
wrmc_ext/bannedWords.js Normal file

File diff suppressed because it is too large Load Diff

866
wrmc_ext/content.js Normal file
View File

@ -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 = `<div style="line-height: 1.6;">`;
// 결과가 없을 경우 안전한 단어 표시
if (results.error) {
html += `<p style="color: red; font-weight: bold;">오류: ${results.error}</p>`;
} else if (!Array.isArray(results) || results.length === 0) {
html += `<p style="color: #7f8c8d; font-style: italic;">지식재산권이 없는 안전한 단어</p>`;
} else {
results.forEach((result, idx) => {
html += `<div style="margin-bottom: 24px; padding: 16px; border: 1px solid #ecf0f1; border-radius: 8px; background-color: #fafafa;">`;
// 결과 헤더 (제목과 개별 금지어 추가 버튼)
html += `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">`;
html += `<h3 style="margin: 0; font-size: 18px; color: #2980b9;">${idx + 1}번 결과</h3>`;
// html += `<button onclick="addIndividualToBannedWords('${result.registration_info?.trademarkName || keyword}', ${idx})"
// id="individual-banned-btn-${idx}"
// style="padding: 4px 8px; background-color: #e67e22; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px;">
// 이 상표를 금지어에 추가
// </button>`;
html += `</div>`;
// 상세 검색 결과 (출원번호 상세 조회)
if (result.detail) {
html += `<h4 style="margin: 12px 0 6px; font-size: 16px; color: #16a085; border-bottom: 1px solid #16a085; display: inline-block;">상세 정보</h4>`;
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 += `<p style="margin: 6px 0;"><strong>상표명:</strong> <span style="color: red; font-weight: bold;">${trademarkName} (불일치-확인필요)</span></p>`;
} else {
html += `<p style="margin: 6px 0;"><strong>상표명:</strong> ${trademarkName}</p>`;
}
html += `<p style="margin: 6px 0;"><strong>출원번호:</strong> ${dreg.applicationNum || "(출원번호 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>출원날짜:</strong> ${dreg.applicationDate || "(출원날짜 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>권리상태:</strong> ${dreg.lastDisposalCodeName || "(권리상태 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>공고번호:</strong> ${dreg.publicationNum || "(공고번호 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>등록번호:</strong> ${dreg.registerNum || "(등록번호 없음)"}</p>`;
html += `<h4 style="margin: 12px 0 6px; font-size: 16px; color: #8e44ad; border-bottom: 1px solid #8e44ad; display: inline-block;">권리정보</h4>`;
if (result.detail.rights_info && Object.keys(result.detail.rights_info).length > 0) {
for (let key in result.detail.rights_info) {
html += `<p style="margin: 6px 0;"><strong>카테고리 코드:</strong> ${key}</p>`;
result.detail.rights_info[key].forEach(item => {
html += `<p style="margin-left: 20px; margin: 4px 0;">- 지정상품명: ${item.asignProductName || ""}</p>`;
html += `<p style="margin-left: 20px; margin: 4px 0;">&nbsp;&nbsp;영문: ${item.asignProductNameEn || ""}</p>`;
html += `<p style="margin-left: 20px; margin: 4px 0;">&nbsp;&nbsp;유사군코드: ${item.similarCodes || ""}</p>`;
});
}
} else {
html += `<p style="margin: 6px 0; color: #7f8c8d;">(권리정보 없음)</p>`;
}
html += `<h4 style="margin: 12px 0 6px; font-size: 16px; color: #e67e22; border-bottom: 1px solid #e67e22; display: inline-block;">출원인 정보</h4>`;
if (result.detail.applicant_info && result.detail.applicant_info.mapping) {
const mapping = result.detail.applicant_info.mapping;
html += `<p style="margin: 6px 0;"><strong>국가명:</strong> ${mapping.nationalCodeName || "(없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>출원인명:</strong> ${mapping.applicantName || "(없음)"}</p>`;
} else {
html += `<p style="margin: 6px 0; color: #7f8c8d;">(출원인 정보 없음)</p>`;
}
} else if (result.detailError) {
html += `<p style="color: red; font-weight: bold;">상세 정보 검색 오류: ${result.detailError}</p>`;
}
html += `</div>`;
});
}
html += `</div>`;
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 += `
<div style="margin-bottom: 20px; padding: 16px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #3498db;">
<h3 style="margin: 0 0 8px 0; color: #2c3e50; font-size: 16px;">원문</h3>
<p style="margin: 0; font-size: 15px; line-height: 1.5; color: #333;">${originalText}</p>
</div>
`;
// 번역 결과가 있을 경우
if (results && results.length > 0) {
html += '<div style="margin-bottom: 16px;">';
results.forEach((result, index) => {
const engineName = getEngineDisplayName(result.engine);
const isSuccess = result.success;
// 각 번역 결과 카드
html += `
<div style="margin-bottom: 16px; padding: 16px; border: 1px solid ${isSuccess ? '#e1e8ed' : '#f8d7da'}; border-radius: 8px; background: ${isSuccess ? '#fff' : '#f8f9fa'};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: ${isSuccess ? '12px' : '8px'};">
<h4 style="margin: 0; color: ${isSuccess ? '#2c3e50' : '#721c24'}; font-size: 14px; font-weight: 600;">
${engineName}
</h4>
<span style="padding: 4px 8px; border-radius: 4px; font-size: 12px; background: ${isSuccess ? '#d4edda' : '#f8d7da'}; color: ${isSuccess ? '#155724' : '#721c24'};">
${isSuccess ? '성공' : '실패'}
</span>
</div>
${isSuccess ?
`<p style="margin: 0; font-size: 15px; line-height: 1.5; color: #333;">${result.translatedText}</p>` :
`<p style="margin: 0; font-size: 14px; color: #721c24;">${result.error || '번역 실패'}</p>`
}
</div>
`;
});
html += '</div>';
} else {
// 번역 결과가 없는 경우
html += `
<div style="text-align: center; padding: 40px; color: #6c757d;">
<p style="margin: 0; font-size: 16px;">번역 결과가 없습니다.</p>
<p style="margin: 8px 0 0 0; font-size: 14px;">다시 시도해 주세요.</p>
</div>
`;
}
// 회원등급별 가이드 메시지
html += `
<div style="margin-top: 20px; padding: 12px; background: #e3f2fd; border-radius: 6px; border-left: 4px solid #2196f3;">
<p style="margin: 0; font-size: 13px; color: #1565c0;">
💡 <strong>${userLevel || 'Basic'} 회원</strong> .
${getUserLevelGuide(userLevel)}
</p>
</div>
`;
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] 번역 툴팁 제거됨');
}
}

BIN
wrmc_ext/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

83
wrmc_ext/manifest.json Normal file
View File

@ -0,0 +1,83 @@
{
"manifest_version": 3,
"name": "내차는언제타냐 통합 확장",
"version": "1.2",
"description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.",
"permissions": [
"storage",
"activeTab",
"contextMenus",
"storage",
"notifications",
"alarms",
"tabs",
"scripting"
],
"host_permissions": [
"*://markinfo.kr/*",
"http://146.56.101.199:8000/*",
"https://oci1ckh08045.duckdns.org:8000/*",
"*://smartstore.naver.com/*",
"*://translate.googleapis.com/*",
"*://api.mymemory.translated.net/*",
"*://openapi.naver.com/*",
"*://api-free.deepl.com/*",
"*://api.deepl.com/*",
"*://api.openai.com/*",
"*://generativelanguage.googleapis.com/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"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": ["<all_urls>"]
}
]
}

649
wrmc_ext/manual.html Normal file
View File

@ -0,0 +1,649 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>내차는언제타냐 통합확장기 매뉴얼</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2rem;
opacity: 0.9;
}
.tabs-container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
.tabs {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
overflow-x: auto;
}
.tab {
padding: 15px 25px;
cursor: pointer;
border: none;
background: none;
font-size: 16px;
font-weight: 500;
color: #6c757d;
transition: all 0.3s ease;
white-space: nowrap;
min-width: 150px;
}
.tab:hover {
background: #e9ecef;
color: #495057;
}
.tab.active {
background: #007bff;
color: white;
position: relative;
}
.tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: #0056b3;
}
.tab-content {
display: none;
padding: 30px;
min-height: 500px;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.feature-card {
background: #f8f9fa;
border-radius: 10px;
padding: 25px;
margin-bottom: 20px;
border-left: 5px solid #007bff;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.feature-card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.feature-card h3 {
color: #007bff;
margin-bottom: 15px;
font-size: 1.4rem;
}
.feature-card p {
line-height: 1.6;
margin-bottom: 15px;
}
.feature-list {
list-style: none;
padding-left: 0;
}
.feature-list li {
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
position: relative;
padding-left: 25px;
}
.feature-list li:before {
content: '✓';
position: absolute;
left: 0;
color: #28a745;
font-weight: bold;
}
.feature-list li:last-child {
border-bottom: none;
}
.screenshot {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
margin: 15px 0;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
transform: translateY(-1px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
transform: translateY(-1px);
}
.alert {
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert-info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.alert-warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.version-info {
background: #e9ecef;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
text-align: center;
}
.keyboard-shortcut {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 5px 10px;
font-family: monospace;
font-size: 0.9rem;
margin: 0 5px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.tab {
padding: 12px 15px;
font-size: 14px;
min-width: 120px;
}
.tab-content {
padding: 20px;
}
.feature-card {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 내차는언제타냐 통합확장기</h1>
<p>매뉴얼 및 기능 가이드</p>
</div>
<div class="tabs-container">
<div class="tabs">
<button class="tab active" data-tab="overview">🏠 개요</button>
<button class="tab" data-tab="trademark">🔍 지재권검색</button>
<button class="tab" data-tab="translation">🌐 멀티번역</button>
<button class="tab" data-tab="timer">⏰ 시간관리</button>
<button class="tab" data-tab="banned-words">🚫 금지어</button>
<button class="tab" data-tab="sayings">💬 타냐대장경</button>
<button class="tab" data-tab="settings">⚙️ 설정</button>
</div>
<div class="tab-content active" id="overview">
<h2>🏠 확장 프로그램 개요</h2>
<div class="alert alert-info">
<strong>환영합니다!</strong> 내차는언제타냐 통합확장기는 지재권검색, 멀티번역, 시간관리 등 다양한 기능을 제공하는 올인원 도구입니다.
</div>
<div class="alert alert-warning">
<strong>주의사항:</strong> 지재권검색 및 멀티번역은 API 호출량을 소모합니다. 회원등급에 따라 일일 사용량이 제한됩니다.
</div>
<div class="grid">
<div class="feature-card">
<h3>🔍 지재권검색 기능</h3>
<p>키프리스(KIPRIS) 지재권검색을 자동화하여 빠르고 정확한 상표조사를 지원합니다.</p>
<ul class="feature-list">
<li>검색할 단어를 드래그 후 단축키(컨트롤+쉬프트+S) 입력</li>
<li>또는 우클릭으로 해당 지재권을 Search하고 결과 수집</li>
<li>현재 활성된 상태의 결과만 필터링</li>
<li>해당하는 상품군을 바로 확인할 수 있음</li>
<li>내 금지어목록에 바로 적용 및 편집알바생과 연동</li>
</ul>
</div>
<div class="feature-card">
<h3>🌐 멀티번역 기능</h3>
<p>여러 언어로 동시 번역하여 애매한 중국어를 이해할수 있도록 지원합니다.</p>
<ul class="feature-list">
<li><strong>멀티번역:</strong> Ctrl+Shift+E</li>
<li>또는 우클릭으로 해당 문장을 Translate하고 결과 수집</li>
<li>무료로 구글번역과 메모리 번역지원</li>
<li>일정등급 이상은 딥러닝 번역 DeepL 사용가능</li>
<li>챗GPT로 의역 지원가능 (Gemini는 현재 점검 중)</li>
<li>원클릭으로 많은 엔진의 결과를 확인할수 있음</li>
</ul>
</div>
<div class="feature-card">
<h3>🌐 원키번역 기능</h3>
<p>구글 기계번역으로 매우 빠르게 번역할수 있습니다.</p>
<ul class="feature-list">
<li>번역할 문장이나 단어를 드래그 후 단축키(컨트롤+쉬프트+Z) 입력</li>
<li>웹페이지에서 바로 한글과 중국어를 빠르게 번역</li>
<li>위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역</li>
<li>웹페이지에서 중국어를 드래그 후 단축키로 바로 번역 결과 확인</li>
</ul>
</div>
<div class="feature-card">
<h3>⏰ 시간관리 기능</h3>
<p>포모도로 기법 기반의 시간관리로 생산성을 극대화합니다.</p>
<ul class="feature-list">
<li>자동 작업/휴식 타이머</li>
<li>휴식시간 추천 활동</li>
<li>실시간 타이머 알림</li>
<li>개인화된 시간 설정</li>
</ul>
</div>
<div class="feature-card">
<h3>🚫 금지어 관리</h3>
<p>지재권검색 시 제외할 키워드를 효율적으로 관리합니다.</p>
<ul class="feature-list">
<li>개인별 금지어 목록</li>
<li>실시간 금지어 추가/삭제</li>
<li>금지어 등급관리 및 검색결과 확인</li>
<li>수동금지어 등록 및 파생금지어 등록가능</li>
</ul>
</div>
</div>
<div class="version-info">
<strong>현재 버전:</strong> v1.0.0 | <strong>최종 업데이트:</strong> 2025년 6월
</div>
</div>
<div class="tab-content" id="trademark">
<h2>🔍 지재권검색 사용법</h2>
<div class="alert alert-info">
키프리스(KIPRIS)에서 자동으로 지재권검색을 수행하고 결과를 정리해주는 기능입니다.
</div>
<div class="feature-card">
<h3>1⃣ 지재권검색 시작하기</h3>
<ul class="feature-list">
<li>검색하고 싶은 단어를 드래그 후 우클릭 - 지재권 검색 선택</li>
<li>검색하고 싶은 단어를 드래그 후 단축키(Search) - Ctrl + Shift + S</li>
<li>로그인 후 지재권검색 기능 활성화</li>
</ul>
</div>
<div class="feature-card">
<h3>2⃣ 검색 과정</h3>
<ul class="feature-list">
<li>해당단어를 키프리스에서 검색실행</li>
<li>검색 결과 유효한 권리상태만 필터링</li>
<li>지재권 해당하는 상품군 정리</li>
<li>실시간 진행상황 팝업 표시</li>
</ul>
</div>
<div class="feature-card">
<h3>3⃣ 결과 확인 및 추가</h3>
<ul class="feature-list">
<li>검색 완료 후 결과 요약 표시</li>
<li>검색결과를 내 금지어에 원클릭 등록가능</li>
<li>상표 이미지 및 상세정보 포함</li>
</ul>
</div>
<!--
<div class="alert alert-warning">
<strong>주의사항:</strong> 지재권검색은 API 호출량을 소모합니다. 회원등급에 따라 일일 사용량이 제한됩니다.
</div> -->
</div>
<div class="tab-content" id="translation">
<h2>🌐 멀티번역 사용법</h2>
<div class="alert alert-info">
하나의 텍스트나 문장을 여러 번역엔진으로 동시에 번역하여 번역기 마다 다른 애매한 내용들을 정리해줍니다.
</div>
<div class="feature-card">
<h3>1⃣ 번역 시작하기</h3>
<ul class="feature-list">
<li>번역할 텍스트를 드래그 후 우클릭 - 멀티번역 선택</li>
<li>번역할 텍스트를 드래그 후 단축키(Translate) - Ctrl + Shift + T</li>
<li>설정에서 선택한 엔진들로 번역</li>
</ul>
</div>
<div class="feature-card">
<h3>2⃣ 지원 엔진</h3>
<div class="grid">
<div>
<strong>무료 엔진:</strong>
<ul class="feature-list">
<li>구글 기계번역</li>
<li>myMemory 메모리 번역</li>
</ul>
</div>
<div>
<strong>프리미엄등급 엔진:</strong>
<ul class="feature-list">
<li>무료엔진</li>
<li>DeepL 딥러닝 번역</li>
</ul>
</div>
</div>
<div>
<strong>VIP등급 엔진 - 의역 지원:</strong>
<ul class="feature-list">
<li>챗GPT 의역 번역</li>
<li>구글 제미나이 의역 번역</li>
</ul>
</div>
</div>
<div class="feature-card">
<h3>3⃣ 번역 결과 활용</h3>
<ul class="feature-list">
<li>실시간 번역 결과 확인</li>
<li>자연스러운 딥러닝 엔진인 Deepl 사용</li>
<li>맥락을 이해하고 현재 단어나 문장이 어떤의미인지 어떨때 쓰이는지 파악</li>
</ul>
</div>
</div>
<div class="tab-content" id="timer">
<h2>⏰ 시간관리 기능</h2>
<div class="alert alert-info">
포모도로 기법을 활용한 자동 시간관리로 생산성을 향상시킵니다.
</div>
<div class="feature-card">
<h3>1⃣ 시간관리 설정</h3>
<ul class="feature-list">
<li>설정 페이지에서 "시간 알림" 활성화</li>
<li>작업 시간 설정 (기본: 60분)</li>
<li>휴식 시간 설정 (기본: 5분)</li>
<li>알림 방식 선택</li>
</ul>
</div>
<div class="feature-card">
<h3>2⃣ 자동 타이머 작동</h3>
<ul class="feature-list">
<li>로그인 시 자동으로 작업 타이머 시작</li>
<li>작업 시간 완료 시 휴식 모달 표시</li>
<li>휴식 시간 동안 추천 활동 제공</li>
<li>휴식 시간 동안 타냐대장경 제공</li>
<li>휴식 완료 후 자동으로 다음 작업 시작</li>
</ul>
</div>
<div class="feature-card">
<h3>3⃣ 휴식 시간 추천 활동</h3>
<div class="grid">
<div>
<strong>신체 활동:</strong>
<ul class="feature-list">
<li>가벼운 스트레칭</li>
<li>목과 어깨 마사지</li>
<li>잠깐 산책하기</li>
</ul>
</div>
<div>
<strong>정신 건강:</strong>
<ul class="feature-list">
<li>깊은 호흡 연습</li>
<li>간단한 명상</li>
<li>긍정적 생각하기</li>
</ul>
</div>
</div>
</div>
<div class="feature-card">
<h3>4⃣ 타이머 상태 확인</h3>
<ul class="feature-list">
<li>팝업에서 다음 휴식까지 남은 시간 확인</li>
<li>작업/휴식 사이클 진행상황 표시</li>
<li>일일 작업 시간 통계</li>
<li>생산성 향상 팁 제공</li>
</ul>
</div>
</div>
<div class="tab-content" id="banned-words">
<h2>🚫 금지어 관리</h2>
<div class="alert alert-info">
상품편집시 지식재산권을 피하기 위해금지 키워드를 관리합니다.
</div>
<div class="feature-card">
<h3>1⃣ 금지어 추가하기</h3>
<ul class="feature-list">
<li>설정 페이지에서 "금지어 관리" 선택</li>
<li>새로운 금지어 입력</li>
<li>"추가" 버튼 클릭하여 저장</li>
<li>실시간으로 금지어 목록 업데이트</li>
</ul>
</div>
<div class="feature-card">
<h3>2⃣ 금지어 관리</h3>
<ul class="feature-list">
<li>기존 금지어 목록 확인</li>
<li>불필요한 금지어 삭제</li>
<li>금지어 등급변경</li>
<li>금지어 검색 및 필터링</li>
</ul>
</div>
<div class="feature-card">
<h3>3⃣ 금지어 적용</h3>
<ul class="feature-list">
<li>편집알바생과 연동된 금지어 목록</li>
</ul>
</div>
<div class="alert alert-warning">
<strong>팁:</strong> 자주 나타나는 불필요한 키워드들을 금지어로 등록하면 편집알바생의 효율성이 크게 향상됩니다.
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="openBannedWords()">🚫 금지어 관리</button>
</div>
</div>
<div class="tab-content" id="sayings">
<h2>💬 타냐대장경</h2>
<div class="alert alert-info">
업무에 도움이 되는 타냐센세의 대장경과 조언을 모아놓은 특별한 기능입니다.
</div>
<div class="feature-card">
<h3>1⃣ 타냐대장경이란?</h3>
<ul class="feature-list">
<li>업무 효율성을 높이는 대장경 모음</li>
<li>동기부여와 영감을 주는 메시지</li>
<li>관리자가 승인한 검증된 조언</li>
</ul>
</div>
<div class="feature-card">
<h3>2⃣ 사용 방법</h3>
<ul class="feature-list">
<li>팝업에서 "타냐대장경" 버튼 클릭</li>
<li>오늘의 대장경 확인</li>
<li>대장경 카테고리별 탐색</li>
</ul>
</div>
<div class="feature-card">
<h3>3⃣ 대장경 카테고리</h3>
<div class="grid">
<div>
<strong>업무 효율성:</strong>
<ul class="feature-list">
<li>시간 관리 조언</li>
<li>생산성 향상 팁</li>
<li>마켓의 로직변화관리</li>
<li>타냐센세의 계절별 조언</li>
</ul>
</div>
<div>
<strong>동기부여:</strong>
<ul class="feature-list">
<li>성공 마인드셋</li>
<li>도전 정신 격려</li>
<li>긍정적 사고방식</li>
</ul>
</div>
</div>
</div>
</div>
<div class="tab-content" id="settings">
<h2>⚙️ 설정 가이드</h2>
<div class="alert alert-info">
확장 프로그램의 모든 기능을 개인 취향에 맞게 설정할 수 있습니다.
</div>
<div class="feature-card">
<h3>1⃣ 번역 설정</h3>
<ul class="feature-list">
<li>번역엔진 활성화</li>
</ul>
</div>
<div class="feature-card">
<h3>2⃣ 시간관리 설정</h3>
<ul class="feature-list">
<li>시간 알림 ON/OFF</li>
<li>작업 시간 설정 (분 단위)</li>
<li>휴식 시간 설정 (분 단위)</li>
<li>알림 방식 선택</li>
</ul>
</div>
<div class="feature-card">
<h3>3⃣ 찜 관리 설정</h3>
<ul class="feature-list">
<li>추후 업데이트</li>
<li>찜 품앗이</li>
</ul>
</div>
</div>
</div>
<script src="manual.js"></script>
</body>
</html>

38
wrmc_ext/manual.js Normal file
View File

@ -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') });
}

496
wrmc_ext/popup.html Normal file
View File

@ -0,0 +1,496 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>내차는언제타냐 통합확장기</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', sans-serif;
background: #f4f6f9;
padding: 20px;
width: 320px;
height: 100vh;
/* overflow: hidden; */
overflow-y: auto;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #2c3e50;
}
.form-group {
margin-bottom: 12px;
position: relative;
}
input[type="email"], input[type="password"] {
width: 100%;
padding: 10px 40px 10px 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus {
border-color: #3498db;
outline: none;
}
.tooltip {
font-size: 11px;
color: #999;
margin-top: 3px;
}
.error {
color: red;
font-size: 12px;
margin-top: 4px;
}
.checkbox-group {
display: flex;
align-items: center;
margin-top: 6px;
font-size: 13px;
}
.checkbox-group input {
margin-right: 6px;
}
.btn {
width: 100%;
padding: 10px;
background: #3498db;
border: none;
color: white;
font-weight: bold;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
}
.btn:disabled {
background: #bdc3c7;
}
#password-toggle {
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
color: #555;
user-select: none;
}
.status {
margin-top: 10px;
text-align: center;
font-size: 13px;
}
.loading {
text-align: center;
font-size: 14px;
color: #3498db;
}
.level {
margin-top: 10px;
text-align: center;
font-weight: bold;
color: #27ae60;
}
.debug-info {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
color: #6c757d;
}
.debug-info button {
display: block;
margin-top: 8px;
padding: 6px 12px;
font-size: 11px;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
transition: background-color 0.2s;
}
.debug-info button:hover {
background: #005a9e;
}
/* 로그인 화면과 사용자 정보 화면 여백 추가 */
#login-section {
padding: 20px 0;
}
#user-info-section {
padding: 20px 0;
}
/* 설정 버튼 스타일 */
.settings-btn {
background: #9b59b6;
margin-bottom: 8px;
}
.settings-btn:hover {
background: #8e44ad;
}
/* 관리 버튼 스타일 */
.management-buttons {
margin: 15px 0;
}
.management-btn {
background: #27ae60;
margin-bottom: 8px;
}
.management-btn:hover {
background: #219a52;
}
/* 모달 스타일 수정 */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.5);
display: none;
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: 15px auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 900px;
min-width: 400px;
min-height: 300px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.modal-header {
cursor: move;
padding: 10px;
margin: -20px -20px 20px -20px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
border-radius: 5px 5px 0 0;
user-select: none;
}
.resizer {
width: 10px;
height: 10px;
background: #6c757d;
position: absolute;
right: 0;
bottom: 0;
cursor: se-resize;
border-radius: 0 0 5px 0;
}
.resizer:hover {
background: #5a6268;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
color: white;
}
.close:hover {
opacity: 0.7;
}
.modal-body {
padding: 20px;
height: calc(100% - 56px); /* 헤더 높이를 제외한 나머지 */
overflow-y: auto;
}
/* 테이블 컨테이너 높이 조정 */
.banned-words-table-container {
height: calc(100% - 80px); /* 통계 정보 높이를 제외한 나머지 */
overflow: auto;
}
#banned-words-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 14px;
}
/* 테이블 헤더 고정 */
#banned-words-table thead {
position: sticky;
top: 0;
background: #f8f9fa;
z-index: 1;
}
/* 통계 정보 스타일 */
.stats-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
font-size: 14px;
font-weight: bold;
color: #495057;
text-align: center;
}
/* 테이블 스타일 */
.banned-words-table-container {
overflow-x: auto;
}
#banned-words-table th,
#banned-words-table td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: left;
white-space: nowrap;
}
#banned-words-table th {
background-color: #f8f9fa;
font-weight: bold;
}
#banned-words-table tr:nth-child(even) {
background-color: #f9f9f9;
}
/* 순번 열 스타일 */
#banned-words-table th:first-child,
#banned-words-table td:first-child {
width: 60px;
text-align: center;
}
/* 액션 버튼 스타일 */
.action-btn {
padding: 6px 10px;
margin: 2px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
display: inline-block;
min-width: 40px;
}
.view-btn {
background: #3498db;
color: white;
}
.edit-btn {
background: #f39c12;
color: white;
}
.delete-btn {
background: #e74c3c;
color: white;
}
.action-btn:hover {
opacity: 0.8;
}
/* 키프리스 결과 스타일 */
.kipris-results-container {
max-height: 500px;
overflow-y: auto;
}
.kipris-item {
border: 1px solid #ddd;
margin-bottom: 15px;
padding: 15px;
border-radius: 6px;
background: #f9f9f9;
}
.kipris-item h4 {
margin: 0 0 10px 0;
color: #2c3e50;
}
.kipris-field {
margin-bottom: 8px;
}
.kipris-field strong {
color: #34495e;
}
.kipris-drawing {
max-width: 100%;
height: auto;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 등급 표시 스타일 */
.grade-display {
display: inline-block;
padding: 4px 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #495057;
min-width: 60px;
text-align: center;
}
</style>
</head>
<body>
<h2>내차는언제타냐 통합확장 로그인</h2>
<!-- 디버그 정보 -->
<div class="debug-info" id="debug-info">
초기화 중...
</div>
<!-- 로그인 화면 -->
<div id="login-section">
<div class="form-group">
<input type="email" id="email" placeholder="이메일 주소" />
<div class="tooltip">가입한 이메일 주소를 입력하세요</div>
<div class="error" id="email-error"></div>
</div>
<div class="form-group">
<input type="password" id="password" placeholder="비밀번호" />
<span id="password-toggle">👁️</span>
<div class="tooltip">영문, 숫자 포함 6자 이상 입력</div>
<div class="error" id="password-error"></div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="save-login" />
<label for="save-login">로그인 정보 저장</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="auto-login" />
<label for="auto-login">자동 로그인</label>
</div>
<button class="btn" id="login-btn">로그인</button>
<div class="loading" id="loading" style="display: none;">🔄 로그인 중입니다...</div>
<div class="status" id="status"></div>
<div class="level" id="membership-level"></div>
</div>
<!-- 로그인 후 사용자 정보 출력 화면 -->
<div id="user-info-section" style="display: none;">
<h3>👋 내차는언제타냐 통합확장기</h3>
<p><strong>이메일:</strong> <span id="user-email"></span></p>
<p><strong>회원등급:</strong> <span id="user-level"></span></p>
<p><strong>오늘 호출량:</strong> <span id="user-usage"></span></p>
<p><strong>등급 만료일:</strong> <span id="user-expire"></span></p>
<!-- 휴식 시간 카운트다운 -->
<div id="break-timer-section" style="margin: 15px 0; padding: 10px; background: #e8f5e8; border-radius: 6px; border: 1px solid #c3e6c3;">
<p style="margin: 0; font-weight: bold; color: #2d5a2d;">⏰ 다음 휴식까지</p>
<p id="break-countdown" style="margin: 5px 0 0 0; font-size: 16px; font-weight: bold; color: #1e7e1e;">계산 중...</p>
</div>
<!-- 관리 버튼들 -->
<div class="management-buttons">
<button class="btn management-btn" id="settings-btn">⚙️ 설정</button>
<button class="btn management-btn" id="banned-words-btn">🚫 금지어 관리</button>
<button class="btn management-btn" id="sayings-btn">💬 타냐대장경</button>
<button class="btn management-btn" id="zzim-btn" disabled>💝 찜관리</button>
<button class="btn management-btn" id="manual-btn">📚 매뉴얼</button>
</div>
<button class="btn" id="logout-btn">로그아웃</button>
</div>
<!-- 스크립트를 일반 스크립트로 변경 -->
<script src="popup.js"></script>
<!-- 금지어 관리 모달 -->
<div id="banned-words-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>🚫 금지어 관리</h3>
<span class="close" id="close-banned-words">&times;</span>
</div>
<div class="modal-body">
<div id="banned-words-stats" class="stats-info">
<!-- 통계 정보가 여기에 표시됩니다 -->
</div>
<div class="banned-words-table-container">
<table id="banned-words-table">
<thead>
<tr>
<th>순번</th>
<th>금지어</th>
<th>등급</th>
<th>작업</th>
</tr>
</thead>
<tbody id="banned-words-tbody">
<!-- 동적으로 생성됨 -->
</tbody>
</table>
</div>
<div class="loading" id="banned-words-loading" style="display: none;">
🔄 금지어 목록을 불러오는 중...
</div>
</div>
</div>
</div>
<!-- 키프리스 결과 모달 -->
<div id="kipris-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>🔍 키프리스 검색 결과</h3>
<span class="close" id="close-kipris">&times;</span>
</div>
<div class="modal-body">
<div id="kipris-word-title"></div>
<div class="kipris-results-container">
<div id="kipris-results">
<!-- 동적으로 생성됨 -->
</div>
</div>
<div class="loading" id="kipris-loading" style="display: none;">
🔄 키프리스 결과를 불러오는 중...
</div>
</div>
</div>
</div>
</body>
</html>

1864
wrmc_ext/popup.js Normal file

File diff suppressed because it is too large Load Diff

296
wrmc_ext/rest-modal.html Normal file
View File

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>휴식 시간</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #333;
overflow: hidden;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.modal-container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 90%;
text-align: center;
position: relative;
animation: modalFadeIn 0.5s ease-out;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-50px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.close-btn {
position: absolute;
top: 15px;
right: 20px;
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.close-btn:hover {
background: #f0f0f0;
color: #666;
}
.rest-icon {
font-size: 4rem;
margin-bottom: 20px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
.rest-title {
font-size: 2.5rem;
color: #4a5568;
margin-bottom: 15px;
font-weight: 700;
}
.rest-subtitle {
font-size: 1.2rem;
color: #718096;
margin-bottom: 30px;
}
.timer-display {
font-size: 3rem;
color: #667eea;
font-weight: bold;
margin-bottom: 30px;
font-family: 'Courier New', monospace;
}
.activity-section {
background: #f8f9fa;
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
}
.activity-title {
font-size: 1.3rem;
color: #4a5568;
margin-bottom: 15px;
font-weight: 600;
}
.activity-suggestion {
font-size: 1.1rem;
color: #2d3748;
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.saying-section {
background: linear-gradient(135deg, #ffeaa7, #fab1a0);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
color: #2d3748;
}
.saying-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.saying-content {
font-size: 1.1rem;
font-style: italic;
line-height: 1.6;
margin-bottom: 10px;
}
.saying-author {
font-size: 0.9rem;
color: #666;
text-align: right;
}
.actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.progress-bar {
width: 100%;
height: 6px;
background: #e2e8f0;
border-radius: 3px;
overflow: hidden;
margin-top: 20px;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 3px;
transition: width 1s ease;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="modal-overlay" id="modalOverlay">
<div class="modal-container">
<button class="close-btn" id="closeBtn" title="ESC키로 닫기">×</button>
<div class="rest-icon">🧘‍♀️</div>
<h1 class="rest-title">휴식 시간입니다!</h1>
<p class="rest-subtitle">열심히 일한 당신, 잠시 휴식을 취하세요</p>
<div class="timer-display" id="timerDisplay">05:00</div>
<div class="activity-section">
<h3 class="activity-title">💡 추천 활동</h3>
<div class="activity-suggestion" id="activitySuggestion">
<div class="loading-spinner"></div>
활동을 불러오는 중...
</div>
</div>
<div class="saying-section">
<h3 class="saying-title">
<span></span>
<span>타냐의 한마디</span>
<span></span>
</h3>
<div class="saying-content" id="sayingContent">
<div class="loading-spinner"></div>
어록을 불러오는 중...
</div>
<div class="saying-author" id="sayingAuthor"></div>
</div>
<div class="actions">
<button class="btn btn-secondary" id="skipBtn">휴식 건너뛰기</button>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
</div>
<script src="rest-modal.js"></script>
</body>
</html>

304
wrmc_ext/rest-modal.js Normal file
View File

@ -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 = `
<div class="rest-icon">🚀</div>
<h1 class="rest-title">휴식 완료!</h1>
<p class="rest-subtitle">이제 다시 열심히 월매출 1억을 향해 달려가세요!</p>
<div style="font-size: 1.2rem; color: #667eea; margin-top: 20px;">
💪 화이팅! 성공은 바로 앞에 있습니다!
</div>
<div class="actions" style="margin-top: 30px;">
<button class="btn btn-primary" onclick="window.close()">확인</button>
</div>
`;
}
closeModal() {
if (this.timer) {
clearInterval(this.timer);
}
// 창 닫기
window.close();
}
}
// 페이지 로드 시 휴식 모달 초기화
document.addEventListener('DOMContentLoaded', () => {
const restModal = new RestModal();
restModal.init();
});

1195
wrmc_ext/sayings.html Normal file

File diff suppressed because it is too large Load Diff

2845
wrmc_ext/sayings.js Normal file

File diff suppressed because it is too large Load Diff

582
wrmc_ext/settings.html Normal file
View File

@ -0,0 +1,582 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>설정 - 내차는언제타냐 통합확장기</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 28px;
font-weight: 600;
}
.header p {
margin: 10px 0 0 0;
opacity: 0.9;
font-size: 16px;
}
/* 탭 스타일 */
.tab-container {
display: flex;
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
}
.tab-button {
flex: 1;
padding: 15px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 500;
color: #6c757d;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
}
.tab-button:hover {
background: #e9ecef;
color: #495057;
}
.tab-button.active {
color: #9b59b6;
border-bottom-color: #9b59b6;
background: #fff;
}
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
.content {
padding: 0;
}
.membership-info {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border: 1px solid #90caf9;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
text-align: center;
}
.membership-title {
font-size: 18px;
font-weight: bold;
color: #1976d2;
margin-bottom: 10px;
}
.membership-engines {
color: #424242;
line-height: 1.6;
font-size: 14px;
}
.section {
margin-bottom: 30px;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.section-description {
color: #6c757d;
font-size: 14px;
margin-bottom: 20px;
line-height: 1.5;
}
.engine-list {
display: grid;
gap: 15px;
}
.engine-item {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
}
.engine-item:hover {
border-color: #9b59b6;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.engine-item.disabled {
opacity: 0.6;
background: #f1f3f4;
border-color: #d1d5db;
}
.engine-item.disabled:hover {
transform: none;
box-shadow: none;
border-color: #d1d5db;
}
.engine-info {
flex: 1;
}
.engine-name {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
.engine-badge {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
}
.badge-free {
background: #d4edda;
color: #155724;
}
.badge-premium {
background: #fff3cd;
color: #856404;
}
.badge-vip {
background: #f8d7da;
color: #721c24;
}
.engine-description {
color: #6c757d;
font-size: 14px;
line-height: 1.4;
margin-bottom: 8px;
}
.engine-features {
font-size: 12px;
color: #9b59b6;
font-weight: 500;
}
.toggle-switch {
position: relative;
width: 60px;
height: 30px;
background: #ccc;
border-radius: 30px;
cursor: pointer;
transition: background 0.3s;
margin-left: 15px;
}
.toggle-switch.active {
background: #9b59b6;
}
.toggle-switch.disabled {
background: #e9ecef;
cursor: not-allowed;
}
.toggle-switch::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 24px;
height: 24px;
border-radius: 50%;
background: white;
transition: left 0.3s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.toggle-switch.active::after {
left: 33px;
}
/* 시간 알람 설정 스타일 */
.time-alarm-section {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.time-input-group {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.time-input-group label {
font-weight: 500;
color: #495057;
min-width: 80px;
}
.time-input-group input {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
width: 80px;
}
.time-input-group span {
color: #6c757d;
font-size: 14px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
}
.checkbox-group label {
font-size: 14px;
color: #495057;
}
.actions {
text-align: center;
padding: 20px 0;
border-top: 1px solid #e9ecef;
margin-top: 30px;
}
.btn {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(155, 89, 182, 0.4);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d, #5a6268);
}
.btn-secondary:hover {
box-shadow: 0 5px 15px rgba(108, 117, 125, 0.4);
}
.loading {
display: none;
text-align: center;
color: #9b59b6;
font-size: 14px;
margin-top: 15px;
}
.message {
padding: 15px;
border-radius: 8px;
margin: 15px 0;
font-size: 14px;
display: none;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #6c757d;
font-size: 14px;
}
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 8px;
}
.header {
padding: 20px;
}
.tab-content {
padding: 20px;
}
.engine-item {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.toggle-switch {
margin-left: 0;
align-self: flex-end;
}
.time-input-group {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚙️ 설정</h1>
<p>번역 엔진 및 시간 알람 설정을 관리합니다</p>
</div>
<!-- 탭 메뉴 -->
<div class="tab-container">
<button class="tab-button active" data-tab="translation">🌐 번역 엔진</button>
<button class="tab-button" data-tab="time-alarm">⏰ 시간 알람</button>
</div>
<div class="content">
<!-- 번역 엔진 설정 탭 -->
<div class="tab-content active" id="translation-tab">
<!-- 회원등급 정보 -->
<div class="membership-info">
<div class="membership-title">현재 회원등급: <span id="current-membership">로딩 중...</span></div>
<div class="membership-engines" id="membership-engines-info">회원 정보를 불러오는 중입니다...</div>
</div>
<!-- 번역 엔진 설정 -->
<div class="section">
<div class="section-title">
<span>📋</span>
노출할 번역 엔진 선택
</div>
<div class="section-description">
체크된 엔진만 멀티번역 결과에 표시됩니다. 회원등급에 따라 일부 엔진은 사용할 수 없습니다.
</div>
<div class="engine-list">
<div class="engine-item" data-engine="google">
<div class="engine-info">
<div class="engine-name">
<span>🌐</span>
Google 번역
<span class="engine-badge badge-free">무료</span>
</div>
<div class="engine-description">
빠른 속도와 다양한 언어 지원을 제공하는 구글의 무료 번역 서비스
</div>
<div class="engine-features">
✓ 100개 이상 언어 지원 ✓ 실시간 번역 ✓ 무료 사용
</div>
</div>
<div class="toggle-switch" data-engine="google"></div>
</div>
<div class="engine-item" data-engine="mymemory">
<div class="engine-info">
<div class="engine-name">
<span>💾</span>
MyMemory 번역
<span class="engine-badge badge-free">무료</span>
</div>
<div class="engine-description">
번역 메모리 기반으로 높은 품질의 번역을 제공하는 무료 서비스
</div>
<div class="engine-features">
✓ 번역 메모리 활용 ✓ 높은 번역 품질 ✓ 무료 사용
</div>
</div>
<div class="toggle-switch" data-engine="mymemory"></div>
</div>
<div class="engine-item" data-engine="deepl">
<div class="engine-info">
<div class="engine-name">
<span>🎯</span>
DeepL 번역
<span class="engine-badge badge-premium">프리미엄</span>
</div>
<div class="engine-description">
높은 번역 품질로 유명한 AI 번역 서비스
</div>
<div class="engine-features">
✓ 최고 품질 번역 ✓ 문맥 이해 ✓ 프리미엄 이상 사용 가능
</div>
</div>
<div class="toggle-switch" data-engine="deepl"></div>
</div>
<div class="engine-item" data-engine="openai">
<div class="engine-info">
<div class="engine-name">
<span>🤖</span>
OpenAI ChatGPT
<span class="engine-badge badge-vip">VIP</span>
</div>
<div class="engine-description">
문맥을 이해하는 고품질 AI 번역, 의역 및 설명 포함
</div>
<div class="engine-features">
✓ AI 기반 번역 ✓ 문맥 이해 ✓ 의역 제공 ✓ VIP 전용
</div>
</div>
<div class="toggle-switch" data-engine="openai"></div>
</div>
<div class="engine-item disabled" data-engine="gemini">
<div class="engine-info">
<div class="engine-name">
<span>💎</span>
Google Gemini
<span class="engine-badge badge-vip">비활성화</span>
</div>
<div class="engine-description">
현재 사용할 수 없습니다 (서비스 점검 중)
</div>
<div class="engine-features">
✗ 현재 사용 불가 ✗ 서비스 점검 중
</div>
</div>
<div class="toggle-switch disabled" data-engine="gemini"></div>
</div>
</div>
</div>
</div>
<!-- 시간 알람 설정 탭 -->
<div class="tab-content" id="time-alarm-tab">
<div class="section">
<div class="section-title">
<span></span>
시간 알람 설정
</div>
<div class="section-description">
작업 시간과 휴식 시간을 설정하여 건강한 작업 패턴을 유지하세요.
</div>
<div class="time-alarm-section">
<div class="time-input-group">
<label>시간 알람:</label>
<div class="toggle-switch" id="timeAlarmToggle"></div>
<span>알람 활성화/비활성화</span>
</div>
<div class="time-input-group">
<label>작업 시간:</label>
<input type="number" id="workTimeInput" min="1" max="480" value="60">
<span>분 (작업 후 휴식 알림)</span>
</div>
<div class="time-input-group">
<label>휴식 시간:</label>
<input type="number" id="restTimeInput" min="1" max="60" value="5">
<span>분 (휴식 시간 길이)</span>
</div>
<div class="checkbox-group">
<input type="checkbox" id="autoZzimCheckbox" disabled>
<label for="autoZzimCheckbox">휴식 중 자동 찜 기능 활성화</label>
</div>
</div>
</div>
</div>
<div class="message" id="message"></div>
<div class="loading" id="loading">⏳ 설정을 저장하는 중...</div>
<div class="actions">
<button class="btn" id="save-settings">💾 설정 저장</button>
<button class="btn btn-secondary" id="reset-settings">🔄 기본값으로 복원</button>
</div>
</div>
<div class="footer">
<p>© 2024 내차는언제타냐 통합확장기 - 번역 엔진 및 시간 알람 설정</p>
</div>
</div>
<script src="settings.js"></script>
</body>
</html>

546
wrmc_ext/settings.js Normal file
View File

@ -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 = `
<strong>사용 가능한 번역 엔진:</strong><br>
${engineNames.join(', ')}<br><br>
<strong>등급별 혜택:</strong><br>
기본: Google, MyMemory (무료 엔진)<br>
프리미엄: + DeepL (고품질 엔진)<br>
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('설정 페이지 종료');
});

288
wrmc_ext/zzim.html Normal file
View File

@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>찜관리</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
margin: 0;
padding: 20px;
background: #f8f9fa;
}
.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.stat-title {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
}
.stat-limit {
font-size: 12px;
color: #888;
margin-top: 5px;
}
.section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.market-form {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 10px;
align-items: center;
margin-bottom: 15px;
}
.market-form input[type="text"] {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.market-form input[type="text"]:focus {
outline: none;
border-color: #3498db;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: background-color 0.2s;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #219a52;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-large {
padding: 15px 30px;
font-size: 16px;
margin: 10px;
}
.market-list {
margin-top: 20px;
}
.market-item {
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
margin-bottom: 10px;
background: #f9f9f9;
}
.market-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.market-info {
flex: 1;
}
.market-name {
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.market-nickname {
color: #666;
font-size: 12px;
margin-bottom: 5px;
}
.market-url {
color: #888;
font-size: 12px;
word-break: break-all;
}
.market-actions {
display: flex;
gap: 5px;
}
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.loading {
text-align: center;
color: #3498db;
font-size: 14px;
margin: 20px 0;
}
.error {
color: #e74c3c;
font-size: 14px;
margin: 10px 0;
}
.success {
color: #27ae60;
font-size: 14px;
margin: 10px 0;
}
.url-prefix {
background: #f8f9fa;
padding: 8px 12px;
border: 1px solid #ddd;
border-right: none;
border-radius: 4px 0 0 4px;
font-size: 14px;
color: #666;
white-space: nowrap;
}
.url-input {
border-radius: 0 4px 4px 0 !important;
border-left: none !important;
}
</style>
</head>
<body>
<div class="stats-container">
<div class="stat-card">
<div class="stat-title">오늘 찜한 갯수</div>
<div class="stat-value" id="today-zzim-count">0</div>
<div class="stat-limit">/ <span id="today-zzim-limit">100</span></div>
</div>
<div class="stat-card">
<div class="stat-title">찜 마일리지</div>
<div class="stat-value" id="zzim-mileage">0</div>
<div class="stat-limit">/ <span id="zzim-mileage-limit">1000</span></div>
</div>
</div>
<div class="section">
<div class="section-title">
🏪 내 마켓 등록
</div>
<div class="market-form">
<span class="url-prefix">https://smartstore.naver.com/</span>
<input type="text" id="market-url" placeholder="마켓 주소 (예: abcd1234)" class="url-input">
<button class="btn btn-primary" id="add-market-btn">등록</button>
</div>
<div class="market-form">
<label>마켓 이름:</label>
<input type="text" id="market-name" placeholder="마켓 이름">
<span></span>
</div>
<div class="market-form">
<label>마켓 별명:</label>
<input type="text" id="market-nickname" placeholder="마켓 별명">
<span></span>
</div>
<div class="market-list">
<h4>내 마켓 목록</h4>
<div id="my-markets-list">
<!-- 마켓 목록이 여기에 표시됩니다 -->
</div>
</div>
</div>
<div class="section">
<div class="section-title">
🎯 찜하기 작업
</div>
<div class="action-buttons">
<button class="btn btn-success btn-large" id="my-market-zzim-btn">
💝 내 마켓 찜하기
</button>
<button class="btn btn-warning btn-large" id="mutual-zzim-btn">
🤝 품앗이 찜하기
</button>
</div>
<div id="zzim-status" class="loading" style="display: none;"></div>
<div id="zzim-error" class="error" style="display: none;"></div>
<div id="zzim-success" class="success" style="display: none;"></div>
</div>
<script src="zzim.js"></script>
</body>
</html>

616
wrmc_ext/zzim.js Normal file
View File

@ -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 = '<p style="text-align: center; color: #666; padding: 20px;">등록된 마켓이 없습니다.</p>';
return;
}
marketsList.innerHTML = markets.map(market => `
<div class="market-item" data-id="${market.id}">
<div class="market-header">
<div class="market-info">
<div class="market-name">${market.market_name}</div>
<div class="market-nickname">${market.market_nickname}</div>
<div class="market-url">${market.market_url}</div>
</div>
<div class="market-actions">
<button onclick="zzimManager.editMarket(${market.id})" class="btn btn-warning">수정</button>
<button onclick="zzimManager.deleteMarket(${market.id})" class="btn btn-danger">삭제</button>
</div>
</div>
</div>
`).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();
});

View File

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