diff --git a/main.py b/main.py
index ff5fb5f..b3292eb 100644
--- a/main.py
+++ b/main.py
@@ -131,9 +131,9 @@ class ExtensionInstaller(QWidget):
guide = QLabel(
"설치 안내:
"
- "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
"
- "2. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
"
- "3. 설치 후 브라우저별 단계별 안내를 참고하세요.")
+ # "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
"
+ "1. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
"
+ "2. 설치 후 브라우저별 단계별 안내를 참고하세요.")
guide.setWordWrap(True)
guide.setStyleSheet("margin-bottom:12px;")
layout.addWidget(guide)
@@ -256,7 +256,7 @@ class ExtensionInstaller(QWidget):
row2.setTextFormat(Qt.RichText)
row2.setWordWrap(True)
# 3번 압축해제된 확장프로그램 로드
- row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 버튼 클릭")
+ row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 또는 [압축해제된 확장앱 설치] 버튼 클릭")
row3.setTextFormat(Qt.RichText)
row3.setWordWrap(True)
# 4번 폴더 선택 및 복사
diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js
index 9c53e72..45bd5f4 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,11 @@ chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
- // 새 어록 감지 알람 생성 (1분마다)
- chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 });
+ // 새 어록 감지 알람 생성 (5분마다)
+ chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 });
+
+ // 1시간마다 자동 품앗이 찜 알람
+ chrome.alarms.create("autoMutualZzim", { periodInMinutes: 60 });
// 초기 마지막 확인 시간 설정
chrome.storage.local.set({ lastSayingsCheck: Date.now() });
@@ -35,21 +38,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 +308,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) {
@@ -118,6 +328,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
console.log("[background.js] 서비스 워커 유지 알람 실행됨");
} else if (alarm.name === "checkNewSayings") {
checkForNewSayings();
+ } else if (alarm.name === "autoMutualZzim") {
+ console.log("[background.js] autoMutualZzim 주기 실행");
+ chrome.runtime.sendMessage({ action: "autoMutualZzim" });
}
});
@@ -139,10 +352,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;
@@ -1762,6 +1975,7 @@ async function handleKoreanToChinese(selectedText, tab) {
if (!selectedText) {
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '텍스트 선택 필요',
message: '번역할 텍스트를 먼저 선택해주세요.'
});
@@ -1804,6 +2018,7 @@ async function handleKoreanToChinese(selectedText, tab) {
// 한국어도 중국어도 아닌 경우
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '지원하지 않는 언어',
message: '한국어 또는 중국어 텍스트를 선택해주세요.'
});
@@ -1811,14 +2026,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 +2057,7 @@ async function handleKoreanToChinese(selectedText, tab) {
console.error('[background.js] 한국어↔중국어 번역 중 오류:', error);
chrome.notifications.create({
type: 'basic',
+ iconUrl: 'icon.png',
title: '번역 오류',
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
});
@@ -1835,31 +2066,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 +2431,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] 팝업 표시 완료');
}
// ==================== 시간 알람 기능 ====================
@@ -1896,7 +2705,9 @@ class TimeAlarmManager {
enabled: true,
workTime: 60, // 분
restTime: 5, // 분
- autoZzim: false
+ autoZzim: false,
+ pomodoro: false,
+ cycle: 0 // 포모도로 현재 사이클(0~3)
};
this.startTime = null;
}
@@ -1951,9 +2762,10 @@ class TimeAlarmManager {
}
this.startTime = Date.now();
- const workTimeMs = this.settings.workTime * 60 * 1000;
+ const workMin = this.settings.pomodoro ? 35 : this.settings.workTime;
+ const workTimeMs = workMin * 60 * 1000;
- console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}분`);
+ console.log(`[TimeAlarm] 작업 타이머 시작: ${workMin}분`);
this.workTimer = setTimeout(() => {
this.showRestModal();
@@ -1984,7 +2796,8 @@ class TimeAlarmManager {
}
startBreakTimer(popupId) {
- const breakTimeMs = this.settings.restTime * 60 * 1000;
+ const breakTimeMin = this.settings.restTime;
+ const breakTimeMs = breakTimeMin * 60 * 1000;
console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}분`);
@@ -1999,6 +2812,11 @@ class TimeAlarmManager {
// 작업 완료 알림
this.showWorkCompleteNotification();
+ // 포모도로 모드: 사이클 증가 및 리셋
+ if (this.settings.pomodoro) {
+ this.settings.cycle = (this.settings.cycle + 1) % 4;
+ }
+
// 다음 작업 타이머 시작
this.startWorkTimer();
@@ -2010,7 +2828,7 @@ class TimeAlarmManager {
type: 'basic',
iconUrl: 'icon.png',
title: '휴식 시간입니다! 🧘♀️',
- message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.`
+ message: `${this.settings.pomodoro ? 35 : this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.`
});
}
@@ -2073,7 +2891,7 @@ class TimeAlarmManager {
// 남은 시간 계산
const now = Date.now();
const elapsed = now - this.startTime;
- const totalWorkTime = this.settings.workTime * 60 * 1000;
+ const totalWorkTime = (this.settings.pomodoro ? 35 : this.settings.workTime) * 60 * 1000;
const remainingTime = totalWorkTime - elapsed;
if (remainingTime <= 0) {
@@ -2086,7 +2904,7 @@ class TimeAlarmManager {
return {
isRunning: true,
remainingTime: remainingTime,
- workTime: this.settings.workTime,
+ workTime: this.settings.pomodoro ? 35 : this.settings.workTime,
restTime: this.settings.restTime,
startTime: this.startTime,
elapsed: elapsed
@@ -2216,6 +3034,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleAddToZzim(message, sendResponse);
return true; // 비동기 응답을 위해 true 반환
}
+
+ if (message.action === 'executeBackgroundZzim') {
+ handleExecuteBackgroundZzim(message, sendResponse);
+ return true; // 비동기 응답을 위해 true 반환
+ }
});
// 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거)
@@ -2425,3 +3248,467 @@ 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: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
+ });
+ }
+}
+
+// ===== 메시지 리스너: 백그라운드 자동 품앗이 =====
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message && message.action === 'autoMutualZzim') {
+ autoMutualZzim()
+ .then(() => sendResponse({ success: true }))
+ .catch(err => sendResponse({ success: false, error: err.message }));
+ return true; // async 응답
+ }
+});
+
+// ===== 자동 품앗이 찜 메인 함수 =====
+async function autoMutualZzim() {
+ try {
+ console.log('[autoMutualZzim] 시작');
+
+ // 1. 기본 정보 및 설정
+ const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
+ if (!access_token || !user_id) {
+ console.log('[autoMutualZzim] 토큰/사용자 정보 없음');
+ return;
+ }
+
+ const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
+
+ // 2. 내 마일리지 및 회원등급 조회
+ const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=available_zzim_mile,membership_level`, {
+ headers: {
+ Authorization: `Bearer ${access_token}`,
+ apikey: SUPABASE_ANON_KEY,
+ 'Content-Type': 'application/json'
+ }
+ });
+ if (!userRes.ok) {
+ console.warn('[autoMutualZzim] 사용자 조회 실패', userRes.status);
+ return;
+ }
+ const me = (await userRes.json())[0];
+ const myAvail = me?.available_zzim_mile || 0;
+ const myLevel = me?.membership_level || 'basic';
+
+ // 2-1. 등급별 한도 확인
+ const levelRes = await fetch(`${SUPABASE_URL}/rest/v1/membership_levels?level=eq.${myLevel}&select=max_zzim_mileage,mileage_per_zzim`, {
+ headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
+ });
+ const levelConf = levelRes.ok ? (await levelRes.json())[0] : { max_zzim_mileage: 500, mileage_per_zzim: 1 };
+
+ if (myAvail >= levelConf.max_zzim_mileage) {
+ console.log('[autoMutualZzim] 마일리지 최대치 도달 – 실행 안 함');
+ return;
+ }
+
+ // 3. 후보 마켓 조회 (v_user_market_stats 뷰)
+ const candRes = await fetch(`${SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, {
+ headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json' }
+ });
+ if (!candRes.ok) {
+ console.warn('[autoMutualZzim] 후보 조회 실패', candRes.status);
+ return;
+ }
+ const users = await candRes.json();
+ const pool = [];
+ users.forEach(u => {
+ const markets = u.my_markets || [];
+ markets.forEach(m => {
+ if (m.is_visible !== false && m.for_mutual_zzim !== false) {
+ pool.push({ ...m, owner_user_id: u.user_id, owner_available_mileage: u.available_zzim_mile });
+ }
+ });
+ });
+ if (pool.length === 0) {
+ console.log('[autoMutualZzim] 후보 마켓 없음');
+ return;
+ }
+
+ // 4. 랜덤 1개 선택
+ const target = pool[Math.floor(Math.random() * pool.length)];
+ const targetUrl = `${target.market_url.replace(/\/$/, '')}/category/ALL?cp=1&auto_zzim=true&max_zzim=50`;
+
+ // 5. 백그라운드 찜 실행
+ const msg = {
+ action: 'executeBackgroundZzim',
+ market: { ...target, target_url: targetUrl },
+ zzimType: 'mutual',
+ userId: user_id,
+ accessToken: access_token,
+ settings: { totalDelay: 1000, latestFirst: false, backgroundMode: true }
+ };
+
+ chrome.runtime.sendMessage(msg, (res) => {
+ if (chrome.runtime.lastError) {
+ console.error('[autoMutualZzim] runtime error', chrome.runtime.lastError.message);
+ } else if (res && res.success) {
+ chrome.notifications.create({
+ type: 'basic',
+ iconUrl: 'icon.png',
+ title: '🎉 자동 품앗이 완료',
+ message: `${target.market_nickname || '마켓'}에 찜 50개 완료!`
+ });
+ } else {
+ console.warn('[autoMutualZzim] 실행 실패', res?.error);
+ }
+ });
+ } catch (e) {
+ console.error('[autoMutualZzim] 오류', e);
+ }
+}
diff --git a/wrmc_ext/content.js b/wrmc_ext/content.js
index 9699c14..0e5f15e 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 };
});
-document.addEventListener("contextmenu", (e) => {
- lastContextMenuPos = { 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;
+ }
+}
-// ESC 키로 모달 닫기
+// 향상된 텍스트 선택 감지
+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);
+// }
+// }
+// });
+
+// 향상된 단축키 처리
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);
@@ -170,6 +693,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
+ // 선택 영역 교체 요청
+ if (message.action === 'replaceSelectedText' && message.text) {
+ // 선택 영역이 있으면 해당 부분만 교체, 아니면 포커스된 엘리먼트 전체 교체
+ if (!replaceSelectedText(message.text)) {
+ // selection이 없다면, 포커스된 .edit 등 target element 전체 교체 시도
+ const activeElem = document.activeElement;
+ if (activeElem && activeElem.classList.contains('edit')) {
+ activeElem.innerText = message.text;
+ } else {
+ // 특정 요소를 찾고 싶으면 아래처럼 직접 지정
+ const target = document.querySelector('.edit[contenteditable="false"]');
+ if (target) {
+ target.innerText = message.text;
+ }
+ }
+ }
+ sendResponse({success: true});
+ return true;
+ }
+
// 로딩 인디케이터 제거 요청
if (message.action === "hideLoading") {
removeLoadingIndicator();
@@ -355,6 +898,18 @@ function positionTooltip(tooltip, pos) {
}
}
+// selection 영역 교체 함수 (간단 버전)
+function replaceSelectedText(newText) {
+ const selection = window.getSelection();
+ if (selection && selection.rangeCount) {
+ const range = selection.getRangeAt(0);
+ range.deleteContents();
+ range.insertNode(document.createTextNode(newText));
+ return true;
+ }
+ return false;
+}
+
function removeTooltip() {
if (tooltipEl && tooltipEl.parentNode) {
tooltipEl.parentNode.removeChild(tooltipEl);
@@ -858,9 +1413,1588 @@ 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();
}
}
+
+// 자동 찜하기 전역 변수들
+let autoZzimState = {
+ isRunning: false,
+ maxZzim: 50,
+ actualZzimCount: 0,
+ currentPage: 1,
+ delay: 1000,
+ statusDiv: null,
+ startTime: null
+};
+
+// 찜하기 버튼 찾기 함수 (전역) - 정확한 선택자 사용
+ function findZzimButtons() {
+ console.log('[AutoZzim] 찜 버튼 찾기 시작...');
+
+ // 네이버 스마트스토어 정확한 선택자 사용
+ const zzimButtonSelector = 'div#CategoryProducts button.zzim_button[aria-pressed="false"]';
+
+ try {
+ const zzimButtons = document.querySelectorAll(zzimButtonSelector);
+ console.log(`[AutoZzim] 찜 가능한 버튼 발견: ${zzimButtons.length}개`);
+
+ // 실제로 보이고 클릭 가능한 버튼만 필터링
+ const visibleButtons = Array.from(zzimButtons).filter(btn => {
+ const rect = btn.getBoundingClientRect();
+ const style = window.getComputedStyle(btn);
+
+ const isVisible = rect.width > 0 &&
+ rect.height > 0 &&
+ style.display !== 'none' &&
+ style.visibility !== 'hidden' &&
+ style.opacity !== '0' &&
+ !btn.disabled;
+
+ if (isVisible) {
+ // 버튼 주변에 다른 클릭 가능한 요소가 있는지 확인
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ // 버튼 중심 좌표에서 실제 클릭될 요소 확인
+ const elementAtPoint = document.elementFromPoint(centerX, centerY);
+
+ // 클릭될 요소가 찜 버튼이거나 찜 버튼의 자식 요소인지 확인
+ const isCorrectElement = elementAtPoint === btn || btn.contains(elementAtPoint);
+
+ if (!isCorrectElement) {
+ console.log('[AutoZzim] 버튼이 다른 요소에 가려짐:', {
+ button: btn,
+ elementAtPoint: elementAtPoint,
+ elementAtPointTag: elementAtPoint?.tagName,
+ elementAtPointClass: elementAtPoint?.className
+ });
+ return false;
+ }
+
+ // 버튼 텍스트 확인 (찜하기 관련 텍스트가 있는지)
+ const buttonText = btn.textContent?.trim().toLowerCase() || '';
+ const buttonAriaLabel = btn.getAttribute('aria-label')?.toLowerCase() || '';
+
+ const isZzimButton = buttonText.includes('찜') ||
+ buttonText.includes('하트') ||
+ buttonAriaLabel.includes('찜') ||
+ buttonAriaLabel.includes('wishlist') ||
+ btn.className.includes('zzim') ||
+ btn.className.includes('wish');
+
+ if (!isZzimButton) {
+ console.log('[AutoZzim] 찜하기 버튼이 아님:', {
+ text: buttonText,
+ ariaLabel: buttonAriaLabel,
+ className: btn.className
+ });
+ return false;
+ }
+
+ // 판매자 정보나 다른 기능 버튼이 아닌지 확인
+ const isNotOtherButton = !buttonText.includes('판매자') &&
+ !buttonText.includes('상점') &&
+ !buttonText.includes('문의') &&
+ !buttonText.includes('연락') &&
+ !buttonText.includes('전화') &&
+ !buttonText.includes('리뷰') &&
+ !buttonText.includes('평점') &&
+ !buttonAriaLabel.includes('판매자') &&
+ !buttonAriaLabel.includes('상점');
+
+ if (!isNotOtherButton) {
+ console.log('[AutoZzim] 다른 기능 버튼임:', {
+ text: buttonText,
+ ariaLabel: buttonAriaLabel
+ });
+ return false;
+ }
+
+ console.log('[AutoZzim] 찜 가능한 버튼 확인:', {
+ text: btn.textContent?.trim().substring(0, 30),
+ className: btn.className,
+ ariaPressed: btn.getAttribute('aria-pressed'),
+ position: `x:${Math.round(rect.x)}, y:${Math.round(rect.y)}`,
+ size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
+ isCorrectElement: isCorrectElement,
+ isZzimButton: isZzimButton,
+ isNotOtherButton: isNotOtherButton
+ });
+ }
+
+ return isVisible;
+ });
+
+ console.log(`[AutoZzim] 최종 찜 가능한 버튼: ${visibleButtons.length}개`);
+ return visibleButtons;
+
+ } catch (error) {
+ console.error('[AutoZzim] 찜 버튼 찾기 오류:', error);
+ return [];
+ }
+}
+
+// 이미 찜한 상품인지 확인 함수 (전역) - 단순화
+function isAlreadyZzimed_Improved(button) {
+ try {
+ // aria-pressed 속성으로 확인 (가장 정확한 방법)
+ const ariaPressed = button.getAttribute('aria-pressed');
+ const isZzimed = ariaPressed === 'true';
+
+ if (isZzimed) {
+ console.log('[AutoZzim] 이미 찜됨 (aria-pressed=true)');
+ }
+
+ return isZzimed;
+
+ } catch (e) {
+ console.error('[AutoZzim] 찜 상태 확인 오류:', e);
+ return false;
+ }
+ }
+
+// 찜하기 버튼 클릭 함수 (전역) - 개선된 버전
+ async function clickZzimButton(button) {
+ console.log('[AutoZzim] 찜 버튼 클릭 시작:', {
+ text: button.textContent?.trim().substring(0, 30),
+ className: button.className,
+ ariaPressed: button.getAttribute('aria-pressed'),
+ tagName: button.tagName,
+ id: button.id
+ });
+
+ return new Promise((resolve) => {
+ try {
+ // 클릭 전 상태 확인
+ const beforePressed = button.getAttribute('aria-pressed');
+ console.log('[AutoZzim] 클릭 전 aria-pressed:', beforePressed);
+
+ if (beforePressed === 'true') {
+ console.log('[AutoZzim] 이미 찜된 상품, 건너뛰기');
+ resolve(false);
+ return;
+ }
+
+ // 버튼이 클릭 가능한지 확인
+ if (button.disabled) {
+ console.log('[AutoZzim] 버튼이 비활성화되어 있음');
+ resolve(false);
+ return;
+ }
+
+ // 버튼 주변 요소 확인 (판매자 정보 등 다른 클릭 가능한 요소가 있는지)
+ const rect = button.getBoundingClientRect();
+ console.log('[AutoZzim] 버튼 위치 및 크기:', {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ centerX: rect.x + rect.width / 2,
+ centerY: rect.y + rect.height / 2
+ });
+
+ // 버튼이 화면에 보이도록 스크롤 (부드럽게)
+ button.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center'
+ });
+
+ setTimeout(() => {
+ if (!autoZzimState.isRunning) {
+ console.log('[AutoZzim] 찜하기가 중단됨');
+ resolve(false);
+ return;
+ }
+
+ // 버튼 클릭 시도 (이벤트 전파 방지)
+ try {
+ console.log('[AutoZzim] 정확한 버튼 클릭 시도 (이벤트 전파 방지)');
+
+ // 방법 1: 이벤트 전파를 막는 직접 클릭
+ const clickHandler = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ console.log('[AutoZzim] 클릭 이벤트 전파 차단됨');
+ };
+
+ // 임시 이벤트 리스너 추가 (이벤트 전파 방지)
+ button.addEventListener('click', clickHandler, { capture: true, once: true });
+
+ // 포커스 설정
+ if (button.focus) {
+ button.focus();
+ }
+
+ // 직접 클릭 (가장 안전한 방법)
+ button.click();
+
+ console.log('[AutoZzim] 직접 클릭 완료');
+
+ // 클릭 후 상태 변화 확인
+ let checkAttempts = 0;
+ const maxAttempts = 10; // 최대 5초간 확인
+
+ const checkStateChange = () => {
+ checkAttempts++;
+ const afterPressed = button.getAttribute('aria-pressed');
+
+ console.log(`[AutoZzim] 상태 확인 ${checkAttempts}/${maxAttempts}:`, {
+ before: beforePressed,
+ after: afterPressed,
+ success: beforePressed === 'false' && afterPressed === 'true'
+ });
+
+ // 성공 조건: false → true로 변경
+ if (beforePressed === 'false' && afterPressed === 'true') {
+ console.log('[AutoZzim] ✅ 찜하기 성공!');
+ autoZzimState.actualZzimCount++;
+
+ // 확인 모달 처리 (필요한 경우)
+ setTimeout(() => {
+ handleConfirmationModals();
+ }, 200);
+
+ resolve(true);
+ return;
+ }
+
+ // 재시도 조건
+ if (checkAttempts < maxAttempts) {
+ setTimeout(checkStateChange, 500); // 0.5초마다 재확인
+ } else {
+ console.log('[AutoZzim] ❌ 찜하기 실패 (상태 변화 없음), 대안 방법 시도');
+
+ // 대안 방법: 더 정확한 이벤트 생성
+ try {
+ console.log('[AutoZzim] 대안 방법: 정확한 좌표로 클릭 이벤트 생성');
+
+ // 버튼의 정확한 중심 좌표 계산
+ const updatedRect = button.getBoundingClientRect();
+ const centerX = updatedRect.left + updatedRect.width / 2;
+ const centerY = updatedRect.top + updatedRect.height / 2;
+
+ // 더 정확한 마우스 이벤트 생성 (이벤트 전파 방지)
+ const mouseDownEvent = new MouseEvent('mousedown', {
+ bubbles: false, // 이벤트 전파 방지
+ cancelable: true,
+ view: window,
+ clientX: centerX,
+ clientY: centerY,
+ button: 0
+ });
+
+ const mouseUpEvent = new MouseEvent('mouseup', {
+ bubbles: false, // 이벤트 전파 방지
+ cancelable: true,
+ view: window,
+ clientX: centerX,
+ clientY: centerY,
+ button: 0
+ });
+
+ const clickEvent = new MouseEvent('click', {
+ bubbles: false, // 이벤트 전파 방지
+ cancelable: true,
+ view: window,
+ clientX: centerX,
+ clientY: centerY,
+ button: 0
+ });
+
+ // 순차적으로 이벤트 발생
+ button.dispatchEvent(mouseDownEvent);
+ setTimeout(() => {
+ button.dispatchEvent(mouseUpEvent);
+ setTimeout(() => {
+ button.dispatchEvent(clickEvent);
+
+ // 최종 상태 확인
+ setTimeout(() => {
+ const finalPressed = button.getAttribute('aria-pressed');
+ if (beforePressed === 'false' && finalPressed === 'true') {
+ console.log('[AutoZzim] ✅ 대안 방법으로 찜하기 성공!');
+ autoZzimState.actualZzimCount++;
+ resolve(true);
+ } else {
+ console.log('[AutoZzim] ❌ 대안 방법도 실패');
+ resolve(false);
+ }
+ }, 1000);
+ }, 50);
+ }, 50);
+
+ } catch (alternativeError) {
+ console.error('[AutoZzim] 대안 방법 실패:', alternativeError);
+ resolve(false);
+ }
+ }
+ };
+
+ // 첫 번째 상태 확인은 0.5초 후
+ setTimeout(checkStateChange, 500);
+
+ } catch (clickError) {
+ console.error('[AutoZzim] 버튼 클릭 오류:', clickError);
+ resolve(false);
+ }
+
+ }, 800); // 스크롤 후 충분한 대기 시간
+
+ } catch (e) {
+ console.error('[AutoZzim] 찜 버튼 클릭 중 예외 오류:', e);
+ resolve(false);
+ }
+ });
+}
+
+// 확인 모달/팝업 처리 함수 (전역) - 개선된 버전
+function handleConfirmationModals() {
+ console.log('[AutoZzim] 확인 모달 처리 시작');
+
+ // 찜하기 관련 확인 모달만 처리하도록 제한
+ const confirmSelectors = [
+ // 네이버 스마트스토어 찜하기 전용 확인 버튼
+ 'button[data-testid="wishlist-confirm"]',
+ 'button[data-testid="zzim-confirm"]',
+ '.zzim_confirm_btn',
+ '.wishlist_confirm_btn',
+
+ // 일반적인 확인 버튼 (찜하기 관련 텍스트가 있는 경우만)
+ 'button[class*="confirm"]:contains("찜")',
+ 'button[class*="confirm"]:contains("확인")',
+
+ // 모달 내부의 확인 버튼 (찜하기 관련만)
+ '.modal button[class*="confirm"]',
+ '.popup button[class*="confirm"]',
+ '.dialog button[class*="confirm"]',
+
+ // 레이어 팝업 내 확인 버튼
+ '[class*="layer"] button[class*="confirm"]',
+ '[role="dialog"] button[class*="primary"]'
+ ];
+
+ for (const selector of confirmSelectors) {
+ try {
+ let confirmButtons = [];
+
+ // :contains() 선택자 수동 처리
+ if (selector.includes(':contains(')) {
+ const [baseSelector, containsText] = selector.split(':contains(');
+ const searchText = containsText.replace(/[\(\)\"\']/g, '');
+ const candidateElements = document.querySelectorAll(baseSelector);
+
+ confirmButtons = Array.from(candidateElements).filter(el => {
+ const text = el.textContent && el.textContent.trim();
+ return text && text.includes(searchText);
+ });
+ } else {
+ confirmButtons = Array.from(document.querySelectorAll(selector));
+ }
+
+ // 보이고 클릭 가능한 버튼 중에서 찜하기 관련만 필터링
+ for (const btn of confirmButtons) {
+ if (btn && btn.offsetParent !== null && !btn.disabled) {
+ const rect = btn.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+
+ // 버튼 텍스트 확인 - 찜하기 관련 텍스트가 있는지 확인
+ const buttonText = btn.textContent?.trim().toLowerCase() || '';
+ const isZzimRelated = buttonText.includes('찜') ||
+ buttonText.includes('확인') ||
+ buttonText.includes('ok') ||
+ buttonText.includes('예') ||
+ buttonText.includes('yes');
+
+ // 판매자 정보나 다른 팝업이 아닌지 확인
+ const isNotSellerInfo = !buttonText.includes('판매자') &&
+ !buttonText.includes('상점') &&
+ !buttonText.includes('업체') &&
+ !buttonText.includes('문의') &&
+ !buttonText.includes('연락') &&
+ !buttonText.includes('전화');
+
+ if (isZzimRelated && isNotSellerInfo) {
+ console.log('[AutoZzim] 찜하기 관련 확인 버튼 클릭:', {
+ selector: selector,
+ text: buttonText,
+ className: btn.className
+ });
+
+ // 이벤트 전파를 막고 클릭
+ const clickHandler = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ };
+
+ btn.addEventListener('click', clickHandler, { capture: true, once: true });
+ btn.click();
+
+ return; // 첫 번째 적절한 확인 버튼만 클릭하고 종료
+ } else {
+ console.log('[AutoZzim] 찜하기와 무관한 버튼 무시:', {
+ text: buttonText,
+ isZzimRelated: isZzimRelated,
+ isNotSellerInfo: isNotSellerInfo
+ });
+ }
+ }
+ }
+ }
+ } catch (e) {
+ console.log('[AutoZzim] 확인 버튼 검색 오류:', selector, e.message);
+ }
+ }
+
+ console.log('[AutoZzim] 찜하기 관련 확인 버튼을 찾지 못했습니다');
+}
+
+// 현재 페이지 찜하기 처리 함수 (전역) - 개선된 버전
+ async function processCurrentPage() {
+ if (!autoZzimState.isRunning || autoZzimState.actualZzimCount >= autoZzimState.maxZzim) {
+ console.log('[AutoZzim] 찜하기 중단 또는 목표 달성');
+ return false;
+ }
+
+ console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 찜하기 처리 시작`);
+ updateZzimStatus(autoZzimState.statusDiv, `페이지 ${autoZzimState.currentPage} 분석 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
+
+ // 찜 가능한 버튼 찾기
+ const zzimButtons = findZzimButtons();
+ console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에서 찾은 찜 가능한 버튼: ${zzimButtons.length}개`);
+
+ if (zzimButtons.length === 0) {
+ console.log('[AutoZzim] 현재 페이지에 찜할 상품이 없음');
+ return false;
+ }
+
+ let processedInPage = 0;
+
+ // 각 버튼을 순차적으로 처리
+ for (let i = 0; i < zzimButtons.length && autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim; i++) {
+ const button = zzimButtons[i];
+
+ updateZzimStatus(autoZzimState.statusDiv, `찜하기 진행 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim}) - 페이지 ${autoZzimState.currentPage}`);
+
+ console.log(`[AutoZzim] ${i + 1}/${zzimButtons.length}번째 버튼 처리 중...`);
+
+ const success = await clickZzimButton(button);
+ if (success) {
+ processedInPage++;
+ console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에서 ${processedInPage}번째 찜 성공`);
+ }
+
+ // 다음 버튼 처리 전 대기 (설정된 간격)
+ if (i < zzimButtons.length - 1 && autoZzimState.isRunning) {
+ console.log(`[AutoZzim] ${autoZzimState.delay}ms 대기 중...`);
+ await new Promise(resolve => setTimeout(resolve, autoZzimState.delay));
+ }
+ }
+
+ console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 완료: ${processedInPage}개 찜함`);
+ return processedInPage > 0;
+ }
+
+// 자동 찜하기 메인 루프 함수 (전역)
+async function autoZzimMainLoop() {
+ console.log('[AutoZzim] 메인 루프 시작');
+
+ while (autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim && autoZzimState.currentPage <= 10) {
+ console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 시작`);
+
+ // 페이지 로드 대기
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // 현재 페이지 처리
+ const hasProducts = await processCurrentPage();
+
+ if (!hasProducts) {
+ console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에 더 이상 찜할 상품이 없음`);
+
+ // 다음 페이지로 이동 시도
+ updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
+ const moved = await goToNextPage();
+ if (!moved) {
+ console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
+ break;
+ }
+ } else {
+ // 현재 페이지에서 더 찜할 수 있는지 확인
+ const remainingButtons = findZzimButtons();
+ if (remainingButtons.length === 0) {
+ // 다음 페이지로 이동
+ updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
+ const moved = await goToNextPage();
+ if (!moved) {
+ console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
+ break;
+ }
+ }
+ }
+
+ // 페이지 간 대기
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ }
+
+ // 완료 처리
+ console.log(`[AutoZzim] 찜하기 완료: 실제 ${autoZzimState.actualZzimCount}개 찜함`);
+ updateZzimStatus(autoZzimState.statusDiv, `찜하기 완료! (실제 ${autoZzimState.actualZzimCount}개 찜함)`);
+
+ // 상태 초기화
+ autoZzimState.isRunning = false;
+
+ // 5초 후 상태 UI 제거
+ setTimeout(() => {
+ if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
+ autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
+ autoZzimState.statusDiv = null;
+ }
+ }, 5000);
+ }
+
+// 자동 찜하기 기능 (개선된 버전)
+function startAutoZzim(maxZzim = 50, delay = 1000) {
+ console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay });
+
+ // 상태 초기화
+ autoZzimState = {
+ isRunning: true,
+ maxZzim: maxZzim,
+ actualZzimCount: 0,
+ currentPage: 1,
+ delay: delay,
+ statusDiv: null,
+ startTime: Date.now()
+ };
+
+ // 상태 표시 UI 생성
+ autoZzimState.statusDiv = createZzimStatusUI();
+ updateZzimStatus(autoZzimState.statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`);
+
+ // 60초 후 자동 종료
+ setTimeout(() => {
+ if (autoZzimState.isRunning) {
+ autoZzimState.isRunning = false;
+ console.log('[AutoZzim] 시간 초과로 찜하기 종료');
+ updateZzimStatus(autoZzimState.statusDiv, `시간 초과로 종료 (실제 ${autoZzimState.actualZzimCount}개 찜함)`);
+
+ // 5초 후 상태 UI 제거
+ setTimeout(() => {
+ if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
+ autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
+ autoZzimState.statusDiv = null;
+ }
+ }, 5000);
+ }
+ }, 60000);
+
+ // 메인 루프 시작
+ setTimeout(autoZzimMainLoop, 2000); // 페이지 로드 후 2초 대기
+}
+
+// 다음 페이지로 이동 함수 (기존 goToNextPage 함수 수정)
+async function goToNextPage() {
+ try {
+ console.log(`[AutoZzim] 다음 페이지로 이동 시도: ${autoZzimState.currentPage} → ${autoZzimState.currentPage + 1}`);
+
+ // 네이버 스마트스토어 URL 분석
+ const urlInfo = parseNaverSmartStoreUrl(window.location.href);
+
+ if (!urlInfo) {
+ console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href);
+ return false;
+ }
+
+ if (!urlInfo.isProductListPage) {
+ console.log('[AutoZzim] 상품 목록 페이지가 아님, 올바른 페이지로 이동');
+
+ // 올바른 상품 목록 페이지 URL 생성
+ const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, false, 50);
+
+ console.log('[AutoZzim] 올바른 상품 목록 페이지로 이동:', correctUrl);
+ window.location.href = correctUrl;
+ return true;
+ }
+
+ // 1단계: 현재 페이지 번호 확인
+ const currentPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`);
+ let actualCurrentPage = 1;
+
+ if (currentPageButton) {
+ const currentPageId = currentPageButton.getAttribute('data-shp-contents-id');
+ actualCurrentPage = parseInt(currentPageId) || 1;
+ console.log(`[AutoZzim] 현재 페이지 확인: ${actualCurrentPage} (DOM에서 감지)`);
+ } else {
+ // aria-current 없이 data-shp-contents-id로만 확인
+ const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`);
+ console.log(`[AutoZzim] 전체 페이지 버튼 개수: ${allPageButtons.length}`);
+
+ // URL의 cp 파라미터로 현재 페이지 추정
+ const urlParams = new URLSearchParams(window.location.search);
+ const cpParam = urlParams.get('cp');
+ if (cpParam) {
+ actualCurrentPage = parseInt(cpParam) || 1;
+ console.log(`[AutoZzim] URL 파라미터에서 현재 페이지 확인: ${actualCurrentPage}`);
+ }
+ }
+
+ // currentPage 변수 업데이트
+ autoZzimState.currentPage = actualCurrentPage;
+ const nextPageNum = autoZzimState.currentPage + 1;
+
+ console.log(`[AutoZzim] 다음 페이지 번호: ${nextPageNum}`);
+
+ // 2단계: 다음 페이지 버튼 찾기 (네이버 스마트스토어 특화)
+ const nextPageSelectors = [
+ // 네이버 스마트스토어 페이지 버튼 (가장 정확한 방법)
+ `div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id='${nextPageNum}']`,
+
+ // 일반적인 다음 페이지 버튼
+ 'a.pagination_next:not(.pagination_disabled)',
+ 'a[class*="pagination"][class*="next"]:not([class*="disabled"])',
+ 'button[class*="pagination"][class*="next"]:not([disabled])',
+ '.paginate_next:not(.disabled)',
+ '[class*="paging"] a[class*="next"]:not([class*="disabled"])',
+
+ // aria-label 기반
+ 'a[aria-label*="다음"]:not([aria-disabled="true"])',
+ 'button[aria-label*="다음"]:not([disabled])',
+ 'a[title*="다음"]:not([class*="disabled"])',
+
+ // 페이지 번호로 다음 페이지 찾기 (URL 기반)
+ `a[href*="cp=${nextPageNum}"]`,
+ `button[data-page="${nextPageNum}"]`,
+ `a[href*="page=${nextPageNum}"]`
+ ];
+
+ let nextButton = null;
+ let selectedMethod = '';
+
+ for (const selector of nextPageSelectors) {
+ try {
+ const buttons = document.querySelectorAll(selector);
+ console.log(`[AutoZzim] 선택자 "${selector}" 검색 결과: ${buttons.length}개`);
+
+ for (const btn of buttons) {
+ if (btn && btn.offsetParent !== null && !btn.disabled && !btn.classList.contains('disabled')) {
+ nextButton = btn;
+ selectedMethod = selector;
+ console.log(`[AutoZzim] 다음 페이지 버튼 발견:`, {
+ selector: selector,
+ text: btn.textContent?.trim(),
+ className: btn.className,
+ dataShpContentsId: btn.getAttribute('data-shp-contents-id'),
+ href: btn.href,
+ tagName: btn.tagName
+ });
+ break;
+ }
+ }
+ if (nextButton) break;
+ } catch (e) {
+ console.log(`[AutoZzim] 선택자 오류 (${selector}):`, e.message);
+ }
+ }
+
+ if (nextButton) {
+ // 다음 페이지 버튼 클릭
+ console.log(`[AutoZzim] 다음 페이지 버튼 클릭 시도 (방법: ${selectedMethod})`);
+
+ // 버튼이 화면에 보이도록 스크롤
+ nextButton.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center'
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // 클릭 전 상태 로그
+ console.log('[AutoZzim] 클릭 전 버튼 상태:', {
+ disabled: nextButton.disabled,
+ offsetParent: nextButton.offsetParent !== null,
+ classList: Array.from(nextButton.classList)
+ });
+
+ // 다양한 방식으로 클릭 시도
+ try {
+ // 방법 1: 일반 클릭
+ nextButton.click();
+ console.log('[AutoZzim] 일반 클릭 완료');
+ } catch (e1) {
+ console.log('[AutoZzim] 일반 클릭 실패, 이벤트 방식 시도:', e1.message);
+
+ try {
+ // 방법 2: 마우스 이벤트 시뮬레이션
+ const rect = nextButton.getBoundingClientRect();
+ const clickEvent = new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ clientX: rect.left + rect.width / 2,
+ clientY: rect.top + rect.height / 2
+ });
+ nextButton.dispatchEvent(clickEvent);
+ console.log('[AutoZzim] 마우스 이벤트 클릭 완료');
+ } catch (e2) {
+ console.log('[AutoZzim] 마우스 이벤트 클릭 실패:', e2.message);
+
+ // 방법 3: href가 있는 경우 직접 이동
+ if (nextButton.href) {
+ console.log('[AutoZzim] href로 직접 이동:', nextButton.href);
+ window.location.href = nextButton.href;
+ }
+ }
+ }
+
+ // 페이지 로드 대기
+ console.log('[AutoZzim] 페이지 로드 대기 중...');
+ await new Promise(resolve => setTimeout(resolve, 3000));
+
+ // 페이지 변경 확인
+ const newPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`);
+ if (newPageButton) {
+ const newPageId = newPageButton.getAttribute('data-shp-contents-id');
+ const newPageNum = parseInt(newPageId) || 1;
+
+ if (newPageNum > actualCurrentPage) {
+ console.log(`[AutoZzim] ✅ 페이지 이동 성공: ${actualCurrentPage} → ${newPageNum}`);
+ autoZzimState.currentPage = newPageNum;
+ return true;
+ } else {
+ console.log(`[AutoZzim] ❌ 페이지 이동 실패: 여전히 ${newPageNum} 페이지`);
+ }
+ } else {
+ // aria-current가 없어도 URL로 확인
+ const urlParams = new URLSearchParams(window.location.search);
+ const newCpParam = urlParams.get('cp');
+ if (newCpParam && parseInt(newCpParam) > actualCurrentPage) {
+ const newPageNum = parseInt(newCpParam);
+ console.log(`[AutoZzim] ✅ 페이지 이동 성공 (URL 확인): ${actualCurrentPage} → ${newPageNum}`);
+ autoZzimState.currentPage = newPageNum;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // 3단계: 버튼이 없으면 URL 직접 변경
+ console.log('[AutoZzim] 다음 페이지 버튼이 없음, URL 직접 변경 시도');
+
+ // 마지막 페이지 번호 확인
+ const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`);
+ const pageNumbers = Array.from(allPageButtons).map(btn => {
+ const id = btn.getAttribute('data-shp-contents-id');
+ return parseInt(id) || 0;
+ }).filter(num => num > 0);
+
+ const maxPage = Math.max(...pageNumbers);
+ console.log(`[AutoZzim] 감지된 페이지 번호들:`, pageNumbers);
+ console.log(`[AutoZzim] 최대 페이지 번호: ${maxPage}`);
+
+ if (nextPageNum > maxPage) {
+ console.log(`[AutoZzim] 다음 페이지 ${nextPageNum}이 최대 페이지 ${maxPage}를 초과함`);
+ return false;
+ }
+
+ // URL 직접 변경
+ const currentUrl = new URL(window.location.href);
+ currentUrl.searchParams.set('cp', nextPageNum.toString());
+
+ console.log(`[AutoZzim] URL 직접 변경: cp=${actualCurrentPage} → cp=${nextPageNum}`);
+ console.log('[AutoZzim] 다음 페이지 URL:', currentUrl.href);
+
+ window.location.href = currentUrl.href;
+ return true;
+
+ } catch (e) {
+ console.error('[AutoZzim] 다음 페이지 이동 오류:', e);
+ return false;
+ }
+}
+
+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;
+ }
+}
+
+// 페이지 로드 완료 감지 함수
+function waitForPageLoad() {
+ return new Promise((resolve) => {
+ // 1. DOM 로드 확인
+ if (document.readyState === 'complete') {
+ console.log('[AutoZzim] DOM 이미 로드 완료');
+ resolve();
+ return;
+ }
+
+ // 2. 페이지 로드 이벤트 리스너
+ const handleLoad = () => {
+ console.log('[AutoZzim] 페이지 로드 완료');
+ window.removeEventListener('load', handleLoad);
+ resolve();
+ };
+
+ window.addEventListener('load', handleLoad);
+
+ // 3. 최대 10초 대기
+ setTimeout(() => {
+ console.log('[AutoZzim] 페이지 로드 대기 시간 초과');
+ window.removeEventListener('load', handleLoad);
+ resolve();
+ }, 10000);
+ });
+}
+
+// 페이지 로드 시 URL 파라미터 확인 (개선된 버전)
+async function checkAutoZzimParam() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const autoZzim = urlParams.get('auto_zzim');
+ const maxZzim = parseInt(urlParams.get('max_zzim')) || 50;
+
+ console.log('[AutoZzim] URL 파라미터 확인:', {
+ autoZzim: autoZzim,
+ maxZzim: maxZzim,
+ currentUrl: window.location.href
+ });
+
+ if (autoZzim === 'true') {
+ console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작');
+
+ // 네이버 스마트스토어 URL 분석
+ const urlInfo = parseNaverSmartStoreUrl(window.location.href);
+
+ if (!urlInfo) {
+ console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href);
+ return;
+ }
+
+ if (!urlInfo.isProductListPage) {
+ console.log('[AutoZzim] 현재 페이지가 상품 목록 페이지가 아님, 올바른 페이지로 이동');
+
+ // 올바른 상품 목록 페이지 URL 생성
+ const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, true, maxZzim);
+
+ console.log('[AutoZzim] 올바른 상품 목록 페이지로 리다이렉트:', correctUrl);
+ window.location.href = correctUrl;
+ return;
+ }
+
+ // 페이지 완전 로드 대기
+ console.log('[AutoZzim] 페이지 로드 완료 대기 중...');
+ await waitForPageLoad();
+
+ // 추가 로드 대기 (네이버 스마트스토어 특화)
+ await new Promise(resolve => setTimeout(resolve, 3000));
+
+ // 상품 목록이 로드되었는지 확인
+ const productCheck = await waitForProductsToLoad();
+
+ if (productCheck) {
+ console.log('[AutoZzim] 상품 목록 로드 완료, 찜하기 시작');
+
+ // 찜하기 시작
+ startAutoZzim(maxZzim);
+ } else {
+ console.log('[AutoZzim] 상품 목록 로드 실패');
+ }
+ }
+}
+
+// 상품 목록 로드 대기 함수
+function waitForProductsToLoad() {
+ return new Promise((resolve) => {
+ let checkCount = 0;
+ const maxChecks = 20; // 최대 20번 확인 (20초)
+
+ const checkProducts = () => {
+ checkCount++;
+
+ // 상품 컨테이너 확인
+ const productContainers = document.querySelectorAll('[data-shp-contents-id], [class*="product"], [class*="item"]');
+ const hasProducts = productContainers.length > 0;
+
+ console.log(`[AutoZzim] 상품 로드 확인 ${checkCount}/${maxChecks}: ${productContainers.length}개 상품 발견`);
+
+ if (hasProducts) {
+ console.log('[AutoZzim] ✅ 상품 목록 로드 완료');
+ resolve(true);
+ return;
+ }
+
+ if (checkCount >= maxChecks) {
+ console.log('[AutoZzim] ❌ 상품 목록 로드 시간 초과');
+ resolve(false);
+ return;
+ }
+
+ // 1초 후 재확인
+ setTimeout(checkProducts, 1000);
+ };
+
+ // 즉시 첫 번째 확인
+ checkProducts();
+ });
+}
+
+// 페이지 로드 완료 시 자동 찜하기 파라미터 확인 (개선된 버전)
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ setTimeout(checkAutoZzimParam, 1000);
+ });
+} else {
+ setTimeout(checkAutoZzimParam, 1000);
+}
+
+// 추가: 페이지 변경 감지 (SPA 대응)
+let lastUrl = window.location.href;
+const urlChangeObserver = new MutationObserver(() => {
+ if (window.location.href !== lastUrl) {
+ lastUrl = window.location.href;
+ console.log('[AutoZzim] URL 변경 감지:', lastUrl);
+
+ // URL 변경 후 잠시 대기 후 파라미터 확인
+ setTimeout(checkAutoZzimParam, 2000);
+ }
+});
+
+// URL 변경 감지 시작
+urlChangeObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+});
+
+// popstate 이벤트도 감지 (뒤로가기/앞으로가기)
+window.addEventListener('popstate', () => {
+ console.log('[AutoZzim] popstate 이벤트 감지');
+ setTimeout(checkAutoZzimParam, 2000);
+});
+
+// 자동 찜하기 파라미터 확인
+ checkAutoZzimParam();
+
+// 디버깅용 전역 함수들 추가
+window.debugZzimButtons = function() {
+ console.log('=== 네이버 스마트스토어 찜 버튼 디버깅 ===');
+
+ // 정확한 선택자로 찜 버튼 찾기
+ const zzimButtonSelector = 'div#CategoryProducts button.zzim_button';
+ const allZzimButtons = document.querySelectorAll(zzimButtonSelector);
+ console.log(`전체 찜 버튼: ${allZzimButtons.length}개`);
+
+ // 찜 가능한 버튼 (aria-pressed="false")
+ const availableZzimButtons = document.querySelectorAll('div#CategoryProducts button.zzim_button[aria-pressed="false"]');
+ console.log(`찜 가능한 버튼: ${availableZzimButtons.length}개`);
+
+ // 이미 찜한 버튼 (aria-pressed="true")
+ const zzimmedButtons = document.querySelectorAll('div#CategoryProducts button.zzim_button[aria-pressed="true"]');
+ console.log(`이미 찜한 버튼: ${zzimmedButtons.length}개`);
+
+ // 각 버튼 상세 정보 출력
+ console.log('--- 찜 가능한 버튼 상세 정보 ---');
+ availableZzimButtons.forEach((btn, index) => {
+ const rect = btn.getBoundingClientRect();
+ console.log(`[${index + 1}] 찜 가능한 버튼:`, {
+ text: btn.textContent?.trim().substring(0, 30),
+ className: btn.className,
+ ariaPressed: btn.getAttribute('aria-pressed'),
+ isVisible: rect.width > 0 && rect.height > 0,
+ position: { x: Math.round(rect.x), y: Math.round(rect.y) },
+ size: { width: Math.round(rect.width), height: Math.round(rect.height) },
+ disabled: btn.disabled,
+ element: btn
+ });
+ });
+
+ console.log('--- 이미 찜한 버튼 상세 정보 ---');
+ zzimmedButtons.forEach((btn, index) => {
+ const rect = btn.getBoundingClientRect();
+ console.log(`[${index + 1}] 이미 찜한 버튼:`, {
+ text: btn.textContent?.trim().substring(0, 30),
+ className: btn.className,
+ ariaPressed: btn.getAttribute('aria-pressed'),
+ isVisible: rect.width > 0 && rect.height > 0,
+ position: { x: Math.round(rect.x), y: Math.round(rect.y) },
+ element: btn
+ });
+ });
+
+ // 상품 컨테이너 확인
+ const categoryProducts = document.getElementById('CategoryProducts');
+ if (categoryProducts) {
+ console.log('CategoryProducts 컨테이너 발견:', categoryProducts);
+ } else {
+ console.log('❌ CategoryProducts 컨테이너를 찾을 수 없음');
+ }
+
+ return {
+ totalZzimButtons: allZzimButtons.length,
+ availableZzimButtons: availableZzimButtons.length,
+ zzimmedButtons: zzimmedButtons.length,
+ availableButtons: Array.from(availableZzimButtons),
+ zzimmedButtonsArray: Array.from(zzimmedButtons)
+ };
+};
+
+window.findCurrentZzimButtons = function() {
+ console.log('=== 현재 페이지 찜 버튼 찾기 ===');
+
+ const buttons = findZzimButtons();
+ console.log(`찾은 찜 가능한 버튼: ${buttons.length}개`);
+
+ buttons.forEach((btn, index) => {
+ console.log(`[${index}]`, {
+ text: btn.textContent?.trim().substring(0, 30),
+ className: btn.className,
+ ariaPressed: btn.getAttribute('aria-pressed'),
+ element: btn
+ });
+ });
+
+ return buttons;
+};
+
+window.testZzimClick = function(buttonIndex = 0) {
+ console.log('=== 찜 버튼 클릭 테스트 ===');
+
+ const buttons = findZzimButtons();
+ if (buttons.length === 0) {
+ console.log('❌ 클릭할 찜 버튼이 없습니다.');
+ return;
+ }
+
+ if (buttonIndex >= buttons.length) {
+ console.log(`❌ 인덱스 ${buttonIndex}는 범위를 벗어났습니다. (최대: ${buttons.length - 1})`);
+ return;
+ }
+
+ const button = buttons[buttonIndex];
+ console.log(`${buttonIndex}번 버튼 클릭 테스트:`, {
+ text: button.textContent?.trim(),
+ className: button.className,
+ ariaPressed: button.getAttribute('aria-pressed')
+ });
+
+ // 전역 클릭 함수 사용
+ clickZzimButton(button).then(success => {
+ console.log('클릭 결과:', success ? '✅ 성공' : '❌ 실패');
+
+ // 클릭 후 상태 확인
+ setTimeout(() => {
+ const afterPressed = button.getAttribute('aria-pressed');
+ console.log('클릭 후 aria-pressed:', afterPressed);
+ }, 1000);
+ });
+};
+
+// 찜 버튼 상태 실시간 모니터링
+window.monitorZzimButtons = function(duration = 10000) {
+ console.log(`=== 찜 버튼 상태 모니터링 (${duration/1000}초) ===`);
+
+ const startTime = Date.now();
+ const interval = setInterval(() => {
+ const debug = window.debugZzimButtons();
+ console.log(`[${new Date().toLocaleTimeString()}] 찜 가능: ${debug.availableZzimButtons}개, 찜 완료: ${debug.zzimmedButtons}개`);
+
+ if (Date.now() - startTime >= duration) {
+ clearInterval(interval);
+ console.log('=== 모니터링 종료 ===');
+ }
+ }, 2000);
+
+ return interval;
+};
+
+// 자동 찜하기 상태 확인 함수
+window.checkAutoZzimStatus = function() {
+ console.log('=== 자동 찜하기 상태 확인 ===');
+ console.log('현재 상태:', {
+ isRunning: autoZzimState.isRunning,
+ actualZzimCount: autoZzimState.actualZzimCount,
+ maxZzim: autoZzimState.maxZzim,
+ currentPage: autoZzimState.currentPage,
+ delay: autoZzimState.delay,
+ hasStatusDiv: !!autoZzimState.statusDiv,
+ startTime: autoZzimState.startTime ? new Date(autoZzimState.startTime).toLocaleTimeString() : null
+ });
+
+ return autoZzimState;
+};
+
+// 자동 찜하기 강제 중단 함수
+window.stopAutoZzim = function() {
+ console.log('=== 자동 찜하기 강제 중단 ===');
+
+ if (autoZzimState.isRunning) {
+ autoZzimState.isRunning = false;
+ console.log('✅ 자동 찜하기가 중단되었습니다.');
+
+ if (autoZzimState.statusDiv) {
+ updateZzimStatus(autoZzimState.statusDiv, `수동 중단됨 (${autoZzimState.actualZzimCount}개 찜함)`);
+
+ // 3초 후 상태 UI 제거
+ setTimeout(() => {
+ if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
+ autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
+ autoZzimState.statusDiv = null;
+ }
+ }, 3000);
+ }
+ } else {
+ console.log('❌ 현재 실행 중인 자동 찜하기가 없습니다.');
+ }
+};
+
+// 수동 찜하기 테스트 함수
+window.testManualZzim = function(maxCount = 5) {
+ console.log(`=== 수동 찜하기 테스트 (최대 ${maxCount}개) ===`);
+
+ if (autoZzimState.isRunning) {
+ console.log('❌ 자동 찜하기가 실행 중입니다. 먼저 중단해주세요.');
+ return;
+ }
+
+ const buttons = findZzimButtons();
+ if (buttons.length === 0) {
+ console.log('❌ 찜할 수 있는 버튼이 없습니다.');
+ return;
+ }
+
+ console.log(`찾은 찜 버튼: ${buttons.length}개`);
+
+ let count = 0;
+ const testCount = Math.min(maxCount, buttons.length);
+
+ const testNext = async () => {
+ if (count >= testCount) {
+ console.log(`✅ 수동 찜하기 테스트 완료: ${count}개 시도`);
+ return;
+ }
+
+ const button = buttons[count];
+ console.log(`[${count + 1}/${testCount}] 찜 버튼 클릭 테스트:`, {
+ text: button.textContent?.trim().substring(0, 30),
+ className: button.className.substring(0, 50)
+ });
+
+ const success = await clickZzimButton(button);
+ console.log(`결과: ${success ? '✅ 성공' : '❌ 실패'}`);
+
+ count++;
+
+ // 1초 대기 후 다음 버튼
+ setTimeout(testNext, 1000);
+ };
+
+ testNext();
+};
+
+// 페이지 로드 시 자동 디버깅 (개발 중에만)
+if (window.location.href.includes('smartstore.naver.com') && window.location.search.includes('debug=true')) {
+ setTimeout(() => {
+ console.log('🔍 디버그 모드: 자동 찜 버튼 분석 시작');
+ window.debugZzimButtons();
+ window.debugPagination();
+ }, 2000);
+}
+
+// 네이버 스마트스토어 URL 구조 분석 함수
+function parseNaverSmartStoreUrl(url) {
+ try {
+ const urlObj = new URL(url);
+
+ // 네이버 스마트스토어 도메인 확인
+ if (!urlObj.hostname.includes('smartstore.naver.com')) {
+ console.log('[AutoZzim] 네이버 스마트스토어 도메인이 아님:', urlObj.hostname);
+ return null;
+ }
+
+ // 경로 분석: /storename/... 형태
+ const pathParts = urlObj.pathname.split('/').filter(part => part);
+ console.log('[AutoZzim] 경로 분석:', pathParts);
+
+ if (pathParts.length === 0) {
+ console.log('[AutoZzim] 경로가 없음');
+ return null;
+ }
+
+ // 첫 번째 경로가 스토어명
+ const storeName = pathParts[0];
+ const currentSection = pathParts[1] || 'main';
+
+ // 상품 목록 페이지인지 확인
+ const productListSections = ['category', 'best', 'new', 'sale'];
+ const isProductListPage = productListSections.includes(currentSection);
+
+ console.log('[AutoZzim] URL 분석 결과:', {
+ storeName: storeName,
+ currentSection: currentSection,
+ isProductListPage: isProductListPage,
+ fullPath: urlObj.pathname
+ });
+
+ return {
+ storeName: storeName,
+ currentSection: currentSection,
+ isProductListPage: isProductListPage,
+ origin: urlObj.origin,
+ searchParams: urlObj.searchParams
+ };
+
+ } catch (error) {
+ console.error('[AutoZzim] URL 분석 오류:', error);
+ return null;
+ }
+}
+
+// 올바른 상품 목록 페이지 URL 생성 함수
+function generateCorrectProductListUrl(storeName, origin, autoZzim = false, maxZzim = 50) {
+ const baseUrl = `${origin}/${storeName}/category/ALL`;
+ const params = new URLSearchParams();
+ params.set('cp', '1');
+
+ if (autoZzim) {
+ params.set('auto_zzim', 'true');
+ params.set('max_zzim', maxZzim.toString());
+ }
+
+ return `${baseUrl}?${params.toString()}`;
+}
+
+// 키보드 이벤트 리스너 추가
+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+Z: 한중번역
+ 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+K: 직번역
+ 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();
+
+// iframe 환경에서도 이벤트 리스너 설정
+if (isIframeEnvironment) {
+ addKeyboardListeners(window.parent.document);
+}
+
+// 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
+});
+
+// 클릭 이벤트 모니터링 함수 추가
+window.monitorClicks = function(duration = 30000) {
+ console.log(`=== 클릭 이벤트 모니터링 시작 (${duration/1000}초) ===`);
+
+ const startTime = Date.now();
+
+ const clickHandler = (e) => {
+ const target = e.target;
+ const currentTime = Date.now();
+
+ if (currentTime - startTime > duration) {
+ document.removeEventListener('click', clickHandler, true);
+ console.log('=== 클릭 이벤트 모니터링 종료 ===');
+ return;
+ }
+
+ console.log('[클릭 모니터링] 클릭 이벤트 감지:', {
+ tagName: target.tagName,
+ className: target.className,
+ id: target.id,
+ textContent: target.textContent?.trim().substring(0, 50),
+ ariaLabel: target.getAttribute('aria-label'),
+ dataTestId: target.getAttribute('data-testid'),
+ isZzimButton: target.className.includes('zzim_button'),
+ ariaPressed: target.getAttribute('aria-pressed'),
+ position: {
+ x: e.clientX,
+ y: e.clientY
+ },
+ timestamp: new Date().toLocaleTimeString()
+ });
+
+ // 판매자 정보나 팝업 관련 클릭 감지
+ const isSellerRelated = target.textContent?.includes('판매자') ||
+ target.textContent?.includes('상점') ||
+ target.textContent?.includes('업체') ||
+ target.className.includes('seller') ||
+ target.className.includes('shop');
+
+ if (isSellerRelated) {
+ console.warn('[클릭 모니터링] ⚠️ 판매자 관련 요소 클릭 감지!', {
+ element: target,
+ text: target.textContent?.trim()
+ });
+ }
+
+ // 팝업이나 모달 관련 클릭 감지
+ const isPopupRelated = target.className.includes('popup') ||
+ target.className.includes('modal') ||
+ target.className.includes('dialog') ||
+ target.closest('.popup') ||
+ target.closest('.modal') ||
+ target.closest('.dialog');
+
+ if (isPopupRelated) {
+ console.warn('[클릭 모니터링] ⚠️ 팝업/모달 관련 요소 클릭 감지!', {
+ element: target,
+ text: target.textContent?.trim()
+ });
+ }
+ };
+
+ document.addEventListener('click', clickHandler, true);
+
+ return () => {
+ document.removeEventListener('click', clickHandler, true);
+ console.log('=== 클릭 이벤트 모니터링 수동 종료 ===');
+ };
+};
+
+// 요소 겹침 확인 함수
+window.checkElementOverlap = function(selector = 'div#CategoryProducts button.zzim_button') {
+ console.log(`=== 요소 겹침 확인: ${selector} ===`);
+
+ const elements = document.querySelectorAll(selector);
+ console.log(`찾은 요소: ${elements.length}개`);
+
+ elements.forEach((element, index) => {
+ const rect = element.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ const elementAtPoint = document.elementFromPoint(centerX, centerY);
+ const isCorrect = elementAtPoint === element || element.contains(elementAtPoint);
+
+ console.log(`[${index + 1}] 요소 겹침 확인:`, {
+ element: element,
+ text: element.textContent?.trim().substring(0, 30),
+ className: element.className,
+ rect: {
+ x: Math.round(rect.x),
+ y: Math.round(rect.y),
+ width: Math.round(rect.width),
+ height: Math.round(rect.height),
+ centerX: Math.round(centerX),
+ centerY: Math.round(centerY)
+ },
+ elementAtPoint: elementAtPoint,
+ elementAtPointTag: elementAtPoint?.tagName,
+ elementAtPointClass: elementAtPoint?.className,
+ elementAtPointText: elementAtPoint?.textContent?.trim().substring(0, 30),
+ isCorrect: isCorrect,
+ warning: !isCorrect ? '⚠️ 다른 요소가 겹쳐있음!' : '✅ 정상'
+ });
+ });
+
+ return elements;
+};
+
+// 안전한 찜하기 테스트 함수
+window.testSafeZzimClick = function(buttonIndex = 0) {
+ console.log('=== 안전한 찜 버튼 클릭 테스트 ===');
+
+ // 클릭 모니터링 시작
+ const stopMonitoring = window.monitorClicks(10000);
+
+ const buttons = findZzimButtons();
+ if (buttons.length === 0) {
+ console.log('❌ 클릭할 찜 버튼이 없습니다.');
+ stopMonitoring();
+ return;
+ }
+
+ if (buttonIndex >= buttons.length) {
+ console.log(`❌ 인덱스 ${buttonIndex}는 범위를 벗어났습니다. (최대: ${buttons.length - 1})`);
+ stopMonitoring();
+ return;
+ }
+
+ const button = buttons[buttonIndex];
+ console.log(`${buttonIndex}번 버튼 안전 클릭 테스트:`, {
+ text: button.textContent?.trim(),
+ className: button.className,
+ ariaPressed: button.getAttribute('aria-pressed')
+ });
+
+ // 요소 겹침 확인
+ const rect = button.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+ const elementAtPoint = document.elementFromPoint(centerX, centerY);
+
+ console.log('클릭 전 요소 겹침 확인:', {
+ targetButton: button,
+ elementAtPoint: elementAtPoint,
+ isCorrect: elementAtPoint === button || button.contains(elementAtPoint)
+ });
+
+ // 전역 클릭 함수 사용
+ clickZzimButton(button).then(success => {
+ console.log('클릭 결과:', success ? '✅ 성공' : '❌ 실패');
+
+ setTimeout(() => {
+ stopMonitoring();
+ const afterPressed = button.getAttribute('aria-pressed');
+ console.log('클릭 후 aria-pressed:', afterPressed);
+ }, 2000);
+ });
+};
+
diff --git a/wrmc_ext/manifest.json b/wrmc_ext/manifest.json
index a7e63c0..7b88985 100644
--- a/wrmc_ext/manifest.json
+++ b/wrmc_ext/manifest.json
@@ -11,15 +11,16 @@
"notifications",
"alarms",
"tabs",
- "scripting"
+ "scripting",
+ "clipboardRead"
],
"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/*",
+ "https://market.m.taobao.com/*",
"*://openapi.naver.com/*",
"*://api-free.deepl.com/*",
"*://api.deepl.com/*",
@@ -33,6 +34,7 @@
{
"matches": [""],
"js": ["content.js"],
+ "all_frames": true,
"run_at": "document_end"
}
],
@@ -59,6 +61,12 @@
"default": "Ctrl+Shift+Z"
},
"description": "한국어↔중국어 양방향 번역"
+ },
+ "direct-translate": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+K"
+ },
+ "description": "직번역 (텍스트 바로 대체)"
}
},
"web_accessible_resources": [
diff --git a/wrmc_ext/manual.html b/wrmc_ext/manual.html
index 607b0b7..334bf28 100644
--- a/wrmc_ext/manual.html
+++ b/wrmc_ext/manual.html
@@ -330,7 +330,7 @@
- 번역할 문장이나 단어를 드래그 후 단축키(컨트롤+쉬프트+Z) 입력
- 웹페이지에서 바로 한글과 중국어를 빠르게 번역
- - 위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역
+ - 위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역[현재 위챗의 특수성으로 적용되지않음]
- 웹페이지에서 중국어를 드래그 후 단축키로 바로 번역 결과 확인
@@ -343,6 +343,7 @@
휴식시간 추천 활동
실시간 타이머 알림
개인화된 시간 설정
+ [포모도로 타이머가 활성화 되면 35분/5분으로 기본설정되며 4사이클 총180분(3시간) 휴식시간 추천 활동 제공]
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/settings.html b/wrmc_ext/settings.html
index f801582..32eb3e0 100644
--- a/wrmc_ext/settings.html
+++ b/wrmc_ext/settings.html
@@ -542,21 +542,29 @@
알람 활성화/비활성화
+
+
+
-
+
분 (작업 후 휴식 알림)
-
+
분 (휴식 시간 길이)
-
+
+
diff --git a/wrmc_ext/settings.js b/wrmc_ext/settings.js
index 8a7288a..28f81cf 100644
--- a/wrmc_ext/settings.js
+++ b/wrmc_ext/settings.js
@@ -236,7 +236,9 @@ class SettingsManager {
enabled: true,
workTime: 60, // 분
restTime: 5, // 분
- autoZzim: false
+ autoZzim: false,
+ pomodoro: false,
+ cycle: 0
};
console.log('시간 알람 설정 로드 완료:', this.timeAlarmSettings);
} catch (error) {
@@ -245,7 +247,9 @@ class SettingsManager {
enabled: true,
workTime: 60,
restTime: 5,
- autoZzim: false
+ autoZzim: false,
+ pomodoro: false,
+ cycle: 0
};
}
}
@@ -321,6 +325,7 @@ class SettingsManager {
const workTimeInput = document.getElementById('workTimeInput');
const restTimeInput = document.getElementById('restTimeInput');
const autoZzimCheckbox = document.getElementById('autoZzimCheckbox');
+ const pomodoroToggle = document.getElementById('pomodoroToggle');
if (workTimeInput) {
workTimeInput.addEventListener('change', (e) => {
@@ -339,6 +344,18 @@ class SettingsManager {
this.timeAlarmSettings.autoZzim = e.target.checked;
});
}
+
+ if (pomodoroToggle) {
+ pomodoroToggle.addEventListener('click', () => {
+ this.timeAlarmSettings.pomodoro = !this.timeAlarmSettings.pomodoro;
+ // 포모도로가 활성화되면 고정 값 적용
+ if (this.timeAlarmSettings.pomodoro) {
+ this.timeAlarmSettings.workTime = 35;
+ this.timeAlarmSettings.restTime = 5;
+ }
+ this.updateTimeAlarmUI();
+ });
+ }
}
updateUI() {
@@ -388,12 +405,23 @@ class SettingsManager {
}
}
+ const pomodoroToggle = document.getElementById('pomodoroToggle');
+ if (pomodoroToggle) {
+ if (this.timeAlarmSettings.pomodoro) {
+ pomodoroToggle.classList.add('active');
+ } else {
+ pomodoroToggle.classList.remove('active');
+ }
+ }
+
if (workTimeInput) {
workTimeInput.value = this.timeAlarmSettings.workTime;
+ workTimeInput.disabled = this.timeAlarmSettings.pomodoro;
}
if (restTimeInput) {
restTimeInput.value = this.timeAlarmSettings.restTime;
+ restTimeInput.disabled = this.timeAlarmSettings.pomodoro;
}
if (autoZzimCheckbox) {
@@ -412,6 +440,12 @@ class SettingsManager {
this.showLoading(true);
// 시간 알람 설정 업데이트
+ if (this.timeAlarmSettings.pomodoro) {
+ // 강제 고정 값
+ this.timeAlarmSettings.workTime = 35;
+ this.timeAlarmSettings.restTime = 5;
+ }
+
const workTimeInput = document.getElementById('workTimeInput');
const restTimeInput = document.getElementById('restTimeInput');
const autoZzimCheckbox = document.getElementById('autoZzimCheckbox');
@@ -484,10 +518,12 @@ class SettingsManager {
// 시간 알람 설정 초기화
this.timeAlarmSettings = {
- enabled: false,
+ enabled: true,
workTime: 60,
restTime: 5,
- autoZzim: false
+ autoZzim: false,
+ pomodoro: false,
+ cycle: 0
};
// 저장소에서 시간 알람 설정 제거
diff --git a/wrmc_ext/zzim.html b/wrmc_ext/zzim.html
index e9d39ae..eb1e835 100644
--- a/wrmc_ext/zzim.html
+++ b/wrmc_ext/zzim.html
@@ -140,45 +140,168 @@
}
.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-checkboxes {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ }
+
+ .market-checkbox-input {
+ width: 18px;
+ height: 18px;
+ accent-color: #667eea;
+ cursor: pointer;
+ }
+
+ .market-checkbox-label {
+ font-size: 12px;
+ 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 +345,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 +642,65 @@
- 🎯 찜하기 작업
+ 💝 찜하기 작업
+
+
+
+
+
+ 체크하면 새 탭에서 백그라운드로 찜하기를 실행합니다.
+
+
+
+
+
+
⚙️ 찜하기 옵션
+
+
+
+
+
+
+ 기본 간격:
+
+ ms
+
+
+ + 추가 간격:
+
+ ms
+
+
+ = 총 간격:
+ 1000ms
+
+
+
+ 💡 찜과 찜 사이의 대기 시간을 설정합니다. (1초~10초)
+
+
+
+
+
+
+
+ 체크하면 전체상품(/category/ALL) 대신 최신상품(/best)부터 찜합니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
체크하면 내 마켓 찜하기 작업 시 이 마켓이 포함됩니다.
+
+
+
+
+
+
+
+
체크하면 다른 사람들이 이 마켓에 품앗이 찜을 할 수 있습니다.
+
+
+
+
+
+
+
+
체크하면 품앗이 찜하기에서 다른 사람들이 이 마켓을 찜할 수 있습니다.
+
+
+
+
+
+