From b25d85099bcf2508075c3bcd07a6044f5324ba12 Mon Sep 17 00:00:00 2001
From: 9700X_PC <9700X_PC@gmail.com>
Date: Thu, 27 Nov 2025 15:02:01 +0900
Subject: [PATCH] Refactor background.js and content.js for improved text
selection and context menu handling; update Supabase URLs in multiple files;
enhance UI elements in zzim.html; add new features for managing zzim
operations and user settings.
---
wrmc_ext/background.js | 1257 ++++++++++++++++++++++++++-
wrmc_ext/bannedWords.js | 4 +-
wrmc_ext/content.js | 1095 +++++++++++++++++++++++-
wrmc_ext/manifest.json | 11 +-
wrmc_ext/popup.html | 3 +-
wrmc_ext/popup.js | 8 +-
wrmc_ext/rest-modal.js | 8 +-
wrmc_ext/sayings.js | 4 +-
wrmc_ext/settings.js | 4 +-
wrmc_ext/zzim.html | 509 ++++++++++-
wrmc_ext/zzim.js | 1794 +++++++++++++++++++++++++++++++++------
패킹.md | 1 +
12 files changed, 4338 insertions(+), 360 deletions(-)
create mode 100644 패킹.md
diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js
index 9c53e72..b516628 100644
--- a/wrmc_ext/background.js
+++ b/wrmc_ext/background.js
@@ -4,7 +4,7 @@ chrome.runtime.onInstalled.addListener(() => {
// 컨텍스트 메뉴를 개별적으로 생성 (단축키 포함)
chrome.contextMenus.create({
id: "searchTrademark",
- title: "지재권 검색 (Ctrl+Shift+S)",
+ title: "지재권검색 (Ctrl+Shift+S)",
contexts: ["selection"]
});
@@ -16,8 +16,8 @@ chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
- // 새 어록 감지 알람 생성 (1분마다)
- chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 });
+ // 새 어록 감지 알람 생성 (5분마다)
+ chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 });
// 초기 마지막 확인 시간 설정
chrome.storage.local.set({ lastSayingsCheck: Date.now() });
@@ -35,21 +35,226 @@ chrome.commands.onCommand.addListener(async (command) => {
return;
}
- // 선택된 텍스트 가져오기
+ // 선택된 텍스트 가져오기 (개선된 방식)
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
function: () => {
- return window.getSelection().toString().trim();
+ // 다양한 방식으로 선택된 텍스트 감지
+ function getSelectedTextAdvanced() {
+ let selectedText = '';
+ console.log('[텍스트선택] 선택된 텍스트 감지 시작');
+
+ // 1. 기본 window.getSelection() 시도
+ const selection = window.getSelection();
+ if (selection && selection.toString().trim()) {
+ selectedText = selection.toString().trim();
+ console.log('[텍스트선택] window.getSelection()으로 감지:', selectedText);
+ return selectedText;
+ }
+
+ // 2. document.getSelection() 시도
+ if (document.getSelection) {
+ const docSelection = document.getSelection();
+ if (docSelection && docSelection.toString().trim()) {
+ selectedText = docSelection.toString().trim();
+ console.log('[텍스트선택] document.getSelection()으로 감지:', selectedText);
+ return selectedText;
+ }
+ }
+
+ // 3. activeElement에서 선택된 텍스트 확인 (input, textarea)
+ const activeElement = document.activeElement;
+ if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
+ const start = activeElement.selectionStart;
+ const end = activeElement.selectionEnd;
+ if (start !== end && start !== null && end !== null) {
+ selectedText = activeElement.value.substring(start, end).trim();
+ console.log('[텍스트선택] activeElement에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+
+ // 4. contentEditable 요소에서 선택된 텍스트 확인
+ const editableElements = document.querySelectorAll('[contenteditable="true"]');
+ for (const element of editableElements) {
+ const elementSelection = element.ownerDocument.getSelection();
+ if (elementSelection && elementSelection.toString().trim()) {
+ selectedText = elementSelection.toString().trim();
+ console.log('[텍스트선택] contentEditable에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+
+ // 5. iframe 내부 확인
+ const iframes = document.querySelectorAll('iframe');
+ for (const iframe of iframes) {
+ try {
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+ const iframeSelection = iframeDoc.getSelection();
+ if (iframeSelection && iframeSelection.toString().trim()) {
+ selectedText = iframeSelection.toString().trim();
+ console.log('[텍스트선택] iframe에서 감지:', selectedText);
+ return selectedText;
+ }
+ } catch (e) {
+ // 크로스 오리진 iframe은 접근 불가
+ console.log('[텍스트선택] iframe 접근 제한:', e.message);
+ }
+ }
+
+ // 6. Shadow DOM 확인
+ function checkShadowDom(element) {
+ if (element.shadowRoot) {
+ const shadowSelection = element.shadowRoot.getSelection();
+ if (shadowSelection && shadowSelection.toString().trim()) {
+ return shadowSelection.toString().trim();
+ }
+ }
+
+ for (const child of element.children) {
+ const result = checkShadowDom(child);
+ if (result) return result;
+ }
+ return null;
+ }
+
+ const shadowResult = checkShadowDom(document.body);
+ if (shadowResult) {
+ console.log('[텍스트선택] Shadow DOM에서 감지:', shadowResult);
+ return shadowResult;
+ }
+
+ // 7. 특정 컨테이너 내부 확인 (ice-container 등)
+ const containers = document.querySelectorAll('.ice-container, [class*="container"], [class*="chat"], [class*="message"], [class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], [data-testid*="message"], [role="textbox"], [contenteditable]');
+ for (const container of containers) {
+ try {
+ const containerSelection = container.ownerDocument.getSelection();
+ if (containerSelection && containerSelection.toString().trim()) {
+ // 선택 범위가 해당 컨테이너 내부인지 확인
+ const range = containerSelection.getRangeAt(0);
+ if (range && (container.contains(range.commonAncestorContainer) ||
+ container.contains(range.startContainer) ||
+ container.contains(range.endContainer))) {
+ selectedText = containerSelection.toString().trim();
+ console.log('[텍스트선택] 특정 컨테이너에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+ } catch (e) {
+ console.log('[텍스트선택] 컨테이너 확인 오류:', e.message);
+ }
+ }
+
+ // 8. 모든 요소에서 선택 상태 확인 (최후의 수단)
+ const allElements = document.querySelectorAll('*');
+ for (const element of allElements) {
+ try {
+ // 요소가 포커스되어 있고 선택된 텍스트가 있는지 확인
+ if (element === document.activeElement || element.contains(document.activeElement)) {
+ const computedStyle = window.getComputedStyle(element);
+ if (computedStyle.userSelect !== 'none') {
+ const elementText = element.textContent || element.innerText;
+ if (elementText && elementText.trim()) {
+ // 마우스로 선택된 영역이 이 요소 내부에 있는지 확인
+ const selection = window.getSelection();
+ if (selection && selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0);
+ if (element.contains(range.commonAncestorContainer)) {
+ selectedText = selection.toString().trim();
+ if (selectedText) {
+ console.log('[텍스트선택] 활성 요소에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (e) {
+ // 무시
+ }
+ }
+
+ // 9. 클립보드에서 최근 복사된 텍스트 확인 (사용자가 Ctrl+C를 눌렀을 가능성)
+ try {
+ // 참고: 보안상의 이유로 클립보드 읽기는 제한적임
+ console.log('[텍스트선택] 클립보드 확인은 보안 제약으로 제한됨');
+ } catch (e) {
+ console.log('[텍스트선택] 클립보드 접근 불가:', e.message);
+ }
+
+ console.log('[텍스트선택] 선택된 텍스트를 찾을 수 없음');
+ return '';
+ }
+
+ return getSelectedTextAdvanced();
}
});
const selectedText = results[0]?.result;
if (!selectedText) {
+ // 클립보드에서 텍스트 가져오기 시도
+ try {
+ const clipboardText = await navigator.clipboard.readText();
+ if (clipboardText && clipboardText.trim() && clipboardText.length < 1000) {
+ console.log('[단축키] 클립보드에서 텍스트 사용:', clipboardText.substring(0, 50));
+
+ // 클립보드 텍스트로 처리 계속
+ await ensureContentScriptReady(tab.id);
+
+ if (command === 'trademark-search') {
+ try {
+ await chrome.tabs.sendMessage(tab.id, {
+ action: "showLoading",
+ message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 지재권 검색 중... (클립보드 사용)`
+ });
+ } catch (loadingError) {
+ console.log('[단축키-지재권검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
+ }
+
+ await handleTrademarkSearch(clipboardText.trim(), tab);
+
+ try {
+ await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
+ } catch (e) {
+ console.log('[단축키-지재권검색] 로딩 인디케이터 제거 실패 (무시):', e.message);
+ }
+
+ } else if (command === 'multi-translate') {
+ try {
+ await chrome.tabs.sendMessage(tab.id, {
+ action: "showLoading",
+ message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 번역 중... (클립보드 사용)`
+ });
+ } catch (loadingError) {
+ console.log('[단축키-멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
+ }
+
+ await handleMultiTranslate({ selectionText: clipboardText.trim() });
+
+ try {
+ await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
+ } catch (e) {
+ console.log('[단축키-멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message);
+ }
+
+ } else if (command === 'korean-to-chinese') {
+ await handleKoreanToChinese(clipboardText.trim(), tab);
+ } else if (command === 'direct-translate') {
+ await handleDirectTranslation(clipboardText.trim(), tab);
+ }
+
+ return; // 클립보드 처리 완료
+ }
+ } catch (clipboardError) {
+ console.log('[단축키] 클립보드 읽기 실패:', clipboardError.message);
+ }
+
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '텍스트 선택 필요',
- message: '먼저 텍스트를 선택해주세요.'
+ message: 'AliWangWang 등의 앱에서는 텍스트를 정확히 드래그 선택 후 단축키를 눌러주세요. 선택이 안 되면 Ctrl+C로 복사 후 다시 시도해보세요.'
});
return;
}
@@ -100,6 +305,8 @@ chrome.commands.onCommand.addListener(async (command) => {
} else if (command === 'korean-to-chinese') {
await handleKoreanToChinese(selectedText, tab);
+ } else if (command === 'direct-translate') {
+ await handleDirectTranslation(selectedText, tab);
}
} catch (error) {
@@ -139,10 +346,10 @@ async function checkForNewSayings() {
lastSayingsCheck = result.lastSayingsCheck;
} catch (storageError) {
console.error("[background.js] 스토리지 접근 오류:", storageError);
- lastSayingsCheck = Date.now() - 60000; // 기본값: 1분 전
+ lastSayingsCheck = Date.now() - 300000; // 기본값: 1분 전
}
- const lastCheckTime = lastSayingsCheck || Date.now() - 60000;
+ const lastCheckTime = lastSayingsCheck || Date.now() - 300000;
// 액세스 토큰 가져오기 (안전한 방식)
let access_token;
@@ -160,8 +367,8 @@ async function checkForNewSayings() {
}
// 올바른 Supabase URL과 헤더 사용
- const SUPABASE_URL = "http://146.56.101.199:8000";
- const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
+ const SUPABASE_URL = "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 새 어록 API 호출
const apiUrl = `${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${new Date(lastCheckTime).toISOString()}&admin_approval=eq.true&order=created_at.desc`;
@@ -375,8 +582,8 @@ async function handleTrademarkSearch(keyword, tab) {
});
// 3. 사용자 정보 및 회원등급 확인
- const SUPABASE_URL = "http://146.56.101.199:8000";
- const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
+ const SUPABASE_URL = "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 사용자 기본 정보 가져오기 (토큰 검증)
const authUrl = `${SUPABASE_URL}/auth/v1/user`;
@@ -1144,8 +1351,8 @@ async function getApiKey(service) {
try {
console.log(`[background.js] ${service} API 키 조회 시작`);
- const SUPABASE_URL = "http://146.56.101.199:8000";
- const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
+ const SUPABASE_URL = "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 먼저 간단한 테스트 호출 (인증 없이)
const testUrl = `${SUPABASE_URL}/rest/v1/api_keys?select=*&limit=1`;
@@ -1762,6 +1969,7 @@ async function handleKoreanToChinese(selectedText, tab) {
if (!selectedText) {
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '텍스트 선택 필요',
message: '번역할 텍스트를 먼저 선택해주세요.'
});
@@ -1804,6 +2012,7 @@ async function handleKoreanToChinese(selectedText, tab) {
// 한국어도 중국어도 아닌 경우
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '지원하지 않는 언어',
message: '한국어 또는 중국어 텍스트를 선택해주세요.'
});
@@ -1811,14 +2020,29 @@ async function handleKoreanToChinese(selectedText, tab) {
}
// 선택된 텍스트를 번역된 텍스트로 대체
- await chrome.scripting.executeScript({
- target: { tabId: tab.id },
- function: replaceSelectedText,
- args: [translatedText]
- });
+ console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText);
+
+ try {
+ const result = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ function: replaceSelectedText,
+ args: [translatedText]
+ });
+
+ console.log('[background.js] replaceSelectedText 실행 결과:', result);
+
+ if (result && result[0] && result[0].result !== undefined) {
+ console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result);
+ } else {
+ console.log('[background.js] 스크립트 실행 완료, 반환값 없음');
+ }
+ } catch (scriptError) {
+ console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError);
+ }
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '번역 완료',
message: `${direction}로 변환되었습니다.`
});
@@ -1827,6 +2051,7 @@ async function handleKoreanToChinese(selectedText, tab) {
console.error('[background.js] 한국어↔중국어 번역 중 오류:', error);
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '번역 오류',
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
});
@@ -1835,31 +2060,363 @@ async function handleKoreanToChinese(selectedText, tab) {
// 범용 번역 함수
async function translateText(text, sourceLang, targetLang) {
- const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`);
+ console.log(`[translateText] 번역 시작: ${sourceLang} -> ${targetLang}, 텍스트: ${text}`);
- if (!response.ok) {
- throw new Error('Google 번역 요청 실패');
+ // 1. Google 번역 시도
+ try {
+ const googleUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
+ console.log('[translateText] Google 번역 시도:', googleUrl);
+
+ const response = await fetch(googleUrl, {
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+
+ if (data && data[0] && data[0][0] && data[0][0][0]) {
+ const translatedText = data[0][0][0];
+ console.log('[translateText] Google 번역 성공:', translatedText);
+ return translatedText;
+ }
+ }
+
+ console.log('[translateText] Google 번역 응답 형식 오류');
+ } catch (error) {
+ console.error('[translateText] Google 번역 실패:', error);
}
- const data = await response.json();
-
- if (!data || !data[0] || !data[0][0] || !data[0][0][0]) {
- throw new Error('Google 번역 응답 형식 오류');
+ // 2. MyMemory 번역 시도
+ try {
+ console.log('[translateText] MyMemory 번역 시도');
+ const myMemoryUrl = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`;
+
+ const response = await fetch(myMemoryUrl);
+
+ if (response.ok) {
+ const data = await response.json();
+
+ if (data && data.responseData && data.responseData.translatedText) {
+ const translatedText = data.responseData.translatedText;
+ console.log('[translateText] MyMemory 번역 성공:', translatedText);
+ return translatedText;
+ }
+ }
+
+ console.log('[translateText] MyMemory 번역 응답 형식 오류');
+ } catch (error) {
+ console.error('[translateText] MyMemory 번역 실패:', error);
}
- return data[0][0][0];
+ // 3. 간단한 사전 기반 번역 (한중/중한만)
+ if ((sourceLang === 'ko' && targetLang === 'zh') || (sourceLang === 'zh' && targetLang === 'ko')) {
+ try {
+ console.log('[translateText] 사전 기반 번역 시도');
+ const dictResult = await simpleDictionaryTranslate(text, sourceLang, targetLang);
+ if (dictResult) {
+ console.log('[translateText] 사전 기반 번역 성공:', dictResult);
+ return dictResult;
+ }
+ } catch (error) {
+ console.error('[translateText] 사전 기반 번역 실패:', error);
+ }
+ }
+
+ // 4. 모든 번역 실패 시 원본 텍스트 반환
+ console.log('[translateText] 모든 번역 방법 실패, 원본 텍스트 반환');
+ throw new Error('모든 번역 엔진에서 번역에 실패했습니다');
+}
+
+// 간단한 사전 기반 번역 함수
+async function simpleDictionaryTranslate(text, sourceLang, targetLang) {
+ // 기본적인 한중/중한 단어 사전
+ const koreanToChinese = {
+ '안녕하세요': '你好',
+ '감사합니다': '谢谢',
+ '죄송합니다': '对不起',
+ '네': '是',
+ '아니요': '不是',
+ '좋습니다': '好的',
+ '플래그십': '旗舰',
+ '스토어': '店',
+ '상점': '商店',
+ '가게': '店铺'
+ };
+
+ const chineseToKorean = {
+ '你好': '안녕하세요',
+ '谢谢': '감사합니다',
+ '对不起': '죄송합니다',
+ '是': '네',
+ '不是': '아니요',
+ '好的': '좋습니다',
+ '旗舰': '플래그십',
+ '店': '스토어',
+ '商店': '상점',
+ '店铺': '가게',
+ '旗舰店': '플래그십 스토어'
+ };
+
+ if (sourceLang === 'ko' && targetLang === 'zh') {
+ return koreanToChinese[text.trim()] || null;
+ } else if (sourceLang === 'zh' && targetLang === 'ko') {
+ return chineseToKorean[text.trim()] || null;
+ }
+
+ return null;
}
// 선택된 텍스트를 새로운 텍스트로 대체하는 함수 (content script에서 실행)
function replaceSelectedText(newText) {
+ console.log('[replaceSelectedText] 텍스트 대체 시작:', newText);
+ console.log('[replaceSelectedText] 현재 URL:', window.location.href);
+ console.log('[replaceSelectedText] activeElement:', document.activeElement);
+
const selection = window.getSelection();
+ const activeElement = document.activeElement;
+ let textReplaced = false;
+
+ // 선택된 텍스트의 위치 정보 저장
+ let selectionRect = null;
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
+ selectionRect = range.getBoundingClientRect();
+ }
+
+ // 알리왕왕 특화 처리 - 더 광범위한 선택자 사용
+ if (window.location.href.includes('aliexpress') || window.location.href.includes('1688') ||
+ document.querySelector('.ice-container') || document.querySelector('[class*="aliwangwang"]') ||
+ window.location.href.includes('alibaba') || document.querySelector('[class*="im-"]')) {
+ console.log('[replaceSelectedText] 알리왕왕/알리바바 환경 감지됨');
- // 선택된 텍스트가 input이나 textarea인지 확인
- const activeElement = document.activeElement;
+ // 더 광범위한 입력창 선택자
+ const inputSelectors = [
+ // 기본 입력 요소
+ 'textarea', 'input[type="text"]', 'input:not([type])',
+ // contentEditable 요소
+ '[contenteditable="true"]', '[contenteditable=""]', 'div[contenteditable]',
+ // 알리왕왕 특화 선택자
+ '.ice-container textarea', '.ice-container input', '.ice-container [contenteditable]',
+ '.chat-input textarea', '.chat-input input', '.chat-input [contenteditable]',
+ '[class*="input"] textarea', '[class*="input"] input', '[class*="input"] [contenteditable]',
+ '[class*="chat"] textarea', '[class*="chat"] input', '[class*="chat"] [contenteditable]',
+ '[class*="message"] textarea', '[class*="message"] input', '[class*="message"] [contenteditable]',
+ '[class*="editor"] textarea', '[class*="editor"] input', '[class*="editor"] [contenteditable]',
+ // 역할 기반 선택자
+ 'div[role="textbox"]', '[role="textbox"]', '[role="combobox"]',
+ // 플레이스홀더 기반
+ '[placeholder*="메시지"]', '[placeholder*="message"]', '[placeholder*="输入"]', '[placeholder*="请输入"]',
+ // 클래스명 기반
+ '.editor', '.input-box', '.text-input', '.message-input', '.chat-editor'
+ ];
+
+ const allInputs = document.querySelectorAll(inputSelectors.join(','));
+ console.log('[replaceSelectedText] 찾은 모든 입력 요소 수:', allInputs.length);
+
+ // 각 입력 요소 상세 검사
+ for (let i = 0; i < allInputs.length; i++) {
+ const input = allInputs[i];
+ const rect = input.getBoundingClientRect();
+ const isVisible = rect.width > 0 && rect.height > 0;
+ const isFocused = input.matches(':focus') || input === activeElement;
+ const hasText = input.value || input.textContent || input.innerText;
+
+ console.log(`[replaceSelectedText] 입력창 ${i+1}:`, {
+ tagName: input.tagName,
+ className: input.className,
+ id: input.id,
+ isVisible: isVisible,
+ isFocused: isFocused,
+ hasText: !!hasText,
+ placeholder: input.placeholder,
+ contentEditable: input.contentEditable
+ });
+
+ // 활성화된 또는 보이는 입력창에 텍스트 삽입 시도
+ if (isVisible && (isFocused || rect.width > 100)) {
+ console.log(`[replaceSelectedText] 입력창 ${i+1} 처리 시도`);
+
+ if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
+ try {
+ // 포커스 설정
+ input.focus();
+ input.click();
+
+ // 기존 값 백업
+ const originalValue = input.value || '';
+ console.log('[replaceSelectedText] 기존 값:', originalValue);
+
+ // 선택 영역 확인
+ const start = input.selectionStart || 0;
+ const end = input.selectionEnd || originalValue.length;
+
+ // 텍스트 대체 - 여러 방법 시도
+
+ // 방법 1: 직접 value 설정
+ if (start !== end && start < end) {
+ // 선택된 텍스트 대체
+ input.value = originalValue.substring(0, start) + newText + originalValue.substring(end);
+ input.selectionStart = start;
+ input.selectionEnd = start + newText.length;
+ } else {
+ // 전체 텍스트 대체 또는 추가
+ if (originalValue.trim()) {
+ input.value = newText; // 기존 텍스트를 새 텍스트로 완전 대체
+ } else {
+ input.value = newText; // 빈 입력창에 새 텍스트 입력
+ }
+ input.selectionStart = input.selectionEnd = newText.length;
+ }
+
+ console.log('[replaceSelectedText] 새로운 값:', input.value);
+
+ // 방법 2: execCommand 시도 (폴백)
+ if (input.value !== newText && !input.value.includes(newText)) {
+ input.select();
+ document.execCommand('insertText', false, newText);
+ }
+
+ // 다양한 이벤트 발생
+ const events = [
+ 'focus', 'input', 'change', 'keydown', 'keyup', 'keypress',
+ 'textInput', 'compositionstart', 'compositionend', 'blur'
+ ];
+
+ events.forEach(eventType => {
+ try {
+ let event;
+ if (eventType.includes('key')) {
+ event = new KeyboardEvent(eventType, {
+ key: 'a',
+ code: 'KeyA',
+ bubbles: true,
+ cancelable: true
+ });
+ } else {
+ event = new Event(eventType, {
+ bubbles: true,
+ cancelable: true
+ });
+ }
+ input.dispatchEvent(event);
+ } catch (e) {
+ console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e);
+ }
+ });
+
+ // React/Vue 등의 프레임워크를 위한 특별 처리
+ if (input._valueTracker) {
+ input._valueTracker.setValue(originalValue);
+ }
+
+ // 강제로 다시 포커스
+ setTimeout(() => {
+ input.focus();
+ input.selectionStart = input.selectionEnd = input.value.length;
+ }, 100);
+
+ // 텍스트가 실제로 변경되었는지 확인
+ if (input.value.includes(newText)) {
+ textReplaced = true;
+ console.log('[replaceSelectedText] INPUT/TEXTAREA 처리 완료');
+ return true;
+ }
+
+ } catch (error) {
+ console.error('[replaceSelectedText] INPUT/TEXTAREA 처리 중 오류:', error);
+ }
+
+ } else if (input.contentEditable === 'true' || input.contentEditable === '' ||
+ input.getAttribute('contenteditable') === 'true') {
+ try {
+ console.log('[replaceSelectedText] contentEditable 요소 처리');
+
+ // 포커스 설정
+ input.focus();
+ input.click();
+
+ // 기존 내용 백업
+ const originalContent = input.textContent || input.innerHTML || '';
+ console.log('[replaceSelectedText] 기존 내용:', originalContent);
+
+ // 방법 1: 선택 후 대체
+ const sel = window.getSelection();
+
+ // 전체 내용 선택
+ const range = document.createRange();
+ range.selectNodeContents(input);
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ // execCommand로 텍스트 삽입
+ if (document.execCommand('insertText', false, newText)) {
+ console.log('[replaceSelectedText] execCommand insertText 성공');
+ } else {
+ // 방법 2: 직접 DOM 조작
+ input.innerHTML = '';
+ input.textContent = newText;
+
+ // 커서를 끝으로 이동
+ const newRange = document.createRange();
+ newRange.selectNodeContents(input);
+ newRange.collapse(false);
+ sel.removeAllRanges();
+ sel.addRange(newRange);
+ }
+
+ console.log('[replaceSelectedText] 새로운 내용:', input.textContent || input.innerHTML);
+
+ // 이벤트 발생
+ const events = [
+ 'focus', 'input', 'textInput', 'keyup', 'compositionend',
+ 'DOMCharacterDataModified', 'DOMSubtreeModified', 'blur'
+ ];
+
+ events.forEach(eventType => {
+ try {
+ const event = new Event(eventType, { bubbles: true, cancelable: true });
+ input.dispatchEvent(event);
+ } catch (e) {
+ console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e);
+ }
+ });
+
+ // 강제로 다시 포커스
+ setTimeout(() => {
+ input.focus();
+ const finalRange = document.createRange();
+ finalRange.selectNodeContents(input);
+ finalRange.collapse(false);
+ const finalSel = window.getSelection();
+ finalSel.removeAllRanges();
+ finalSel.addRange(finalRange);
+ }, 100);
+
+ // 텍스트가 실제로 변경되었는지 확인
+ if (input.textContent.includes(newText) || input.innerHTML.includes(newText)) {
+ textReplaced = true;
+ console.log('[replaceSelectedText] contentEditable 처리 완료');
+ return true;
+ }
+
+ } catch (error) {
+ console.error('[replaceSelectedText] contentEditable 처리 중 오류:', error);
+ }
+ }
+ }
+ }
+ }
+
+ // 일반 사이트 처리 (기존 로직)
+ if (!textReplaced) {
+ console.log('[replaceSelectedText] 일반 사이트 처리 시작');
+
+ // 1. input/textarea 처리
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
- // input/textarea의 경우
+ console.log('[replaceSelectedText] INPUT/TEXTAREA 요소 처리');
const start = activeElement.selectionStart;
const end = activeElement.selectionEnd;
const value = activeElement.value;
@@ -1868,21 +2425,267 @@ function replaceSelectedText(newText) {
activeElement.selectionStart = start;
activeElement.selectionEnd = start + newText.length;
- // 변경 이벤트 트리거
- activeElement.dispatchEvent(new Event('input', { bubbles: true }));
- activeElement.dispatchEvent(new Event('change', { bubbles: true }));
- } else {
- // 일반 텍스트 노드의 경우
- range.deleteContents();
- range.insertNode(document.createTextNode(newText));
+ // 다양한 이벤트 트리거
+ ['input', 'change', 'keyup', 'blur'].forEach(eventType => {
+ activeElement.dispatchEvent(new Event(eventType, { bubbles: true }));
+ });
- // 새로운 텍스트 선택
- range.setStart(range.startContainer, range.startOffset - newText.length);
- range.setEnd(range.startContainer, range.startOffset);
- selection.removeAllRanges();
- selection.addRange(range);
+ if (activeElement.value.includes(newText)) {
+ textReplaced = true;
+ console.log('[replaceSelectedText] INPUT/TEXTAREA 텍스트 대체 완료');
+ return true;
+ }
+ }
+
+ // 2. contentEditable 요소 처리
+ if (activeElement && activeElement.contentEditable === 'true') {
+ console.log('[replaceSelectedText] contentEditable 요소 처리');
+ if (selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0);
+ range.deleteContents();
+ range.insertNode(document.createTextNode(newText));
+
+ // 커서 위치 조정
+ range.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ // contentEditable 이벤트 트리거
+ ['input', 'textInput', 'keyup'].forEach(eventType => {
+ activeElement.dispatchEvent(new Event(eventType, { bubbles: true }));
+ });
+
+ textReplaced = true;
+ console.log('[replaceSelectedText] contentEditable 텍스트 대체 완료');
+ return true;
+ }
+ }
+
+ // 3. 일반 선택 영역 처리
+ if (selection.rangeCount > 0) {
+ console.log('[replaceSelectedText] 일반 선택 영역 처리');
+ const range = selection.getRangeAt(0);
+
+ // 선택된 텍스트가 있는지 확인
+ if (range.toString().trim()) {
+ range.deleteContents();
+ range.insertNode(document.createTextNode(newText));
+
+ // 새로운 텍스트 선택
+ const newRange = document.createRange();
+ newRange.setStart(range.startContainer, range.startOffset - newText.length);
+ newRange.setEnd(range.startContainer, range.startOffset);
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ textReplaced = true;
+ console.log('[replaceSelectedText] 일반 텍스트 대체 완료');
+ return true;
+ }
}
}
+
+ // 텍스트 대체가 실패한 경우 팝업 표시
+ if (!textReplaced) {
+ console.log('[replaceSelectedText] 텍스트 대체 실패, 팝업 표시');
+ showTranslationPopup(newText, selectionRect);
+
+ // 클립보드에도 복사
+ try {
+ navigator.clipboard.writeText(newText).then(() => {
+ console.log('[replaceSelectedText] 클립보드에 복사 완료');
+ }).catch(err => {
+ console.error('[replaceSelectedText] 클립보드 복사 실패:', err);
+ });
+ } catch (error) {
+ console.error('[replaceSelectedText] 클립보드 접근 실패:', error);
+ }
+ }
+
+ return textReplaced;
+}
+
+// 번역 결과 팝업을 표시하는 함수
+function showTranslationPopup(translatedText, selectionRect) {
+ console.log('[showTranslationPopup] 팝업 표시 시작:', translatedText);
+
+ // 기존 팝업이 있으면 제거
+ const existingPopup = document.getElementById('translation-popup-extension');
+ if (existingPopup) {
+ existingPopup.remove();
+ }
+
+ // 팝업 컨테이너 생성
+ const popup = document.createElement('div');
+ popup.id = 'translation-popup-extension';
+ popup.style.cssText = `
+ position: fixed;
+ z-index: 999999;
+ background: #ffffff;
+ border: 2px solid #4CAF50;
+ border-radius: 8px;
+ padding: 12px 16px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
+ font-family: 'Arial', 'Malgun Gothic', sans-serif;
+ font-size: 14px;
+ line-height: 1.4;
+ max-width: 300px;
+ word-wrap: break-word;
+ animation: fadeInScale 0.3s ease-out;
+ `;
+
+ // 애니메이션 CSS 추가
+ if (!document.getElementById('translation-popup-styles')) {
+ const style = document.createElement('style');
+ style.id = 'translation-popup-styles';
+ style.textContent = `
+ @keyframes fadeInScale {
+ 0% {
+ opacity: 0;
+ transform: scale(0.8) translateY(-10px);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ }
+ @keyframes fadeOut {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // 팝업 내용 생성
+ const content = document.createElement('div');
+ content.innerHTML = `
+
+ 🌐 번역 결과
+
+
+ ${translatedText}
+
+
+
+
+
+ `;
+
+ popup.appendChild(content);
+
+ // 팝업 위치 계산
+ let left = 50;
+ let top = 50;
+
+ if (selectionRect && selectionRect.width > 0 && selectionRect.height > 0) {
+ // 선택된 텍스트 위치 기준으로 팝업 위치 설정
+ left = selectionRect.left + selectionRect.width / 2;
+ top = selectionRect.top - 10; // 선택된 텍스트 위쪽에 표시
+
+ // 화면 경계 확인 및 조정
+ const maxLeft = window.innerWidth - 320; // 팝업 너비 고려
+ const maxTop = window.innerHeight - 150; // 팝업 높이 고려
+
+ if (left > maxLeft) left = maxLeft;
+ if (left < 10) left = 10;
+
+ if (top < 10) {
+ // 위쪽 공간이 부족하면 선택된 텍스트 아래쪽에 표시
+ top = selectionRect.bottom + 10;
+ }
+ if (top > maxTop) top = maxTop;
+ } else {
+ // 선택 영역 정보가 없으면 화면 중앙에 표시
+ left = (window.innerWidth - 300) / 2;
+ top = (window.innerHeight - 150) / 2;
+ }
+
+ popup.style.left = left + 'px';
+ popup.style.top = top + 'px';
+
+ // 문서에 팝업 추가
+ document.body.appendChild(popup);
+
+ // 이벤트 리스너 추가
+ const copyButton = popup.querySelector('#copy-translation');
+ const closeButton = popup.querySelector('#close-popup');
+
+ copyButton.addEventListener('click', async () => {
+ try {
+ await navigator.clipboard.writeText(translatedText);
+ copyButton.textContent = '복사됨!';
+ copyButton.style.background = '#2196F3';
+ setTimeout(() => {
+ copyButton.textContent = '복사';
+ copyButton.style.background = '#4CAF50';
+ }, 1500);
+ } catch (err) {
+ console.error('클립보드 복사 실패:', err);
+ copyButton.textContent = '실패';
+ copyButton.style.background = '#f44336';
+ }
+ });
+
+ closeButton.addEventListener('click', () => {
+ popup.style.animation = 'fadeOut 0.2s ease-in';
+ setTimeout(() => {
+ if (popup.parentNode) {
+ popup.remove();
+ }
+ }, 200);
+ });
+
+ // 5초 후 자동으로 팝업 제거
+ setTimeout(() => {
+ if (popup.parentNode) {
+ popup.style.animation = 'fadeOut 0.3s ease-in';
+ setTimeout(() => {
+ if (popup.parentNode) {
+ popup.remove();
+ }
+ }, 300);
+ }
+ }, 5000);
+
+ // 팝업 외부 클릭 시 닫기
+ document.addEventListener('click', function closeOnOutsideClick(e) {
+ if (!popup.contains(e.target)) {
+ popup.style.animation = 'fadeOut 0.2s ease-in';
+ setTimeout(() => {
+ if (popup.parentNode) {
+ popup.remove();
+ }
+ }, 200);
+ document.removeEventListener('click', closeOnOutsideClick);
+ }
+ });
+
+ console.log('[showTranslationPopup] 팝업 표시 완료');
}
// ==================== 시간 알람 기능 ====================
@@ -2105,8 +2908,8 @@ async function handleAddToZzim(message, sendResponse) {
throw new Error('로그인이 필요합니다');
}
- const SUPABASE_URL = config.SUPABASE_URL || "http://146.56.101.199:8000";
- const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
+ const SUPABASE_URL = config.SUPABASE_URL || "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 사용자 정보 가져오기
const userInfo = await fetchUserInfo(config.ACCESS_TOKEN);
@@ -2216,6 +3019,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleAddToZzim(message, sendResponse);
return true; // 비동기 응답을 위해 true 반환
}
+
+ if (message.action === 'executeBackgroundZzim') {
+ handleExecuteBackgroundZzim(message, sendResponse);
+ return true; // 비동기 응답을 위해 true 반환
+ }
});
// 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거)
@@ -2415,8 +3223,8 @@ async function incrementUserApiCalls(userId, token) {
// 백엔드 설정 중앙 관리
const BACKEND_CONFIG = {
- SUPABASE_URL: "http://146.56.101.199:8000",
- SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
+ SUPABASE_URL: "https://ko.wrmc.cc",
+ SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"
};
// 백엔드 설정 가져오기 함수
@@ -2425,3 +3233,360 @@ function getBackendConfig() {
}
// 검색 결과 개선을 위한 키워드 확장 함수
+
+// 백그라운드 찜하기 실행 함수
+async function handleExecuteBackgroundZzim(message, sendResponse) {
+ try {
+ console.log('[Background] 백그라운드 찜하기 시작:', message);
+
+ const { market, zzimType, userId, accessToken } = message;
+
+ if (!market || !market.market_url) {
+ throw new Error('마켓 정보가 없습니다.');
+ }
+
+ // 백그라운드 탭 생성
+ const tab = await chrome.tabs.create({
+ url: market.market_url,
+ active: false
+ });
+
+ console.log('[Background] 백그라운드 탭 생성됨:', tab.id);
+
+ // 찜하기 스크립트 정의
+ const zzimScript = function() {
+ return new Promise((resolve) => {
+ let zzimCount = 0;
+ const maxZzim = 50; // 최대 찜할 개수
+ let isRunning = true;
+
+ console.log('찜하기 스크립트 시작');
+
+ function findZzimButtons() {
+ // 네이버 스마트스토어의 찜 버튼 선택자들
+ const selectors = [
+ 'button[data-testid="wishlist-button"]:not([aria-pressed="true"])',
+ '.zzim_button:not(.active)',
+ '.wish_button:not(.active)',
+ 'button[class*="wish"]:not([class*="active"])',
+ 'button[aria-label*="찜"]:not([aria-pressed="true"])'
+ ];
+
+ let buttons = [];
+ for (const selector of selectors) {
+ buttons = document.querySelectorAll(selector);
+ if (buttons.length > 0) {
+ console.log(`찜 버튼 발견 (${selector}):`, buttons.length);
+ break;
+ }
+ }
+
+ return Array.from(buttons);
+ }
+
+ function clickZzimButtons() {
+ if (!isRunning || zzimCount >= maxZzim) {
+ console.log('찜하기 중단:', { isRunning, zzimCount, maxZzim });
+ return false;
+ }
+
+ const zzimButtons = findZzimButtons();
+ console.log('찾은 찜 버튼 개수:', zzimButtons.length);
+
+ if (zzimButtons.length === 0) {
+ console.log('더 이상 찜할 상품이 없습니다.');
+ return false;
+ }
+
+ // 최대 10개씩 찜하기
+ const buttonsToClick = zzimButtons.slice(0, Math.min(10, maxZzim - zzimCount));
+
+ buttonsToClick.forEach((btn, index) => {
+ setTimeout(() => {
+ if (!isRunning) return;
+
+ try {
+ // 버튼이 화면에 보이도록 스크롤
+ btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+ setTimeout(() => {
+ if (!isRunning) return;
+
+ btn.click();
+ zzimCount++;
+ console.log(`찜 버튼 클릭: ${zzimCount}개`);
+
+ // 클릭 후 잠시 대기
+ setTimeout(() => {
+ // 추가 확인 버튼이 있다면 클릭
+ const confirmBtn = document.querySelector('button[data-testid="confirm"], .confirm_btn, button:contains("확인")');
+ if (confirmBtn) {
+ confirmBtn.click();
+ }
+ }, 200);
+
+ }, 300); // 스크롤 후 클릭까지 대기
+
+ } catch (e) {
+ console.error('찜 버튼 클릭 오류:', e);
+ }
+ }, index * 800); // 0.8초 간격으로 클릭
+ });
+
+ return buttonsToClick.length > 0;
+ }
+
+ // 페이지 스크롤 및 찜하기 반복
+ function scrollAndZzim() {
+ if (!isRunning || zzimCount >= maxZzim) {
+ console.log('찜하기 완료:', zzimCount);
+ resolve(zzimCount);
+ return;
+ }
+
+ // 페이지 하단으로 스크롤
+ window.scrollTo(0, document.body.scrollHeight);
+
+ // 스크롤 후 잠시 대기하여 새 상품 로드
+ setTimeout(() => {
+ if (clickZzimButtons()) {
+ setTimeout(scrollAndZzim, 5000); // 5초 후 다시 시도
+ } else {
+ console.log('더 이상 찜할 상품이 없어 종료');
+ resolve(zzimCount);
+ }
+ }, 2000);
+ }
+
+ // 30초 후 자동 종료
+ setTimeout(() => {
+ isRunning = false;
+ console.log('시간 초과로 찜하기 종료');
+ resolve(zzimCount);
+ }, 30000);
+
+ // 시작
+ console.log('찜하기 시작');
+ setTimeout(scrollAndZzim, 1000); // 페이지 로드 후 1초 대기
+ });
+ };
+
+ // 페이지 로드 완료 후 스크립트 실행
+ const executeZzim = (tabId, changeInfo) => {
+ if (tabId === tab.id && changeInfo.status === 'complete') {
+ chrome.tabs.onUpdated.removeListener(executeZzim);
+
+ console.log('[Background] 페이지 로드 완료, 찜하기 스크립트 실행');
+
+ chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: zzimScript
+ }).then((results) => {
+ const zzimCount = results[0]?.result || 0;
+ console.log('[Background] 찜하기 완료:', zzimCount);
+
+ // 탭 닫기
+ setTimeout(() => {
+ chrome.tabs.remove(tab.id).catch(console.error);
+ }, 2000);
+
+ sendResponse({
+ success: true,
+ zzimCount: zzimCount,
+ message: `${zzimCount}개 상품을 찜했습니다.`
+ });
+
+ }).catch((error) => {
+ console.error('[Background] 찜하기 스크립트 실행 오류:', error);
+ chrome.tabs.remove(tab.id).catch(console.error);
+ sendResponse({
+ success: false,
+ error: '찜하기 스크립트 실행 실패: ' + error.message
+ });
+ });
+ }
+ };
+
+ chrome.tabs.onUpdated.addListener(executeZzim);
+
+ // 타임아웃 설정 (1분)
+ setTimeout(() => {
+ chrome.tabs.onUpdated.removeListener(executeZzim);
+ chrome.tabs.remove(tab.id).catch(console.error);
+ sendResponse({
+ success: false,
+ error: '찜하기 실행 시간 초과'
+ });
+ }, 60000);
+
+ } catch (error) {
+ console.error('[Background] 백그라운드 찜하기 오류:', error);
+ sendResponse({
+ success: false,
+ error: error.message
+ });
+ }
+}
+
+// 메시지 리스너 추가 - content.js에서 보내는 메시지 처리
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ console.log('[Background] 메시지 수신:', message);
+
+ // 지재권 검색 요청
+ if (message.action === "searchTrademark") {
+ console.log('[Background] 지재권 검색 요청 처리:', message.keyword);
+ handleTrademarkSearch(message.keyword, sender.tab)
+ .then(() => {
+ sendResponse({ success: true });
+ })
+ .catch(error => {
+ console.error('[Background] 지재권 검색 처리 실패:', error);
+ sendResponse({ success: false, error: error.message });
+ });
+ return true; // 비동기 응답
+ }
+
+ // 멀티번역 요청
+ if (message.action === "translateText") {
+ console.log('[Background] 멀티번역 요청 처리:', message.text);
+ handleMultiTranslate({ selectionText: message.text })
+ .then(() => {
+ sendResponse({ success: true });
+ })
+ .catch(error => {
+ console.error('[Background] 멀티번역 처리 실패:', error);
+ sendResponse({ success: false, error: error.message });
+ });
+ return true; // 비동기 응답
+ }
+
+ // 직번역 요청
+ if (message.action === "handleDirectTranslation") {
+ const selectedText = message.selectedText || message.text;
+ console.log('[Background] 직번역 요청 처리:', selectedText);
+ handleDirectTranslation(selectedText, sender.tab)
+ .then(() => {
+ sendResponse({ success: true });
+ })
+ .catch(error => {
+ console.error('[Background] 직번역 처리 실패:', error);
+ sendResponse({ success: false, error: error.message });
+ });
+ return true; // 비동기 응답
+ }
+
+ // 한중번역 요청
+ if (message.action === "handleKoreanToChinese") {
+ const selectedText = message.selectedText || message.text;
+ console.log('[Background] 한중번역 요청 처리:', selectedText);
+ handleKoreanToChinese(selectedText, sender.tab)
+ .then(() => {
+ sendResponse({ success: true });
+ })
+ .catch(error => {
+ console.error('[Background] 한중번역 처리 실패:', error);
+ sendResponse({ success: false, error: error.message });
+ });
+ return true; // 비동기 응답
+ }
+
+ // 금지어 추가 요청
+ if (message.action === "addBannedWord") {
+ handleAddBannedWord(message, sendResponse);
+ return true; // 비동기 응답
+ }
+});
+
+// 직번역 처리 함수 (선택된 텍스트를 바로 번역된 텍스트로 대체)
+async function handleDirectTranslation(selectedText, tab) {
+ if (!selectedText) {
+ chrome.notifications.create({
+ type: 'basic',
+ iconUrl: 'icon.png',
+ title: '텍스트 선택 필요',
+ message: '번역할 텍스트를 먼저 선택해주세요.'
+ });
+ return;
+ }
+
+ console.log('[background.js] 직번역 요청:', selectedText);
+
+ try {
+ // 언어 감지 및 번역 방향 결정
+ const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(selectedText);
+ const isChinese = /[\u4e00-\u9fff]/.test(selectedText);
+ const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(selectedText.trim());
+
+ let translatedText;
+ let direction;
+
+ if (isKorean && !isChinese) {
+ // 한국어 → 중국어
+ translatedText = await translateText(selectedText, 'ko', 'zh');
+ direction = '한국어 → 중국어';
+ } else if (isChinese && !isKorean) {
+ // 중국어 → 한국어
+ translatedText = await translateText(selectedText, 'zh', 'ko');
+ direction = '중국어 → 한국어';
+ } else if (isEnglish && !isKorean && !isChinese) {
+ // 영어 → 한국어
+ translatedText = await translateText(selectedText, 'en', 'ko');
+ direction = '영어 → 한국어';
+ } else if (isKorean && isChinese) {
+ // 한국어와 중국어가 섞여있는 경우 - 한국어 비율로 판단
+ const koreanChars = (selectedText.match(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g) || []).length;
+ const chineseChars = (selectedText.match(/[\u4e00-\u9fff]/g) || []).length;
+
+ if (koreanChars >= chineseChars) {
+ // 한국어가 더 많으면 중국어로 번역
+ translatedText = await translateText(selectedText, 'ko', 'zh');
+ direction = '한국어 → 중국어';
+ } else {
+ // 중국어가 더 많으면 한국어로 번역
+ translatedText = await translateText(selectedText, 'zh', 'ko');
+ direction = '중국어 → 한국어';
+ }
+ } else {
+ // 기타 언어는 한국어로 번역
+ translatedText = await translateText(selectedText, 'auto', 'ko');
+ direction = '자동감지 → 한국어';
+ }
+
+ // 선택된 텍스트를 번역된 텍스트로 대체
+ console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText);
+
+ try {
+ const result = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ function: replaceSelectedText,
+ args: [translatedText]
+ });
+
+ console.log('[background.js] replaceSelectedText 실행 결과:', result);
+
+ if (result && result[0] && result[0].result !== undefined) {
+ console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result);
+ } else {
+ console.log('[background.js] 스크립트 실행 완료, 반환값 없음');
+ }
+ } catch (scriptError) {
+ console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError);
+ }
+
+ chrome.notifications.create({
+ type: 'basic',
+ iconUrl: 'icon.png',
+ title: '직번역 완료',
+ message: `${direction}로 번역되어 텍스트가 대체되었습니다.`
+ });
+
+ } catch (error) {
+ console.error('[background.js] 직번역 중 오류:', error);
+ chrome.notifications.create({
+ type: 'basic',
+ iconUrl: 'icon.png',
+ title: '직번역 오류',
+ message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
+ });
+ }
+}
diff --git a/wrmc_ext/bannedWords.js b/wrmc_ext/bannedWords.js
index 929ba4a..0c88078 100644
--- a/wrmc_ext/bannedWords.js
+++ b/wrmc_ext/bannedWords.js
@@ -95,8 +95,8 @@ class BannedWordsManager {
});
// 기본값 설정
- this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000';
- this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE';
+ this.SUPABASE_URL = storageData.SUPABASE_URL || 'https://ko.wrmc.cc';
+ this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU';
this.ACCESS_TOKEN = storageData.access_token;
}
diff --git a/wrmc_ext/content.js b/wrmc_ext/content.js
index 9699c14..18ca614 100644
--- a/wrmc_ext/content.js
+++ b/wrmc_ext/content.js
@@ -4,6 +4,8 @@ let lastContextMenuPos = null;
let tooltipEl = null;
let currentKeyword = null; // 현재 검색 키워드 저장
let loadingIndicator = null; // 로딩 인디케이터 요소
+let isIframeEnvironment = false; // iframe 환경 감지
+let customContextMenu = null; // 커스텀 컨텍스트 메뉴
// 마우스 위치 추적
let currentMousePos = { x: 0, y: 0 };
@@ -11,17 +13,530 @@ document.addEventListener('mousemove', (e) => {
currentMousePos = { x: e.pageX, y: e.pageY };
});
+// iframe 환경 감지
+function detectEnvironment() {
+ try {
+ // 1. iframe 내부인지 확인
+ isIframeEnvironment = window.self !== window.top;
+
+ // 2. 특별한 컨테이너 확인
+ const specialContainers = [
+ '.ice-container',
+ '[class*="aliwangwang"]',
+ '[class*="chat-container"]',
+ '[class*="message-container"]',
+ '[data-testid*="chat"]',
+ '[role="application"]',
+ 'webview',
+ 'embed'
+ ];
+
+ const hasSpecialContainer = specialContainers.some(selector => {
+ return document.querySelector(selector) !== null;
+ });
+
+ console.log('[content.js] 환경 감지:', {
+ isIframe: isIframeEnvironment,
+ hasSpecialContainer: hasSpecialContainer,
+ userAgent: navigator.userAgent.includes('AliWangWang') ? 'AliWangWang' : 'Other'
+ });
+
+ return isIframeEnvironment || hasSpecialContainer;
+ } catch (e) {
+ console.log('[content.js] 환경 감지 오류:', e);
+ return false;
+ }
+}
+
+// 향상된 텍스트 선택 감지
+function getSelectedTextAdvanced() {
+ let selectedText = '';
+
+ try {
+ // 1. 기본 window.getSelection() 확인
+ const selection = window.getSelection();
+ if (selection && selection.toString().trim()) {
+ selectedText = selection.toString().trim();
+ console.log('[content.js] 기본 선택에서 감지:', selectedText);
+ return selectedText;
+ }
+
+ // 2. document.getSelection() 확인
+ if (document.getSelection) {
+ const docSelection = document.getSelection();
+ if (docSelection && docSelection.toString().trim()) {
+ selectedText = docSelection.toString().trim();
+ console.log('[content.js] document 선택에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+
+ // 3. 활성 요소에서 선택된 텍스트 확인
+ const activeElement = document.activeElement;
+ if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
+ const start = activeElement.selectionStart;
+ const end = activeElement.selectionEnd;
+ if (start !== end && start !== null && end !== null) {
+ selectedText = activeElement.value.substring(start, end).trim();
+ console.log('[content.js] 입력 요소에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+
+ // 4. contentEditable 요소 확인
+ const editableElements = document.querySelectorAll('[contenteditable="true"]');
+ for (const element of editableElements) {
+ try {
+ const elementSelection = element.ownerDocument.getSelection();
+ if (elementSelection && elementSelection.toString().trim()) {
+ selectedText = elementSelection.toString().trim();
+ console.log('[content.js] contentEditable에서 감지:', selectedText);
+ return selectedText;
+ }
+ } catch (e) {
+ continue;
+ }
+ }
+
+ // 5. iframe 내부 확인
+ const iframes = document.querySelectorAll('iframe');
+ for (const iframe of iframes) {
+ try {
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+ const iframeSelection = iframeDoc.getSelection();
+ if (iframeSelection && iframeSelection.toString().trim()) {
+ selectedText = iframeSelection.toString().trim();
+ console.log('[content.js] iframe에서 감지:', selectedText);
+ return selectedText;
+ }
+ } catch (e) {
+ // 크로스 오리진 iframe은 접근 불가
+ console.log('[content.js] iframe 접근 제한:', e.message);
+ }
+ }
+
+ // 6. 특별한 컨테이너 내부 확인 (알리왕왕, ice-container 등)
+ const containers = document.querySelectorAll(
+ '.ice-container, [class*="container"], [class*="chat"], [class*="message"], ' +
+ '[class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], ' +
+ '[data-testid*="message"], [role="textbox"], [contenteditable], [class*="aliwangwang"], ' +
+ '[class*="input"], [class*="text"], webview, embed'
+ );
+
+ for (const container of containers) {
+ try {
+ const containerSelection = container.ownerDocument.getSelection();
+ if (containerSelection && containerSelection.toString().trim()) {
+ // 선택 범위가 해당 컨테이너 내부인지 확인
+ const range = containerSelection.getRangeAt(0);
+ if (range && (container.contains(range.commonAncestorContainer) ||
+ container.contains(range.startContainer) ||
+ container.contains(range.endContainer))) {
+ selectedText = containerSelection.toString().trim();
+ console.log('[content.js] 특별 컨테이너에서 감지:', selectedText);
+ return selectedText;
+ }
+ }
+ } catch (e) {
+ continue;
+ }
+ }
+
+ // 7. Shadow DOM 확인
+ function checkShadowDom(element) {
+ if (element.shadowRoot) {
+ try {
+ const shadowSelection = element.shadowRoot.getSelection();
+ if (shadowSelection && shadowSelection.toString().trim()) {
+ return shadowSelection.toString().trim();
+ }
+ } catch (e) {
+ // 무시
+ }
+ }
+
+ for (const child of element.children) {
+ const result = checkShadowDom(child);
+ if (result) return result;
+ }
+ return null;
+ }
+
+ const shadowResult = checkShadowDom(document.body);
+ if (shadowResult) {
+ console.log('[content.js] Shadow DOM에서 감지:', shadowResult);
+ return shadowResult;
+ }
+
+ console.log('[content.js] 선택된 텍스트를 찾을 수 없음');
+ return '';
+
+ } catch (e) {
+ console.error('[content.js] 텍스트 선택 감지 오류:', e);
+ return '';
+ }
+}
+
+// 커스텀 컨텍스트 메뉴 생성
+function createCustomContextMenu(e) {
+ // 기존 커스텀 메뉴 제거
+ removeCustomContextMenu();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (!selectedText) return;
+
+ customContextMenu = document.createElement('div');
+ customContextMenu.id = 'custom-context-menu';
+ customContextMenu.style.cssText = `
+ position: fixed;
+ z-index: 999999;
+ background: white;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ font-family: 'Segoe UI', sans-serif;
+ font-size: 14px;
+ min-width: 200px;
+ padding: 4px 0;
+ `;
+
+ // 메뉴 항목들
+ const menuItems = [
+ {
+ text: '🔍 지재권검색(내차확장기)',
+ action: () => {
+ console.log('[CustomMenu] 지재권 검색 클릭:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "searchTrademark",
+ keyword: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[CustomMenu] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[CustomMenu] 지재권 검색 메시지 전송 성공:', response);
+ }
+ });
+ removeCustomContextMenu();
+ }
+ },
+ {
+ text: '🌐 멀티번역하기(내차확장기)',
+ action: () => {
+ console.log('[CustomMenu] 멀티번역 클릭:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "translateText",
+ text: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[CustomMenu] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[CustomMenu] 멀티번역 메시지 전송 성공:', response);
+ }
+ });
+ removeCustomContextMenu();
+ }
+ },
+ {
+ text: '⚡ 직번역(내차확장기)',
+ action: () => {
+ console.log('[CustomMenu] 직번역 클릭:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "handleDirectTranslation",
+ selectedText: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[CustomMenu] 직번역 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[CustomMenu] 직번역 메시지 전송 성공:', response);
+ }
+ });
+ removeCustomContextMenu();
+ }
+ },
+ {
+ text: '📋 복사',
+ action: () => {
+ console.log('[CustomMenu] 복사 클릭:', selectedText);
+ navigator.clipboard.writeText(selectedText).then(() => {
+ console.log('[CustomMenu] 복사 성공');
+ // 복사 성공 알림 (선택사항)
+ showTemporaryMessage('복사되었습니다!');
+ }).catch(err => {
+ console.error('[CustomMenu] 복사 실패:', err);
+ // 폴백: execCommand 사용
+ try {
+ const textArea = document.createElement('textarea');
+ textArea.value = selectedText;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ console.log('[CustomMenu] 폴백 복사 성공');
+ showTemporaryMessage('복사되었습니다!');
+ } catch (fallbackErr) {
+ console.error('[CustomMenu] 폴백 복사도 실패:', fallbackErr);
+ }
+ });
+ removeCustomContextMenu();
+ }
+ }
+ ];
+
+ menuItems.forEach(item => {
+ const menuItem = document.createElement('div');
+ menuItem.textContent = item.text;
+ menuItem.style.cssText = `
+ padding: 8px 16px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ white-space: nowrap;
+ `;
+
+ menuItem.addEventListener('mouseenter', () => {
+ menuItem.style.backgroundColor = '#f0f0f0';
+ });
+
+ menuItem.addEventListener('mouseleave', () => {
+ menuItem.style.backgroundColor = 'transparent';
+ });
+
+ menuItem.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ console.log('[CustomMenu] 메뉴 항목 클릭:', item.text);
+ item.action();
+ });
+
+ customContextMenu.appendChild(menuItem);
+ });
+
+ document.body.appendChild(customContextMenu);
+
+ // 위치 설정
+ const rect = customContextMenu.getBoundingClientRect();
+ let x = e.clientX;
+ let y = e.clientY;
+
+ // 화면 경계 체크
+ if (x + rect.width > window.innerWidth) {
+ x = window.innerWidth - rect.width - 10;
+ }
+ if (y + rect.height > window.innerHeight) {
+ y = window.innerHeight - rect.height - 10;
+ }
+
+ customContextMenu.style.left = x + 'px';
+ customContextMenu.style.top = y + 'px';
+
+ // 외부 클릭시 메뉴 제거
+ setTimeout(() => {
+ const handleClickOutside = (event) => {
+ if (!customContextMenu.contains(event.target)) {
+ removeCustomContextMenu();
+ document.removeEventListener('click', handleClickOutside);
+ }
+ };
+ document.addEventListener('click', handleClickOutside);
+ }, 100);
+
+ console.log('[CustomMenu] 커스텀 컨텍스트 메뉴 생성 완료:', selectedText);
+}
+
+// 임시 메시지 표시 함수
+function showTemporaryMessage(message) {
+ const messageDiv = document.createElement('div');
+ messageDiv.style.cssText = `
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 9999999;
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 10px 20px;
+ border-radius: 6px;
+ font-family: 'Segoe UI', sans-serif;
+ font-size: 14px;
+ pointer-events: none;
+ `;
+ messageDiv.textContent = message;
+
+ document.body.appendChild(messageDiv);
+
+ setTimeout(() => {
+ if (messageDiv.parentNode) {
+ messageDiv.parentNode.removeChild(messageDiv);
+ }
+ }, 1500);
+}
+
+// 커스텀 컨텍스트 메뉴 제거
+function removeCustomContextMenu() {
+ if (customContextMenu) {
+ customContextMenu.remove();
+ customContextMenu = null;
+ }
+}
+
+// 향상된 컨텍스트 메뉴 이벤트
document.addEventListener("contextmenu", (e) => {
lastContextMenuPos = { x: e.pageX, y: e.pageY };
+
+ // 특별한 환경에서는 커스텀 컨텍스트 메뉴도 표시
+ if (detectEnvironment()) {
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ // 기본 컨텍스트 메뉴를 막지 않고 추가로 커스텀 메뉴 표시
+ setTimeout(() => createCustomContextMenu(e), 50);
+ }
+ }
});
-// ESC 키로 모달 닫기
+// 향상된 단축키 처리
document.addEventListener("keydown", (e) => {
+ // ESC 키 처리
if (e.key === "Escape") {
if (tooltipEl) removeTooltip();
if (loadingIndicator) removeLoadingIndicator();
+ if (customContextMenu) removeCustomContextMenu();
+ return;
}
-});
+
+ // Ctrl+Shift+F: 지재권 검색
+ if (e.ctrlKey && e.shiftKey && e.key === 'S') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ console.log('[Shortcut] Ctrl+Shift+F - 지재권 검색:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "searchTrademark",
+ keyword: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[Shortcut] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[Shortcut] 지재권 검색 메시지 전송 성공:', response);
+ }
+ });
+ } else {
+ console.log('[Shortcut] 선택된 텍스트가 없어 지재권 검색을 실행할 수 없습니다.');
+ showTemporaryMessage('텍스트를 선택해주세요');
+ }
+ return;
+ }
+
+ // Ctrl+Shift+T: 멀티번역
+ if (e.ctrlKey && e.shiftKey && e.key === 'E') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ console.log('[Shortcut] Ctrl+Shift+T - 멀티번역:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "translateText",
+ text: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[Shortcut] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[Shortcut] 멀티번역 메시지 전송 성공:', response);
+ }
+ });
+ } else {
+ console.log('[Shortcut] 선택된 텍스트가 없어 멀티번역을 실행할 수 없습니다.');
+ showTemporaryMessage('텍스트를 선택해주세요');
+ }
+ return;
+ }
+
+ // Ctrl+Shift+K: 한중 번역 (기존 기능 유지)
+ if (e.ctrlKey && e.shiftKey && e.key === 'K') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ console.log('[Shortcut] Ctrl+Shift+K - 한중번역:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "handleKoreanToChinese",
+ selectedText: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[Shortcut] 한중번역 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[Shortcut] 한중번역 메시지 전송 성공:', response);
+ }
+ });
+ } else {
+ console.log('[Shortcut] 선택된 텍스트가 없어 한중번역을 실행할 수 없습니다.');
+ showTemporaryMessage('텍스트를 선택해주세요');
+ }
+ return;
+ }
+
+ // Ctrl+Shift+Z: 직번역 (새로 추가)
+ if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ console.log('[Shortcut] Ctrl+Shift+Z - 직번역:', selectedText);
+ chrome.runtime.sendMessage({
+ action: "handleDirectTranslation",
+ selectedText: selectedText
+ }, (response) => {
+ if (chrome.runtime.lastError) {
+ console.error('[Shortcut] 직번역 메시지 전송 실패:', chrome.runtime.lastError);
+ } else {
+ console.log('[Shortcut] 직번역 메시지 전송 성공:', response);
+ }
+ });
+ } else {
+ console.log('[Shortcut] 선택된 텍스트가 없어 직번역을 실행할 수 없습니다.');
+ showTemporaryMessage('텍스트를 선택해주세요');
+ }
+ return;
+ }
+}, true); // useCapture를 true로 설정하여 이벤트를 먼저 캐치
+
+// iframe 내부에서도 이벤트 감지
+function setupIframeEventListeners() {
+ const iframes = document.querySelectorAll('iframe');
+
+ iframes.forEach(iframe => {
+ try {
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+
+ // iframe 내부에서 컨텍스트 메뉴 이벤트
+ iframeDoc.addEventListener("contextmenu", (e) => {
+ lastContextMenuPos = {
+ x: e.pageX + iframe.offsetLeft,
+ y: e.pageY + iframe.offsetTop
+ };
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ console.log('[iframe] 컨텍스트 메뉴에서 선택된 텍스트:', selectedText);
+ setTimeout(() => createCustomContextMenu(e), 50);
+ }
+ });
+
+ // iframe 내부에서 키보드 이벤트 리스너 추가
+ addKeyboardListeners(iframeDoc);
+
+ } catch (e) {
+ console.log('[content.js] iframe 이벤트 설정 실패:', e.message);
+ }
+ });
+}
+
+// 페이지 로드 시 iframe 이벤트 설정
+setTimeout(setupIframeEventListeners, 1000);
+
// 로딩 인디케이터 생성 및 표시
function showLoadingIndicator(message, position = null) {
@@ -163,6 +678,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
+ // 자동 찜하기 시작
+ if (message.action === "startAutoZzim") {
+ console.log('[AutoZzim] 자동 찜하기 시작:', message);
+ startAutoZzim(message.maxZzim || 50, message.delay || 1000);
+ sendResponse({ success: true });
+ return true;
+ }
+
// 로딩 인디케이터 표시 요청
if (message.action === "showLoading") {
showLoadingIndicator(message.message, message.position);
@@ -858,9 +1381,569 @@ function getUserLevelGuide(userLevel) {
// 번역 툴팁 제거
function removeTranslationTooltip() {
- const translationTooltip = document.getElementById("translation-tooltip");
- if (translationTooltip) {
- translationTooltip.remove();
- console.log('[content.js] 번역 툴팁 제거됨');
+ const existingTooltip = document.getElementById('translation-tooltip');
+ if (existingTooltip) {
+ existingTooltip.remove();
}
}
+
+// 자동 찜하기 기능
+function startAutoZzim(maxZzim = 50, delay = 1000) {
+ console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay });
+
+ let zzimCount = 0;
+ let actualZzimCount = 0; // 실제로 찜한 개수
+ let currentPage = 1;
+ let isRunning = true;
+ let noMoreProducts = false;
+
+ // 상태 표시 UI 생성
+ const statusDiv = createZzimStatusUI();
+ updateZzimStatus(statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`);
+
+ function findZzimButtons() {
+ // 네이버 스마트스토어의 찜 버튼 선택자들 (이미 찜한 상품 제외)
+ const selectors = [
+ // 찜하지 않은 상품만 선택 (aria-pressed="false" 또는 active 클래스가 없는 것)
+ 'button[data-testid="wishlist-button"]:not([aria-pressed="true"])',
+ 'button[data-testid="wishlist-button"][aria-pressed="false"]',
+ '.zzim_button:not(.active):not(.on)',
+ '.wish_button:not(.active):not(.on)',
+ 'button[class*="wish"]:not([class*="active"]):not([class*="on"])',
+ 'button[aria-label*="찜"]:not([aria-pressed="true"])',
+ 'button[class*="zzim"]:not([class*="active"]):not([class*="on"])',
+ 'a[class*="wish"]:not([class*="active"]):not([class*="on"])',
+ '.product_like:not(.active):not(.on)',
+ '.btn_wish:not(.active):not(.on)',
+ // 하트 아이콘이 비어있는 것들
+ 'button:has(svg[fill="none"]) svg[class*="heart"]',
+ 'button:has(.ico_heart):not(.active)',
+ // 추가 선택자들
+ '[class*="ProductWishButton"]:not([class*="active"])',
+ '[data-shp-contents-id] button[class*="wish"]:not([class*="active"])'
+ ];
+
+ let buttons = [];
+ for (const selector of selectors) {
+ try {
+ const foundButtons = document.querySelectorAll(selector);
+ if (foundButtons.length > 0) {
+ // 실제로 보이는 버튼만 필터링
+ const visibleButtons = Array.from(foundButtons).filter(btn => {
+ const rect = btn.getBoundingClientRect();
+ return rect.width > 0 && rect.height > 0 &&
+ window.getComputedStyle(btn).display !== 'none' &&
+ window.getComputedStyle(btn).visibility !== 'hidden';
+ });
+
+ if (visibleButtons.length > 0) {
+ console.log(`[AutoZzim] 찜 버튼 발견 (${selector}):`, visibleButtons.length);
+ buttons = visibleButtons;
+ break;
+ }
+ }
+ } catch (e) {
+ console.log(`[AutoZzim] 선택자 오류 (${selector}):`, e.message);
+ }
+ }
+
+ // 중복 제거 및 이미 찜한 상품 재확인
+ const uniqueButtons = Array.from(new Set(buttons)).filter(btn => {
+ return !isAlreadyZzimed(btn);
+ });
+
+ return uniqueButtons;
+ }
+
+ // 이미 찜한 상품인지 확인
+ function isAlreadyZzimed(button) {
+ try {
+ // 버튼의 상태 확인
+ const isPressed = button.getAttribute('aria-pressed') === 'true';
+ const hasActiveClass = button.classList.contains('active') ||
+ button.classList.contains('on') ||
+ button.classList.contains('selected');
+
+ // 부모 요소에서 찜 상태 확인
+ const parent = button.closest('[class*="product"], [class*="item"], [data-shp-contents-id]');
+ if (parent) {
+ const hasZzimedClass = parent.querySelector('.active, .on, [aria-pressed="true"]');
+ if (hasZzimedClass) return true;
+ }
+
+ // 하트 아이콘 색상 확인
+ const heartIcon = button.querySelector('svg, .ico_heart, [class*="heart"]');
+ if (heartIcon) {
+ const fill = heartIcon.getAttribute('fill');
+ const color = window.getComputedStyle(heartIcon).color;
+ const backgroundColor = window.getComputedStyle(heartIcon).backgroundColor;
+
+ // 빨간색이나 채워진 하트는 이미 찜한 상품
+ if (fill && fill !== 'none' && fill !== 'transparent') return true;
+ if (color && (color.includes('rgb(255') || color.includes('red'))) return true;
+ if (backgroundColor && backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)') return true;
+ }
+
+ return isPressed || hasActiveClass;
+ } catch (e) {
+ console.log('[AutoZzim] 찜 상태 확인 오류:', e);
+ return false;
+ }
+ }
+
+ // 찜하기 버튼 클릭 및 검증
+ async function clickZzimButton(button) {
+ return new Promise((resolve) => {
+ try {
+ // 클릭 전 상태 저장
+ const beforeState = isAlreadyZzimed(button);
+
+ // 버튼이 화면에 보이도록 스크롤
+ button.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+ setTimeout(() => {
+ if (!isRunning) {
+ resolve(false);
+ return;
+ }
+
+ // 버튼 클릭
+ button.click();
+ console.log(`[AutoZzim] 찜 버튼 클릭 시도`);
+
+ // 클릭 후 상태 변화 확인
+ setTimeout(() => {
+ const afterState = isAlreadyZzimed(button);
+ const success = !beforeState && afterState; // 찜하지 않은 상태에서 찜한 상태로 변경
+
+ if (success) {
+ actualZzimCount++;
+ console.log(`[AutoZzim] 찜하기 성공! 총 ${actualZzimCount}개`);
+ } else {
+ console.log(`[AutoZzim] 찜하기 실패 또는 이미 찜한 상품`);
+ }
+
+ // 확인 버튼이 나타나면 클릭
+ setTimeout(() => {
+ const confirmSelectors = [
+ 'button[data-testid="confirm"]',
+ '.confirm_btn',
+ 'button:contains("확인")',
+ 'button:contains("OK")',
+ '.modal button[class*="confirm"]',
+ '.popup button[class*="confirm"]',
+ '[class*="LayerAlert"] button',
+ '[class*="alert"] button[class*="confirm"]'
+ ];
+
+ for (const selector of confirmSelectors) {
+ try {
+ const confirmBtn = document.querySelector(selector);
+ if (confirmBtn && confirmBtn.offsetParent !== null) {
+ confirmBtn.click();
+ console.log('[AutoZzim] 확인 버튼 클릭');
+ break;
+ }
+ } catch (e) {
+ // 무시
+ }
+ }
+ }, 300);
+
+ resolve(success);
+ }, 800); // 상태 변화 확인을 위한 대기 시간
+
+ }, 500); // 스크롤 후 클릭까지 대기
+
+ } catch (e) {
+ console.error('[AutoZzim] 찜 버튼 클릭 오류:', e);
+ resolve(false);
+ }
+ });
+ }
+
+ async function processCurrentPage() {
+ if (!isRunning || actualZzimCount >= maxZzim) {
+ return false;
+ }
+
+ const zzimButtons = findZzimButtons();
+ console.log(`[AutoZzim] 페이지 ${currentPage}에서 찾은 찜 가능한 버튼:`, zzimButtons.length);
+
+ if (zzimButtons.length === 0) {
+ console.log('[AutoZzim] 현재 페이지에 찜할 상품이 없음');
+ return false;
+ }
+
+ // 한 번에 최대 5개씩 처리
+ const batchSize = 5;
+ let processedInPage = 0;
+
+ for (let i = 0; i < zzimButtons.length && isRunning && actualZzimCount < maxZzim; i += batchSize) {
+ const batch = zzimButtons.slice(i, Math.min(i + batchSize, zzimButtons.length));
+
+ for (const button of batch) {
+ if (!isRunning || actualZzimCount >= maxZzim) break;
+
+ zzimCount++; // 시도 횟수
+ updateZzimStatus(statusDiv, `찜하기 진행 중... (${actualZzimCount}/${maxZzim}) - 페이지 ${currentPage}`);
+
+ const success = await clickZzimButton(button);
+ if (success) {
+ processedInPage++;
+ }
+
+ // 설정된 간격으로 대기
+ if (i < zzimButtons.length - 1) {
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+
+ // 배치 간 추가 대기
+ if (i + batchSize < zzimButtons.length) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+ }
+
+ console.log(`[AutoZzim] 페이지 ${currentPage} 처리 완료: ${processedInPage}개 찜함`);
+ return processedInPage > 0;
+ }
+
+ async function goToNextPage() {
+ try {
+ // 다음 페이지 버튼 찾기
+ const nextPageSelectors = [
+ 'a[class*="next"]:not([class*="disabled"])',
+ 'button[class*="next"]:not([disabled])',
+ '.paginate_next:not(.disabled)',
+ '[class*="pagination"] a[class*="next"]',
+ '[class*="paging"] a[class*="next"]',
+ 'a[aria-label*="다음"]',
+ 'button[aria-label*="다음"]',
+ // 페이지 번호로 다음 페이지 찾기
+ `a[href*="cp=${currentPage + 1}"]`,
+ `button[data-page="${currentPage + 1}"]`
+ ];
+
+ let nextButton = null;
+ for (const selector of nextPageSelectors) {
+ try {
+ nextButton = document.querySelector(selector);
+ if (nextButton && nextButton.offsetParent !== null) {
+ console.log(`[AutoZzim] 다음 페이지 버튼 발견: ${selector}`);
+ break;
+ }
+ } catch (e) {
+ continue;
+ }
+ }
+
+ if (!nextButton) {
+ // URL 직접 변경으로 다음 페이지 이동
+ const currentUrl = new URL(window.location.href);
+ const currentPageNum = parseInt(currentUrl.searchParams.get('cp') || '1');
+ currentUrl.searchParams.set('cp', (currentPageNum + 1).toString());
+
+ console.log(`[AutoZzim] URL 직접 변경으로 다음 페이지 이동: ${currentUrl.href}`);
+ window.location.href = currentUrl.href;
+ return true;
+ }
+
+ // 다음 페이지 버튼 클릭
+ nextButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ nextButton.click();
+ console.log(`[AutoZzim] 다음 페이지 버튼 클릭`);
+
+ // 페이지 로드 대기
+ await new Promise(resolve => setTimeout(resolve, 3000));
+
+ currentPage++;
+ return true;
+
+ } catch (e) {
+ console.error('[AutoZzim] 다음 페이지 이동 오류:', e);
+ return false;
+ }
+ }
+
+ async function mainLoop() {
+ while (isRunning && actualZzimCount < maxZzim && currentPage <= 10) { // 최대 10페이지까지
+ updateZzimStatus(statusDiv, `페이지 ${currentPage} 로딩 중... (${actualZzimCount}/${maxZzim})`);
+
+ // 페이지 로드 대기
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // 현재 페이지 처리
+ const hasProducts = await processCurrentPage();
+
+ if (!hasProducts) {
+ console.log(`[AutoZzim] 페이지 ${currentPage}에 더 이상 찜할 상품이 없음`);
+
+ // 다음 페이지로 이동 시도
+ const moved = await goToNextPage();
+ if (!moved) {
+ console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
+ break;
+ }
+ } else {
+ // 현재 페이지에서 더 찜할 수 있는지 확인
+ const remainingButtons = findZzimButtons();
+ if (remainingButtons.length === 0) {
+ // 다음 페이지로 이동
+ const moved = await goToNextPage();
+ if (!moved) {
+ console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
+ break;
+ }
+ }
+ }
+
+ // 페이지 간 대기
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ }
+
+ // 완료 처리
+ console.log(`[AutoZzim] 찜하기 완료: 실제 ${actualZzimCount}개 찜함 (시도: ${zzimCount}회)`);
+ updateZzimStatus(statusDiv, `찜하기 완료! (실제 ${actualZzimCount}개 찜함)`);
+
+ // 5초 후 상태 UI 제거
+ setTimeout(() => {
+ if (statusDiv && statusDiv.parentNode) {
+ statusDiv.parentNode.removeChild(statusDiv);
+ }
+ }, 5000);
+ }
+
+ // 60초 후 자동 종료
+ setTimeout(() => {
+ isRunning = false;
+ console.log('[AutoZzim] 시간 초과로 찜하기 종료');
+ updateZzimStatus(statusDiv, `시간 초과로 종료 (실제 ${actualZzimCount}개 찜함)`);
+
+ // 5초 후 상태 UI 제거
+ setTimeout(() => {
+ if (statusDiv && statusDiv.parentNode) {
+ statusDiv.parentNode.removeChild(statusDiv);
+ }
+ }, 5000);
+ }, 60000);
+
+ // 찜하기 시작
+ setTimeout(mainLoop, 2000); // 페이지 로드 후 2초 대기
+}
+
+function createZzimStatusUI() {
+ const statusDiv = document.createElement('div');
+ statusDiv.id = 'auto-zzim-status';
+ statusDiv.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 999999;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 15px 20px;
+ border-radius: 10px;
+ font-family: 'Segoe UI', sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ min-width: 200px;
+ text-align: center;
+ animation: slideIn 0.3s ease-out;
+ `;
+
+ // CSS 애니메이션 추가
+ if (!document.getElementById('auto-zzim-styles')) {
+ const style = document.createElement('style');
+ style.id = 'auto-zzim-styles';
+ style.textContent = `
+ @keyframes slideIn {
+ 0% { transform: translateX(100%); opacity: 0; }
+ 100% { transform: translateX(0); opacity: 1; }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ document.body.appendChild(statusDiv);
+ return statusDiv;
+}
+
+function updateZzimStatus(statusDiv, message) {
+ if (statusDiv) {
+ statusDiv.textContent = message;
+ }
+}
+
+// 페이지 로드 시 URL 파라미터 확인
+function checkAutoZzimParam() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const autoZzim = urlParams.get('auto_zzim');
+ const maxZzim = parseInt(urlParams.get('max_zzim')) || 50;
+
+ if (autoZzim === 'true') {
+ console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작');
+ setTimeout(() => {
+ startAutoZzim(maxZzim);
+ }, 3000); // 페이지 완전 로드 후 3초 대기
+ }
+}
+
+// 페이지 로드 완료 시 자동 찜하기 파라미터 확인
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', checkAutoZzimParam);
+} else {
+ checkAutoZzimParam();
+}
+
+// 키보드 이벤트 리스너 추가
+function addKeyboardListeners(target = document) {
+ target.addEventListener('keydown', function(e) {
+ // Ctrl+Shift+F1: 지재권검색
+ if (e.ctrlKey && e.shiftKey && e.key === 'S') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText && selectedText.trim()) {
+ console.log('[Content Script] 지재권검색 단축키 실행:', selectedText);
+
+ chrome.runtime.sendMessage({
+ action: 'searchTrademark',
+ text: selectedText.trim()
+ }, (response) => {
+ if (response && response.success) {
+ console.log('[Content Script] 지재권검색 성공');
+ } else {
+ console.error('[Content Script] 지재권검색 실패:', response?.error);
+ }
+ });
+ } else {
+ alert('검색할 텍스트를 선택해주세요.');
+ }
+ }
+
+ // Ctrl+Shift+F2: 멀티번역
+ else if (e.ctrlKey && e.shiftKey && e.key === 'E') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText && selectedText.trim()) {
+ console.log('[Content Script] 멀티번역 단축키 실행:', selectedText);
+
+ chrome.runtime.sendMessage({
+ action: 'translateText',
+ text: selectedText.trim()
+ }, (response) => {
+ if (response && response.success) {
+ console.log('[Content Script] 멀티번역 성공');
+ } else {
+ console.error('[Content Script] 멀티번역 실패:', response?.error);
+ }
+ });
+ } else {
+ alert('번역할 텍스트를 선택해주세요.');
+ }
+ }
+
+ // Ctrl+Shift+F3: 한중번역
+ else if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText && selectedText.trim()) {
+ console.log('[Content Script] 한중번역 단축키 실행:', selectedText);
+
+ chrome.runtime.sendMessage({
+ action: 'handleKoreanToChinese',
+ text: selectedText.trim()
+ }, (response) => {
+ if (response && response.success) {
+ console.log('[Content Script] 한중번역 성공');
+ } else {
+ console.error('[Content Script] 한중번역 실패:', response?.error);
+ }
+ });
+ } else {
+ alert('번역할 텍스트를 선택해주세요.');
+ }
+ }
+
+ // Ctrl+Shift+F4: 직번역
+ else if (e.ctrlKey && e.shiftKey && e.key === 'K') {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText && selectedText.trim()) {
+ console.log('[Content Script] 직번역 단축키 실행:', selectedText);
+
+ chrome.runtime.sendMessage({
+ action: 'handleDirectTranslation',
+ text: selectedText.trim()
+ }, (response) => {
+ if (response && response.success) {
+ console.log('[Content Script] 직번역 성공');
+ } else {
+ console.error('[Content Script] 직번역 실패:', response?.error);
+ }
+ });
+ } else {
+ alert('번역할 텍스트를 선택해주세요.');
+ }
+ }
+ }, true); // useCapture: true로 설정하여 iframe에서도 작동하도록 함
+}
+
+// 환경 감지 및 이벤트 리스너 설정
+detectEnvironment();
+
+// 키보드 이벤트 리스너 추가
+addKeyboardListeners();
+
+// iframe 환경에서도 이벤트 리스너 설정
+if (isIframeEnvironment) {
+ addKeyboardListeners(window.parent.document);
+}
+
+// 컨텍스트 메뉴 이벤트 리스너
+document.addEventListener("contextmenu", (e) => {
+ lastContextMenuPos = { x: e.pageX, y: e.pageY };
+
+ const selectedText = getSelectedTextAdvanced();
+ if (selectedText) {
+ console.log('[Context Menu] 선택된 텍스트:', selectedText);
+ setTimeout(() => createCustomContextMenu(e), 50);
+ }
+});
+
+// iframe 이벤트 설정
+setTimeout(setupIframeEventListeners, 1000);
+
+// 동적으로 추가되는 iframe 감지
+const mainObserver = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ if (node.tagName === 'IFRAME') {
+ setTimeout(() => setupIframeEventListeners(), 500);
+ } else if (node.querySelectorAll && node.querySelectorAll('iframe').length > 0) {
+ setTimeout(() => setupIframeEventListeners(), 500);
+ }
+ }
+ });
+ });
+});
+
+mainObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+});
+
+// 자동 찜하기 파라미터 확인
+checkAutoZzimParam();
diff --git a/wrmc_ext/manifest.json b/wrmc_ext/manifest.json
index a7e63c0..ea2b09f 100644
--- a/wrmc_ext/manifest.json
+++ b/wrmc_ext/manifest.json
@@ -11,11 +11,12 @@
"notifications",
"alarms",
"tabs",
- "scripting"
+ "scripting",
+ "clipboardRead"
],
"host_permissions": [
"*://markinfo.kr/*",
- "http://146.56.101.199:8000/*",
+ "https://ko.wrmc.cc/*",
"https://oci1ckh08045.duckdns.org:8000/*",
"*://smartstore.naver.com/*",
"*://translate.googleapis.com/*",
@@ -59,6 +60,12 @@
"default": "Ctrl+Shift+Z"
},
"description": "한국어↔중국어 양방향 번역"
+ },
+ "direct-translate": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+K"
+ },
+ "description": "직번역 (텍스트 바로 대체)"
}
},
"web_accessible_resources": [
diff --git a/wrmc_ext/popup.html b/wrmc_ext/popup.html
index 5af4375..985f6d1 100644
--- a/wrmc_ext/popup.html
+++ b/wrmc_ext/popup.html
@@ -427,7 +427,8 @@
-
+
+
diff --git a/wrmc_ext/popup.js b/wrmc_ext/popup.js
index 36dc0d2..5df65d9 100644
--- a/wrmc_ext/popup.js
+++ b/wrmc_ext/popup.js
@@ -24,8 +24,8 @@ async function getBackendConfig() {
// 폴백 설정
const fallbackConfig = {
- SUPABASE_URL: "http://146.56.101.199:8000",
- SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
+ SUPABASE_URL: "https://ko.wrmc.cc",
+ SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"
};
console.log('[popup.js] 폴백 설정:', fallbackConfig);
@@ -34,8 +34,8 @@ async function getBackendConfig() {
}
// 디버그 모드 플래그 (개발 시에만 true로 설정)
-// const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김
-const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김
+const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김
+// const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김
// 로그 레벨 정의
const LOG_LEVELS = {
diff --git a/wrmc_ext/rest-modal.js b/wrmc_ext/rest-modal.js
index 85a5638..2a4b59d 100644
--- a/wrmc_ext/rest-modal.js
+++ b/wrmc_ext/rest-modal.js
@@ -93,8 +93,8 @@ class RestModal {
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";
+ const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 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`;
@@ -170,8 +170,8 @@ class RestModal {
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";
+ const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 최근 1달 이내의 승인된 어록 가져오기
const oneMonthAgo = new Date();
diff --git a/wrmc_ext/sayings.js b/wrmc_ext/sayings.js
index 63f3ae5..0a35876 100644
--- a/wrmc_ext/sayings.js
+++ b/wrmc_ext/sayings.js
@@ -67,8 +67,8 @@ class SayingsManager {
});
// 기본값 설정
- this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000';
- this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE';
+ this.SUPABASE_URL = storageData.SUPABASE_URL || 'https://ko.wrmc.cc';
+ this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU';
this.ACCESS_TOKEN = storageData.access_token;
}
diff --git a/wrmc_ext/settings.js b/wrmc_ext/settings.js
index 8a7288a..6f1ff34 100644
--- a/wrmc_ext/settings.js
+++ b/wrmc_ext/settings.js
@@ -116,8 +116,8 @@ class SettingsManager {
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";
+ const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc";
+ const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
console.log('Supabase URL:', SUPABASE_URL);
console.log('토큰 존재 여부:', !!this.config.ACCESS_TOKEN);
diff --git a/wrmc_ext/zzim.html b/wrmc_ext/zzim.html
index e9d39ae..2c78337 100644
--- a/wrmc_ext/zzim.html
+++ b/wrmc_ext/zzim.html
@@ -140,45 +140,162 @@
}
.market-item {
- border: 1px solid #ddd;
- border-radius: 6px;
- padding: 15px;
- margin-bottom: 10px;
- background: #f9f9f9;
+ border: 1px solid #e1e8ed;
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 15px;
+ background: white;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .market-item:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ border-color: #667eea;
+ }
+
+ .market-item::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .market-item:hover::before {
+ opacity: 1;
}
.market-header {
display: flex;
justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 15px;
+ }
+
+ .market-checkbox {
+ display: flex;
align-items: center;
+ gap: 8px;
margin-bottom: 10px;
}
+ .market-checkbox-input {
+ width: 18px;
+ height: 18px;
+ accent-color: #667eea;
+ cursor: pointer;
+ }
+
+ .market-checkbox-label {
+ font-size: 14px;
+ font-weight: 600;
+ color: #2c3e50;
+ cursor: pointer;
+ user-select: none;
+ }
+
.market-info {
flex: 1;
+ margin: 0 15px;
}
.market-name {
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
+ font-size: 16px;
}
.market-nickname {
color: #666;
- font-size: 12px;
- margin-bottom: 5px;
+ font-size: 14px;
+ margin-bottom: 8px;
+ font-weight: 500;
}
.market-url {
color: #888;
font-size: 12px;
word-break: break-all;
+ background: #f8f9fa;
+ padding: 6px 10px;
+ border-radius: 6px;
+ border: 1px solid #e1e8ed;
+ margin-bottom: 10px;
+ font-family: 'Courier New', monospace;
+ }
+
+ .market-stats {
+ display: flex;
+ gap: 15px;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ .zzim-count {
+ font-size: 12px;
+ color: #666;
+ background: #e8f4f8;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-weight: 500;
+ }
+
+ .visibility-status {
+ font-size: 12px;
+ padding: 4px 10px;
+ border-radius: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .visibility-status.visible {
+ background: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+ }
+
+ .visibility-status.hidden {
+ background: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+ }
+
+ .created-date {
+ font-size: 12px;
+ color: #888;
+ background: #f8f9fa;
+ padding: 4px 8px;
+ border-radius: 12px;
}
.market-actions {
display: flex;
- gap: 5px;
+ gap: 8px;
+ flex-direction: column;
+ }
+
+ .market-actions .btn {
+ padding: 8px 12px;
+ font-size: 12px;
+ border-radius: 6px;
+ min-width: 70px;
+ font-weight: 600;
+ transition: all 0.2s ease;
+ }
+
+ .market-actions .btn:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.action-buttons {
@@ -222,6 +339,257 @@
border-radius: 0 4px 4px 0 !important;
border-left: none !important;
}
+
+ /* 모달 스타일 */
+ .modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(3px);
+ animation: fadeIn 0.3s ease;
+ }
+
+ .modal-content {
+ background-color: white;
+ margin: 5% auto;
+ padding: 0;
+ border: none;
+ border-radius: 12px;
+ width: 90%;
+ max-width: 500px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+ animation: slideIn 0.3s ease;
+ overflow: hidden;
+ }
+
+ .modal-header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 20px 25px;
+ border-bottom: none;
+ position: relative;
+ }
+
+ .modal-title {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .modal-close {
+ position: absolute;
+ right: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: white;
+ font-size: 24px;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.2s;
+ }
+
+ .modal-close:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+ }
+
+ .modal-body {
+ padding: 25px;
+ }
+
+ .form-group {
+ margin-bottom: 20px;
+ }
+
+ .form-label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 600;
+ color: #2c3e50;
+ font-size: 14px;
+ }
+
+ .form-input {
+ width: 100%;
+ padding: 12px 15px;
+ border: 2px solid #e1e8ed;
+ border-radius: 8px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+ box-sizing: border-box;
+ }
+
+ .form-input:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+ }
+
+ .form-textarea {
+ min-height: 80px;
+ resize: vertical;
+ font-family: inherit;
+ }
+
+ .modal-footer {
+ padding: 20px 25px;
+ border-top: 1px solid #e1e8ed;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ background-color: #f8f9fa;
+ }
+
+ .btn-modal {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 80px;
+ }
+
+ .btn-modal-cancel {
+ background-color: #6c757d;
+ color: white;
+ }
+
+ .btn-modal-cancel:hover {
+ background-color: #5a6268;
+ }
+
+ .btn-modal-save {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ }
+
+ .btn-modal-save:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+ }
+
+ .btn-modal-save:disabled {
+ background: #cccccc;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ }
+
+ @keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+ }
+
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50px) scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ /* 체크박스 스타일링 */
+ .checkbox-container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 15px;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ border: 2px solid #e1e8ed;
+ transition: border-color 0.3s ease;
+ }
+
+ .checkbox-container:hover {
+ border-color: #667eea;
+ }
+
+ .custom-checkbox {
+ position: relative;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ }
+
+ .custom-checkbox input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ .checkbox-checkmark {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 20px;
+ width: 20px;
+ background-color: white;
+ border: 2px solid #ddd;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+ }
+
+ .custom-checkbox:hover input ~ .checkbox-checkmark {
+ border-color: #667eea;
+ }
+
+ .custom-checkbox input:checked ~ .checkbox-checkmark {
+ background-color: #667eea;
+ border-color: #667eea;
+ }
+
+ .checkbox-checkmark:after {
+ content: "";
+ position: absolute;
+ display: none;
+ }
+
+ .custom-checkbox input:checked ~ .checkbox-checkmark:after {
+ display: block;
+ }
+
+ .custom-checkbox .checkbox-checkmark:after {
+ left: 6px;
+ top: 2px;
+ width: 6px;
+ height: 10px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+ }
+
+ .checkbox-label {
+ font-weight: 600;
+ color: #2c3e50;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .checkbox-description {
+ font-size: 12px;
+ color: #666;
+ margin-top: 5px;
+ }
@@ -268,8 +636,65 @@
- 🎯 찜하기 작업
+ 💝 찜하기 작업
+
+
+
+
+
+ 체크하면 새 탭에서 백그라운드로 찜하기를 실행합니다.
+
+
+
+
+
+
⚙️ 찜하기 옵션
+
+
+
+
+
+
+ 기본 간격:
+
+ ms
+
+
+ + 추가 간격:
+
+ ms
+
+
+ = 총 간격:
+ 1000ms
+
+
+
+ 💡 찜과 찜 사이의 대기 시간을 설정합니다. (1초~10초)
+
+
+
+
+
+
+
+ 체크하면 전체상품(/category/ALL) 대신 최신상품(/best)부터 찜합니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
체크하면 내 마켓 찜하기 작업 시 이 마켓이 포함됩니다.
+
+
+
+
+
+
+
+
체크하면 품앗이 찜하기에서 다른 사람들이 이 마켓을 찜할 수 있습니다.
+
+
+
+
+
+