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();