From debe58a57502a6d1e2fdc24fd868d2943b1e8933 Mon Sep 17 00:00:00 2001 From: 9700X_PC <9700X_PC@gmail.com> Date: Thu, 27 Nov 2025 15:45:31 +0900 Subject: [PATCH] Enhance background.js and content.js with new auto mutual zzim functionality, improve text selection detection, and update context menu options. Adjust alarm settings for new sayings detection and implement advanced error handling in zzim operations. --- wrmc_ext/background.js | 471 ++++++++++++- wrmc_ext/content.js | 1478 +++++++++++++++++++++++++++++++++++++++- wrmc_ext/zzim.js | 2 +- 3 files changed, 1939 insertions(+), 12 deletions(-) diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js index 304a58e..857ff55 100644 --- a/wrmc_ext/background.js +++ b/wrmc_ext/background.js @@ -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() }); @@ -3245,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 a7551b0..5796138 100644 --- a/wrmc_ext/content.js +++ b/wrmc_ext/content.js @@ -13,11 +13,385 @@ 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은 접근 불가 (로그 생략) + } + } + + // 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") { @@ -154,7 +528,10 @@ function setupIframeEventListeners() { addKeyboardListeners(iframeDoc); } catch (e) { - console.log('[content.js] iframe 이벤트 설정 실패:', e.message); + // SecurityError는 무시 (로그 출력 안함) + if (!e.message.includes('SecurityError') && !e.message.includes('Blocked a frame')) { + console.log('[content.js] iframe 이벤트 설정 실패:', e.message); + } } }); } @@ -1038,9 +1415,1092 @@ 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 +}; + +// 찜하기 버튼 찾기 함수 (전역) - Python 로직 기반 단순화 +function findZzimButtons() { + // Python: self.page.locator("div#CategoryProducts .zzim_button[type='button']") + const selector = "div#CategoryProducts .zzim_button[type='button']"; + const buttons = document.querySelectorAll(selector); + return Array.from(buttons); +} + +// 이미 찜한 상품인지 확인 함수 (전역) - 단순화 +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; + } + } + +// 찜하기 버튼 클릭 함수 (전역) - Python 로직 기반 단순화 +async function clickZzimButton(button) { + try { + const beforePressed = button.getAttribute('aria-pressed'); + if (beforePressed === 'true' || button.disabled) { + return false; + } + console.log('[AutoZzim] 찜 버튼 클릭 시도'); + button.click(); + + return new Promise((resolve) => { + let checks = 0; + const interval = setInterval(() => { + const afterPressed = button.getAttribute('aria-pressed'); + if (afterPressed === 'true') { + clearInterval(interval); + console.log('[AutoZzim] 찜하기 성공'); + if (typeof autoZzimState !== 'undefined') autoZzimState.actualZzimCount++; + resolve(true); + } else if (checks++ > 20) { // 2초 대기 + clearInterval(interval); + // 실패해도 에러 아님, 단순히 false 반환 + resolve(button.getAttribute('aria-pressed') === 'true'); + } + }, 100); + }); + } catch (error) { + console.error('[AutoZzim] 클릭 중 오류:', error); + return false; + } +} + +// 확인 모달/팝업 처리 함수 (전역) - Python 로직 기반 단순화 +function handleConfirmationModals() { + // Python 코드에서는 모달 처리 로직이 없으므로 빈 함수로 둡니다. + // 필요하다면 여기에 팝업 닫기 로직을 추가할 수 있습니다. +} + +// 현재 페이지 찜하기 처리 함수 (전역) - 개선된 버전 + 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; + } + +// 모든 상품 로딩을 위한 스크롤 함수 (Lazy Loading 대응) +async function scrollToLoadAllProducts() { + console.log('[AutoZzim] 상품 로딩을 위해 스크롤 실행'); + updateZzimStatus(autoZzimState.statusDiv, '상품 로딩 중... (스크롤)'); + + // 현재 높이 + let lastHeight = document.body.scrollHeight; + let currentPos = 0; + const step = window.innerHeight; + + // 끝까지 스크롤 + while (true) { + currentPos += step; + window.scrollTo(0, currentPos); + await new Promise(r => setTimeout(r, 200)); // 0.2초 대기 + + if (currentPos >= document.body.scrollHeight) { + // 끝에 도달했으면 잠시 대기 후 높이 변화 확인 + await new Promise(r => setTimeout(r, 1000)); + + // 높이가 늘어났으면 계속 진행, 아니면 종료 + if (document.body.scrollHeight <= lastHeight + 100) { // 약간의 오차 허용 + break; + } + lastHeight = document.body.scrollHeight; + } + } + + // 맨 위로 복귀 (찜하기는 위에서부터 순차적으로) + window.scrollTo(0, 0); + await new Promise(r => setTimeout(r, 500)); + console.log('[AutoZzim] 스크롤 완료'); +} + +// 자동 찜하기 메인 루프 (Python 로직 이식) +async function autoZzimMainLoop() { + console.log('[AutoZzim] 메인 루프 시작 (Python Logic)'); + if (!autoZzimState.isRunning) return; + + while (autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim) { + // 0. 스크롤하여 모든 상품 로드 (Lazy Loading 대응) + await scrollToLoadAllProducts(); + + // 1. 현재 페이지의 버튼 찾기 + const buttons = findZzimButtons(); + console.log(`[AutoZzim] 현재 페이지 발견 버튼: ${buttons.length}개`); + + // 2. 버튼 순회하며 클릭 + for (const button of buttons) { + if (!autoZzimState.isRunning || autoZzimState.actualZzimCount >= autoZzimState.maxZzim) break; + + const success = await clickZzimButton(button); + if (success) { + updateZzimStatus(autoZzimState.statusDiv, `찜하기 진행 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`); + + // Random delay: 1000 + random(0~1000) + const waitTime = autoZzimState.delay + Math.random() * 1000; + console.log(`[AutoZzim] 대기: ${Math.round(waitTime)}ms`); + await new Promise(r => setTimeout(r, waitTime)); + } + } + + // 3. 목표 달성 확인 + if (autoZzimState.actualZzimCount >= autoZzimState.maxZzim) break; + + // 4. 다음 페이지 이동 + const moved = await goToNextPage(); + if (!moved) { + console.log('[AutoZzim] 더 이상 페이지가 없거나 이동 실패'); + break; + } + + // 페이지 로드 후 추가 대기 (Python: time.sleep(2)) + await new Promise(r => setTimeout(r, 2000)); + } + + console.log('[AutoZzim] 작업 완료'); + updateZzimStatus(autoZzimState.statusDiv, `완료! 총 ${autoZzimState.actualZzimCount}개 찜함`); + autoZzimState.isRunning = false; + + 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초 대기 +} + +// 다음 페이지 이동 (Python Logic) +async function goToNextPage() { + console.log('[AutoZzim] 다음 페이지 이동 시도'); + + // Python: self.page.locator("div#CategoryProducts div[data-shp-area-id='pgn']") + const paginationContainer = document.querySelector("div#CategoryProducts div[data-shp-area-id='pgn']"); + if (!paginationContainer) { + console.log('[AutoZzim] 페이지네이션 컨테이너를 찾을 수 없음'); + return false; + } + + // 현재 페이지 찾기 (aria-current='true') + const currentBtn = paginationContainer.querySelector("a[aria-current='true'][role='menuitem']"); + if (!currentBtn) { + console.log('[AutoZzim] 현재 페이지 버튼을 찾을 수 없음'); + return false; + } + + const currentPage = parseInt(currentBtn.textContent.trim()); + if (isNaN(currentPage)) return false; + + const nextPageNum = currentPage + 1; + console.log(`[AutoZzim] 현재 페이지: ${currentPage}, 다음 페이지: ${nextPageNum}`); + + // 다음 페이지 버튼 찾기 (숫자 버튼 또는 '다음') + const menuItems = Array.from(paginationContainer.querySelectorAll("[role='menuitem']")); + + // 1. 숫자 버튼 찾기 + let nextBtn = menuItems.find(el => el.textContent.trim() === nextPageNum.toString()); + + // 2. 없으면 '다음' 버튼 찾기 + if (!nextBtn) { + nextBtn = menuItems.find(el => el.textContent.trim().includes('다음')); + } + + if (nextBtn) { + console.log('[AutoZzim] 다음 페이지 버튼 클릭'); + nextBtn.click(); + // Python: time.sleep(3) + await new Promise(r => setTimeout(r, 3000)); + + // 페이지 번호 업데이트 + autoZzimState.currentPage = nextPageNum; + return true; + } + + console.log('[AutoZzim] 다음 페이지 버튼을 찾을 수 없음'); + 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) { + try { + addKeyboardListeners(window.parent.document); + } catch (e) { + // Cross-origin parent 접근 실패 시 무시 + } +} + +// 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/zzim.js b/wrmc_ext/zzim.js index 455ddef..b0024db 100644 --- a/wrmc_ext/zzim.js +++ b/wrmc_ext/zzim.js @@ -1947,7 +1947,7 @@ class ZzimManager { 'Content-Type': 'application/json' } }); - + let myAvailableMileage = 0; if (myStatsResponse.ok) { const myData = await myStatsResponse.json();