wrmc_ext/wrmc_ext/content.js

3001 lines
106 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// content.js
let lastContextMenuPos = null;
let tooltipEl = null;
let currentKeyword = null; // 현재 검색 키워드 저장
let loadingIndicator = null; // 로딩 인디케이터 요소
let isIframeEnvironment = false; // iframe 환경 감지
let customContextMenu = null; // 커스텀 컨텍스트 메뉴
// 마우스 위치 추적
let currentMousePos = { x: 0, y: 0 };
document.addEventListener('mousemove', (e) => {
currentMousePos = { x: e.pageX, y: e.pageY };
});
// iframe 환경 감지
function detectEnvironment() {
try {
// 1. iframe 내부인지 확인
isIframeEnvironment = window.self !== window.top;
// 2. 특별한 컨테이너 확인
const specialContainers = [
'.ice-container',
'[class*="aliwangwang"]',
'[class*="chat-container"]',
'[class*="message-container"]',
'[data-testid*="chat"]',
'[role="application"]',
'webview',
'embed'
];
const hasSpecialContainer = specialContainers.some(selector => {
return document.querySelector(selector) !== null;
});
console.log('[content.js] 환경 감지:', {
isIframe: isIframeEnvironment,
hasSpecialContainer: hasSpecialContainer,
userAgent: navigator.userAgent.includes('AliWangWang') ? 'AliWangWang' : 'Other'
});
return isIframeEnvironment || hasSpecialContainer;
} catch (e) {
console.log('[content.js] 환경 감지 오류:', e);
return false;
}
}
// 향상된 텍스트 선택 감지
function getSelectedTextAdvanced() {
let selectedText = '';
try {
// 1. 기본 window.getSelection() 확인
const selection = window.getSelection();
if (selection && selection.toString().trim()) {
selectedText = selection.toString().trim();
console.log('[content.js] 기본 선택에서 감지:', selectedText);
return selectedText;
}
// 2. document.getSelection() 확인
if (document.getSelection) {
const docSelection = document.getSelection();
if (docSelection && docSelection.toString().trim()) {
selectedText = docSelection.toString().trim();
console.log('[content.js] document 선택에서 감지:', selectedText);
return selectedText;
}
}
// 3. 활성 요소에서 선택된 텍스트 확인
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
const start = activeElement.selectionStart;
const end = activeElement.selectionEnd;
if (start !== end && start !== null && end !== null) {
selectedText = activeElement.value.substring(start, end).trim();
console.log('[content.js] 입력 요소에서 감지:', selectedText);
return selectedText;
}
}
// 4. contentEditable 요소 확인
const editableElements = document.querySelectorAll('[contenteditable="true"]');
for (const element of editableElements) {
try {
const elementSelection = element.ownerDocument.getSelection();
if (elementSelection && elementSelection.toString().trim()) {
selectedText = elementSelection.toString().trim();
console.log('[content.js] contentEditable에서 감지:', selectedText);
return selectedText;
}
} catch (e) {
continue;
}
}
// 5. iframe 내부 확인
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const iframeSelection = iframeDoc.getSelection();
if (iframeSelection && iframeSelection.toString().trim()) {
selectedText = iframeSelection.toString().trim();
console.log('[content.js] iframe에서 감지:', selectedText);
return selectedText;
}
} catch (e) {
// 크로스 오리진 iframe은 접근 불가
console.log('[content.js] iframe 접근 제한:', e.message);
}
}
// 6. 특별한 컨테이너 내부 확인 (알리왕왕, ice-container 등)
const containers = document.querySelectorAll(
'.ice-container, [class*="container"], [class*="chat"], [class*="message"], ' +
'[class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], ' +
'[data-testid*="message"], [role="textbox"], [contenteditable], [class*="aliwangwang"], ' +
'[class*="input"], [class*="text"], webview, embed'
);
for (const container of containers) {
try {
const containerSelection = container.ownerDocument.getSelection();
if (containerSelection && containerSelection.toString().trim()) {
// 선택 범위가 해당 컨테이너 내부인지 확인
const range = containerSelection.getRangeAt(0);
if (range && (container.contains(range.commonAncestorContainer) ||
container.contains(range.startContainer) ||
container.contains(range.endContainer))) {
selectedText = containerSelection.toString().trim();
console.log('[content.js] 특별 컨테이너에서 감지:', selectedText);
return selectedText;
}
}
} catch (e) {
continue;
}
}
// 7. Shadow DOM 확인
function checkShadowDom(element) {
if (element.shadowRoot) {
try {
const shadowSelection = element.shadowRoot.getSelection();
if (shadowSelection && shadowSelection.toString().trim()) {
return shadowSelection.toString().trim();
}
} catch (e) {
// 무시
}
}
for (const child of element.children) {
const result = checkShadowDom(child);
if (result) return result;
}
return null;
}
const shadowResult = checkShadowDom(document.body);
if (shadowResult) {
console.log('[content.js] Shadow DOM에서 감지:', shadowResult);
return shadowResult;
}
console.log('[content.js] 선택된 텍스트를 찾을 수 없음');
return '';
} catch (e) {
console.error('[content.js] 텍스트 선택 감지 오류:', e);
return '';
}
}
// 커스텀 컨텍스트 메뉴 생성
function createCustomContextMenu(e) {
// 기존 커스텀 메뉴 제거
removeCustomContextMenu();
const selectedText = getSelectedTextAdvanced();
if (!selectedText) return;
customContextMenu = document.createElement('div');
customContextMenu.id = 'custom-context-menu';
customContextMenu.style.cssText = `
position: fixed;
z-index: 999999;
background: white;
border: 1px solid #ccc;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
font-family: 'Segoe UI', sans-serif;
font-size: 14px;
min-width: 200px;
padding: 4px 0;
`;
// 메뉴 항목들
const menuItems = [
{
text: '🔍 지재권검색(내차확장기)',
action: () => {
console.log('[CustomMenu] 지재권 검색 클릭:', selectedText);
chrome.runtime.sendMessage({
action: "searchTrademark",
keyword: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[CustomMenu] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[CustomMenu] 지재권 검색 메시지 전송 성공:', response);
}
});
removeCustomContextMenu();
}
},
{
text: '🌐 멀티번역하기(내차확장기)',
action: () => {
console.log('[CustomMenu] 멀티번역 클릭:', selectedText);
chrome.runtime.sendMessage({
action: "translateText",
text: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[CustomMenu] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[CustomMenu] 멀티번역 메시지 전송 성공:', response);
}
});
removeCustomContextMenu();
}
},
{
text: '⚡ 직번역(내차확장기)',
action: () => {
console.log('[CustomMenu] 직번역 클릭:', selectedText);
chrome.runtime.sendMessage({
action: "handleDirectTranslation",
selectedText: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[CustomMenu] 직번역 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[CustomMenu] 직번역 메시지 전송 성공:', response);
}
});
removeCustomContextMenu();
}
},
{
text: '📋 복사',
action: () => {
console.log('[CustomMenu] 복사 클릭:', selectedText);
navigator.clipboard.writeText(selectedText).then(() => {
console.log('[CustomMenu] 복사 성공');
// 복사 성공 알림 (선택사항)
showTemporaryMessage('복사되었습니다!');
}).catch(err => {
console.error('[CustomMenu] 복사 실패:', err);
// 폴백: execCommand 사용
try {
const textArea = document.createElement('textarea');
textArea.value = selectedText;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
console.log('[CustomMenu] 폴백 복사 성공');
showTemporaryMessage('복사되었습니다!');
} catch (fallbackErr) {
console.error('[CustomMenu] 폴백 복사도 실패:', fallbackErr);
}
});
removeCustomContextMenu();
}
}
];
menuItems.forEach(item => {
const menuItem = document.createElement('div');
menuItem.textContent = item.text;
menuItem.style.cssText = `
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
`;
menuItem.addEventListener('mouseenter', () => {
menuItem.style.backgroundColor = '#f0f0f0';
});
menuItem.addEventListener('mouseleave', () => {
menuItem.style.backgroundColor = 'transparent';
});
menuItem.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
console.log('[CustomMenu] 메뉴 항목 클릭:', item.text);
item.action();
});
customContextMenu.appendChild(menuItem);
});
document.body.appendChild(customContextMenu);
// 위치 설정
const rect = customContextMenu.getBoundingClientRect();
let x = e.clientX;
let y = e.clientY;
// 화면 경계 체크
if (x + rect.width > window.innerWidth) {
x = window.innerWidth - rect.width - 10;
}
if (y + rect.height > window.innerHeight) {
y = window.innerHeight - rect.height - 10;
}
customContextMenu.style.left = x + 'px';
customContextMenu.style.top = y + 'px';
// 외부 클릭시 메뉴 제거
setTimeout(() => {
const handleClickOutside = (event) => {
if (!customContextMenu.contains(event.target)) {
removeCustomContextMenu();
document.removeEventListener('click', handleClickOutside);
}
};
document.addEventListener('click', handleClickOutside);
}, 100);
console.log('[CustomMenu] 커스텀 컨텍스트 메뉴 생성 완료:', selectedText);
}
// 임시 메시지 표시 함수
function showTemporaryMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999999;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 20px;
border-radius: 6px;
font-family: 'Segoe UI', sans-serif;
font-size: 14px;
pointer-events: none;
`;
messageDiv.textContent = message;
document.body.appendChild(messageDiv);
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 1500);
}
// 커스텀 컨텍스트 메뉴 제거
function removeCustomContextMenu() {
if (customContextMenu) {
customContextMenu.remove();
customContextMenu = null;
}
}
// // 향상된 컨텍스트 메뉴 이벤트
// document.addEventListener("contextmenu", (e) => {
// lastContextMenuPos = { x: e.pageX, y: e.pageY };
// // 특별한 환경에서는 커스텀 컨텍스트 메뉴도 표시
// if (detectEnvironment()) {
// const selectedText = getSelectedTextAdvanced();
// if (selectedText) {
// // 기본 컨텍스트 메뉴를 막지 않고 추가로 커스텀 메뉴 표시
// setTimeout(() => createCustomContextMenu(e), 50);
// }
// }
// });
// 향상된 단축키 처리
document.addEventListener("keydown", (e) => {
// ESC 키 처리
if (e.key === "Escape") {
if (tooltipEl) removeTooltip();
if (loadingIndicator) removeLoadingIndicator();
if (customContextMenu) removeCustomContextMenu();
return;
}
// Ctrl+Shift+F: 지재권 검색
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText) {
console.log('[Shortcut] Ctrl+Shift+F - 지재권 검색:', selectedText);
chrome.runtime.sendMessage({
action: "searchTrademark",
keyword: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[Shortcut] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[Shortcut] 지재권 검색 메시지 전송 성공:', response);
}
});
} else {
console.log('[Shortcut] 선택된 텍스트가 없어 지재권 검색을 실행할 수 없습니다.');
showTemporaryMessage('텍스트를 선택해주세요');
}
return;
}
// Ctrl+Shift+T: 멀티번역
if (e.ctrlKey && e.shiftKey && e.key === 'E') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText) {
console.log('[Shortcut] Ctrl+Shift+T - 멀티번역:', selectedText);
chrome.runtime.sendMessage({
action: "translateText",
text: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[Shortcut] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[Shortcut] 멀티번역 메시지 전송 성공:', response);
}
});
} else {
console.log('[Shortcut] 선택된 텍스트가 없어 멀티번역을 실행할 수 없습니다.');
showTemporaryMessage('텍스트를 선택해주세요');
}
return;
}
// Ctrl+Shift+K: 한중 번역 (기존 기능 유지)
if (e.ctrlKey && e.shiftKey && e.key === 'K') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText) {
console.log('[Shortcut] Ctrl+Shift+K - 한중번역:', selectedText);
chrome.runtime.sendMessage({
action: "handleKoreanToChinese",
selectedText: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[Shortcut] 한중번역 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[Shortcut] 한중번역 메시지 전송 성공:', response);
}
});
} else {
console.log('[Shortcut] 선택된 텍스트가 없어 한중번역을 실행할 수 없습니다.');
showTemporaryMessage('텍스트를 선택해주세요');
}
return;
}
// Ctrl+Shift+Z: 직번역 (새로 추가)
if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText) {
console.log('[Shortcut] Ctrl+Shift+Z - 직번역:', selectedText);
chrome.runtime.sendMessage({
action: "handleDirectTranslation",
selectedText: selectedText
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[Shortcut] 직번역 메시지 전송 실패:', chrome.runtime.lastError);
} else {
console.log('[Shortcut] 직번역 메시지 전송 성공:', response);
}
});
} else {
console.log('[Shortcut] 선택된 텍스트가 없어 직번역을 실행할 수 없습니다.');
showTemporaryMessage('텍스트를 선택해주세요');
}
return;
}
}, true); // useCapture를 true로 설정하여 이벤트를 먼저 캐치
// iframe 내부에서도 이벤트 감지
function setupIframeEventListeners() {
const iframes = document.querySelectorAll('iframe');
iframes.forEach(iframe => {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// iframe 내부에서 컨텍스트 메뉴 이벤트
iframeDoc.addEventListener("contextmenu", (e) => {
lastContextMenuPos = {
x: e.pageX + iframe.offsetLeft,
y: e.pageY + iframe.offsetTop
};
const selectedText = getSelectedTextAdvanced();
if (selectedText) {
console.log('[iframe] 컨텍스트 메뉴에서 선택된 텍스트:', selectedText);
setTimeout(() => createCustomContextMenu(e), 50);
}
});
// iframe 내부에서 키보드 이벤트 리스너 추가
addKeyboardListeners(iframeDoc);
} catch (e) {
console.log('[content.js] iframe 이벤트 설정 실패:', e.message);
}
});
}
// 페이지 로드 시 iframe 이벤트 설정
setTimeout(setupIframeEventListeners, 1000);
// 로딩 인디케이터 생성 및 표시
function showLoadingIndicator(message, position = null) {
// 기존 로딩 인디케이터가 있으면 제거
if (loadingIndicator) {
removeLoadingIndicator();
}
loadingIndicator = document.createElement("div");
loadingIndicator.id = "markinfo-loading";
loadingIndicator.style.cssText = `
position: fixed;
z-index: 9999999;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 10px;
animation: fadeIn 0.3s ease-out;
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.2);
`;
// 스피너 아이콘
const spinner = document.createElement("div");
spinner.style.cssText = `
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
`;
// 메시지 텍스트
const messageEl = document.createElement("span");
messageEl.textContent = message;
loadingIndicator.appendChild(spinner);
loadingIndicator.appendChild(messageEl);
// CSS 애니메이션 정의
if (!document.getElementById('markinfo-loading-styles')) {
const style = document.createElement('style');
style.id = 'markinfo-loading-styles';
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
0% { opacity: 0; transform: translateY(-10px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-10px); }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(loadingIndicator);
// 위치 설정
const pos = position || getSelectionPosition() || currentMousePos;
positionLoadingIndicator(loadingIndicator, pos);
console.log('[content.js] 로딩 인디케이터 표시:', message);
}
// 로딩 인디케이터 제거
function removeLoadingIndicator() {
if (loadingIndicator) {
loadingIndicator.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
if (loadingIndicator && loadingIndicator.parentNode) {
loadingIndicator.parentNode.removeChild(loadingIndicator);
}
loadingIndicator = null;
}, 300);
console.log('[content.js] 로딩 인디케이터 제거');
}
}
// 선택된 텍스트의 위치 가져오기
function getSelectionPosition() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return {
x: rect.left + window.pageXOffset + rect.width / 2,
y: rect.top + window.pageYOffset - 10
};
}
}
return null;
}
// 로딩 인디케이터 위치 설정
function positionLoadingIndicator(indicator, pos) {
indicator.style.left = (pos.x - 50) + "px"; // 중앙 정렬을 위해 조정
indicator.style.top = (pos.y - 50) + "px";
// 화면 경계 체크
const rect = indicator.getBoundingClientRect();
const docWidth = document.documentElement.clientWidth;
const docHeight = document.documentElement.clientHeight;
if (rect.right > docWidth) {
indicator.style.left = (docWidth - rect.width - 10) + "px";
}
if (rect.left < 0) {
indicator.style.left = "10px";
}
if (rect.bottom > docHeight) {
indicator.style.top = (docHeight - rect.height - 10) + "px";
}
if (rect.top < 0) {
indicator.style.top = "10px";
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[content.js] 메시지 수신:', message);
// 핑 테스트 응답
if (message.action === "ping") {
console.log('[content.js] 핑 메시지 수신, 응답 전송');
sendResponse({ status: "ready" });
return true;
}
// 자동 찜하기 시작
if (message.action === "startAutoZzim") {
console.log('[AutoZzim] 자동 찜하기 시작:', message);
startAutoZzim(message.maxZzim || 50, message.delay || 1000);
sendResponse({ success: true });
return true;
}
// 로딩 인디케이터 표시 요청
if (message.action === "showLoading") {
showLoadingIndicator(message.message, message.position);
sendResponse({ success: true });
return true;
}
// 선택 영역 교체 요청
if (message.action === 'replaceSelectedText' && message.text) {
// 선택 영역이 있으면 해당 부분만 교체, 아니면 포커스된 엘리먼트 전체 교체
if (!replaceSelectedText(message.text)) {
// selection이 없다면, 포커스된 .edit 등 target element 전체 교체 시도
const activeElem = document.activeElement;
if (activeElem && activeElem.classList.contains('edit')) {
activeElem.innerText = message.text;
} else {
// 특정 요소를 찾고 싶으면 아래처럼 직접 지정
const target = document.querySelector('.edit[contenteditable="false"]');
if (target) {
target.innerText = message.text;
}
}
}
sendResponse({success: true});
return true;
}
// 로딩 인디케이터 제거 요청
if (message.action === "hideLoading") {
removeLoadingIndicator();
sendResponse({ success: true });
return true;
}
if (message.action === "showTooltip") {
console.log(`[content.js] showTooltip 메시지 수신, 키워드: ${message.keyword}, 결과 개수: ${message.detailInfo?.length || 0}`);
// 로딩 인디케이터 제거
removeLoadingIndicator();
// 현재 키워드 저장
currentKeyword = message.keyword;
try {
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.id = "markinfo-tooltip";
tooltipEl.style.position = "absolute";
tooltipEl.style.zIndex = "999999";
tooltipEl.style.background = "#fff";
tooltipEl.style.border = "1px solid #ccc";
tooltipEl.style.borderRadius = "8px";
tooltipEl.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
tooltipEl.style.fontFamily = "'Roboto', sans-serif";
tooltipEl.style.fontSize = "14px";
tooltipEl.style.color = "#333";
tooltipEl.style.maxWidth = "600px";
tooltipEl.style.maxHeight = "500px";
// flex 컬럼 레이아웃으로 구성
tooltipEl.style.display = "flex";
tooltipEl.style.flexDirection = "column";
// 헤더: 항상 보이는 영역 (sticky)
const headerDiv = document.createElement("div");
headerDiv.id = "markinfo-tooltip-header";
headerDiv.style.position = "sticky";
headerDiv.style.top = "0";
headerDiv.style.background = "#fff";
headerDiv.style.padding = "12px 16px";
headerDiv.style.borderBottom = "1px solid #ccc";
headerDiv.style.display = "flex";
headerDiv.style.justifyContent = "space-between";
headerDiv.style.alignItems = "center";
// 헤더 내부: 검색 키워드와 제작자 정보를 수직 정렬
const headerContent = document.createElement("div");
headerContent.style.display = "flex";
headerContent.style.flexDirection = "column";
// 검색 키워드 제목
const titleElem = document.createElement("h2");
titleElem.id = "tooltip-title";
titleElem.style.margin = "0";
titleElem.style.fontSize = "20px";
titleElem.style.color = "#2c3e50";
headerContent.appendChild(titleElem);
// 제작자 정보 (작은 글씨)
const creatorElem = document.createElement("span");
creatorElem.id = "tooltip-creator";
creatorElem.textContent = "내차는언제타냐: 지재권 검색기 (ESC키로 닫기)";
creatorElem.style.fontSize = "12px";
creatorElem.style.color = "#7f8c8d";
headerContent.appendChild(creatorElem);
headerDiv.appendChild(headerContent);
// 헤더 버튼 영역
const headerButtons = document.createElement("div");
headerButtons.style.display = "flex";
headerButtons.style.gap = "8px";
// 금지어 추가 버튼 (헤더)
const addBannedBtn = document.createElement("button");
addBannedBtn.id = "add-banned-word-btn";
addBannedBtn.textContent = "내 금지어에 추가";
addBannedBtn.style.padding = "6px 12px";
addBannedBtn.style.backgroundColor = "#f39c12";
addBannedBtn.style.color = "#fff";
addBannedBtn.style.border = "none";
addBannedBtn.style.borderRadius = "4px";
addBannedBtn.style.cursor = "pointer";
addBannedBtn.style.fontSize = "12px";
addBannedBtn.onclick = () => addToBannedWords(currentKeyword);
headerButtons.appendChild(addBannedBtn);
// 내부 닫기 버튼 (헤더 우측)
const headerCloseBtn = document.createElement("button");
headerCloseBtn.textContent = "닫기";
headerCloseBtn.style.padding = "6px 10px";
headerCloseBtn.style.backgroundColor = "#e74c3c";
headerCloseBtn.style.color = "#fff";
headerCloseBtn.style.border = "none";
headerCloseBtn.style.borderRadius = "4px";
headerCloseBtn.style.cursor = "pointer";
headerCloseBtn.onclick = removeTooltip;
headerButtons.appendChild(headerCloseBtn);
headerDiv.appendChild(headerButtons);
// 본문 영역 (스크롤 가능)
const bodyDiv = document.createElement("div");
bodyDiv.id = "markinfo-tooltip-body";
bodyDiv.style.padding = "16px";
bodyDiv.style.overflowY = "auto";
bodyDiv.style.flex = "1 1 auto";
tooltipEl.appendChild(headerDiv);
tooltipEl.appendChild(bodyDiv);
document.body.appendChild(tooltipEl);
// 글로벌 닫기 버튼 (항상 보이는 우측 상단)
ensureGlobalCloseButton();
}
// 업데이트: 헤더 제목에 검색 키워드 설정
document.getElementById("tooltip-title").textContent = "검색 키워드: " + message.keyword;
renderDetailInfo(message.detailInfo, message.keyword);
if (lastContextMenuPos) {
positionTooltip(tooltipEl, lastContextMenuPos);
} else {
tooltipEl.style.top = "10px";
tooltipEl.style.left = "10px";
}
console.log('[content.js] 툴팁 표시 완료');
// 성공 응답 전송
sendResponse({ success: true, message: "툴팁이 성공적으로 표시되었습니다." });
} catch (error) {
console.error('[content.js] 툴팁 표시 중 오류:', error);
// 오류 응답 전송
sendResponse({ success: false, error: error.message });
}
// 비동기 응답을 위해 true 반환
return true;
}
// 멀티번역 결과 표시
if (message.action === "showTranslationTooltip") {
console.log(`[content.js] showTranslationTooltip 메시지 수신, 원문: ${message.originalText}, 결과 개수: ${message.results?.length || 0}`);
// 로딩 인디케이터 제거
removeLoadingIndicator();
try {
showTranslationResults(message.originalText, message.results, message.userLevel);
// 성공 응답 전송
sendResponse({ success: true, message: "번역 결과가 성공적으로 표시되었습니다." });
} catch (error) {
console.error('[content.js] 번역 결과 표시 중 오류:', error);
// 오류 응답 전송
sendResponse({ success: false, error: error.message });
}
// 비동기 응답을 위해 true 반환
return true;
}
});
function positionTooltip(tooltip, pos) {
tooltip.style.left = (pos.x + 10) + "px";
tooltip.style.top = (pos.y + 10) + "px";
const rect = tooltip.getBoundingClientRect();
const docWidth = document.documentElement.clientWidth;
const docHeight = document.documentElement.clientHeight;
if (rect.right > docWidth) {
tooltip.style.left = (docWidth - rect.width - 10) + "px";
}
if (rect.bottom > docHeight) {
tooltip.style.top = (docHeight - rect.height - 10) + "px";
}
}
// selection 영역 교체 함수 (간단 버전)
function replaceSelectedText(newText) {
const selection = window.getSelection();
if (selection && selection.rangeCount) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(newText));
return true;
}
return false;
}
function removeTooltip() {
if (tooltipEl && tooltipEl.parentNode) {
tooltipEl.parentNode.removeChild(tooltipEl);
}
tooltipEl = null;
const globalClose = document.getElementById("tooltip-global-close");
if (globalClose && globalClose.parentNode) {
globalClose.parentNode.removeChild(globalClose);
}
}
function ensureGlobalCloseButton() {
if (!document.getElementById("tooltip-global-close")) {
const btn = document.createElement("button");
btn.id = "tooltip-global-close";
btn.textContent = "닫기";
btn.style.position = "fixed";
btn.style.top = "20px";
btn.style.right = "20px";
btn.style.padding = "8px 12px";
btn.style.backgroundColor = "#e74c3c";
btn.style.color = "#fff";
btn.style.border = "none";
btn.style.borderRadius = "4px";
btn.style.cursor = "pointer";
btn.style.zIndex = "1000000";
btn.onclick = removeTooltip;
document.body.appendChild(btn);
}
}
function renderDetailInfo(results, keyword) {
// 검색 결과를 전역 변수에 저장
window.currentSearchResults = results;
const bodyDiv = document.getElementById("markinfo-tooltip-body");
if (!bodyDiv) return;
let html = `<div style="line-height: 1.6;">`;
// 결과가 없을 경우 안전한 단어 표시
if (results.error) {
html += `<p style="color: red; font-weight: bold;">오류: ${results.error}</p>`;
} else if (!Array.isArray(results) || results.length === 0) {
html += `<p style="color: #7f8c8d; font-style: italic;">지식재산권이 없는 안전한 단어</p>`;
} else {
results.forEach((result, idx) => {
html += `<div style="margin-bottom: 24px; padding: 16px; border: 1px solid #ecf0f1; border-radius: 8px; background-color: #fafafa;">`;
// 결과 헤더 (제목과 개별 금지어 추가 버튼)
html += `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">`;
html += `<h3 style="margin: 0; font-size: 18px; color: #2980b9;">${idx + 1}번 결과</h3>`;
// html += `<button onclick="addIndividualToBannedWords('${result.registration_info?.trademarkName || keyword}', ${idx})"
// id="individual-banned-btn-${idx}"
// style="padding: 4px 8px; background-color: #e67e22; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px;">
// 이 상표를 금지어에 추가
// </button>`;
html += `</div>`;
// 상세 검색 결과 (출원번호 상세 조회)
if (result.detail) {
html += `<h4 style="margin: 12px 0 6px; font-size: 16px; color: #16a085; border-bottom: 1px solid #16a085; display: inline-block;">상세 정보</h4>`;
const dreg = result.detail.registration_info;
// 상표명 불일치 검사 및 표시
const trademarkName = dreg.trademarkName || "(상표명 없음)";
const isNameMismatch = trademarkName !== "(상표명 없음)" &&
keyword &&
trademarkName.toLowerCase() !== keyword.toLowerCase() &&
!trademarkName.toLowerCase().includes(keyword.toLowerCase()) &&
!keyword.toLowerCase().includes(trademarkName.toLowerCase());
if (isNameMismatch) {
html += `<p style="margin: 6px 0;"><strong>상표명:</strong> <span style="color: red; font-weight: bold;">${trademarkName} (불일치-확인필요)</span></p>`;
} else {
html += `<p style="margin: 6px 0;"><strong>상표명:</strong> ${trademarkName}</p>`;
}
html += `<p style="margin: 6px 0;"><strong>출원번호:</strong> ${dreg.applicationNum || "(출원번호 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>출원날짜:</strong> ${dreg.applicationDate || "(출원날짜 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>권리상태:</strong> ${dreg.lastDisposalCodeName || "(권리상태 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>공고번호:</strong> ${dreg.publicationNum || "(공고번호 없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>등록번호:</strong> ${dreg.registerNum || "(등록번호 없음)"}</p>`;
html += `<h4 style="margin: 12px 0 6px; font-size: 16px; color: #8e44ad; border-bottom: 1px solid #8e44ad; display: inline-block;">권리정보</h4>`;
if (result.detail.rights_info && Object.keys(result.detail.rights_info).length > 0) {
for (let key in result.detail.rights_info) {
html += `<p style="margin: 6px 0;"><strong>카테고리 코드:</strong> ${key}</p>`;
result.detail.rights_info[key].forEach(item => {
html += `<p style="margin-left: 20px; margin: 4px 0;">- 지정상품명: ${item.asignProductName || ""}</p>`;
html += `<p style="margin-left: 20px; margin: 4px 0;">&nbsp;&nbsp;영문: ${item.asignProductNameEn || ""}</p>`;
html += `<p style="margin-left: 20px; margin: 4px 0;">&nbsp;&nbsp;유사군코드: ${item.similarCodes || ""}</p>`;
});
}
} else {
html += `<p style="margin: 6px 0; color: #7f8c8d;">(권리정보 없음)</p>`;
}
html += `<h4 style="margin: 12px 0 6px; font-size: 16px; color: #e67e22; border-bottom: 1px solid #e67e22; display: inline-block;">출원인 정보</h4>`;
if (result.detail.applicant_info && result.detail.applicant_info.mapping) {
const mapping = result.detail.applicant_info.mapping;
html += `<p style="margin: 6px 0;"><strong>국가명:</strong> ${mapping.nationalCodeName || "(없음)"}</p>`;
html += `<p style="margin: 6px 0;"><strong>출원인명:</strong> ${mapping.applicantName || "(없음)"}</p>`;
} else {
html += `<p style="margin: 6px 0; color: #7f8c8d;">(출원인 정보 없음)</p>`;
}
} else if (result.detailError) {
html += `<p style="color: red; font-weight: bold;">상세 정보 검색 오류: ${result.detailError}</p>`;
}
html += `</div>`;
});
}
html += `</div>`;
bodyDiv.innerHTML = html;
}
// 금지어 추가 함수
async function addToBannedWords(keyword) {
try {
console.log(`[content.js] 금지어 추가 시작: ${keyword}`);
// Grade 선택 모달 표시
const selectedGrade = await showGradeSelectionModal(keyword);
if (!selectedGrade) {
console.log('[content.js] 사용자가 금지어 추가를 취소했습니다.');
return;
}
// 백그라운드 스크립트에 금지어 추가 요청
chrome.runtime.sendMessage({
action: "addBannedWord",
keyword: keyword,
grade: selectedGrade,
searchResults: getCurrentSearchResults()
}, (response) => {
if (chrome.runtime.lastError) {
console.error('[content.js] 금지어 추가 메시지 전송 실패:', chrome.runtime.lastError);
alert('금지어 추가 중 오류가 발생했습니다.');
return;
}
if (response && response.success) {
console.log('[content.js] 금지어 추가 성공');
alert(`"${keyword}"이(가) 금지어 목록에 추가되었습니다. (등급: ${selectedGrade})`);
// 금지어 추가 버튼 비활성화
const addBtn = document.getElementById("add-banned-word-btn");
if (addBtn) {
addBtn.textContent = "추가 완료";
addBtn.disabled = true;
addBtn.style.backgroundColor = "#95a5a6";
addBtn.style.cursor = "not-allowed";
}
} else {
console.error('[content.js] 금지어 추가 실패:', response?.error);
alert(`금지어 추가 실패: ${response?.error || '알 수 없는 오류'}`);
}
});
} catch (error) {
console.error('[content.js] 금지어 추가 중 오류:', error);
alert('금지어 추가 중 오류가 발생했습니다.');
}
}
// Grade 선택 모달 함수
function showGradeSelectionModal(keyword) {
return new Promise((resolve) => {
// 모달 배경
const modalOverlay = document.createElement('div');
modalOverlay.style.position = 'fixed';
modalOverlay.style.top = '0';
modalOverlay.style.left = '0';
modalOverlay.style.width = '100%';
modalOverlay.style.height = '100%';
modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
modalOverlay.style.zIndex = '1000000';
modalOverlay.style.display = 'flex';
modalOverlay.style.justifyContent = 'center';
modalOverlay.style.alignItems = 'center';
// 모달 컨테이너
const modalContainer = document.createElement('div');
modalContainer.style.backgroundColor = '#fff';
modalContainer.style.borderRadius = '8px';
modalContainer.style.padding = '24px';
modalContainer.style.maxWidth = '400px';
modalContainer.style.width = '90%';
modalContainer.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
modalContainer.style.fontFamily = "'Roboto', sans-serif";
// 제목
const title = document.createElement('h3');
title.textContent = '금지어 등급 선택';
title.style.margin = '0 0 16px 0';
title.style.color = '#2c3e50';
title.style.fontSize = '18px';
modalContainer.appendChild(title);
// 키워드 표시
const keywordLabel = document.createElement('p');
keywordLabel.textContent = `키워드: "${keyword}"`;
keywordLabel.style.margin = '0 0 16px 0';
keywordLabel.style.color = '#7f8c8d';
keywordLabel.style.fontSize = '14px';
modalContainer.appendChild(keywordLabel);
// 설명
const description = document.createElement('p');
description.textContent = '이 키워드의 금지 등급을 선택해주세요:';
description.style.margin = '0 0 12px 0';
description.style.color = '#34495e';
description.style.fontSize = '14px';
modalContainer.appendChild(description);
// Grade 선택 드롭박스
const gradeSelect = document.createElement('select');
gradeSelect.style.width = '100%';
gradeSelect.style.padding = '8px 12px';
gradeSelect.style.border = '1px solid #bdc3c7';
gradeSelect.style.borderRadius = '4px';
gradeSelect.style.fontSize = '14px';
gradeSelect.style.marginBottom = '20px';
// 옵션 추가
const gradeOptions = [
{ value: '비허용', text: '비허용(단어제거)' },
{ value: '금지', text: '금지(상품금지)' }
];
gradeOptions.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.text;
if (option.value === '비허용') {
optionElement.selected = true; // 기본값: 비허용
}
gradeSelect.appendChild(optionElement);
});
modalContainer.appendChild(gradeSelect);
// 버튼 컨테이너
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '8px';
// 취소 버튼
const cancelButton = document.createElement('button');
cancelButton.textContent = '취소';
cancelButton.style.padding = '8px 16px';
cancelButton.style.backgroundColor = '#95a5a6';
cancelButton.style.color = '#fff';
cancelButton.style.border = 'none';
cancelButton.style.borderRadius = '4px';
cancelButton.style.cursor = 'pointer';
cancelButton.style.fontSize = '14px';
cancelButton.onclick = () => {
document.body.removeChild(modalOverlay);
resolve(null); // 취소
};
// 확인 버튼
const confirmButton = document.createElement('button');
confirmButton.textContent = '추가';
confirmButton.style.padding = '8px 16px';
confirmButton.style.backgroundColor = '#f39c12';
confirmButton.style.color = '#fff';
confirmButton.style.border = 'none';
confirmButton.style.borderRadius = '4px';
confirmButton.style.cursor = 'pointer';
confirmButton.style.fontSize = '14px';
confirmButton.onclick = () => {
const selectedGrade = gradeSelect.value;
document.body.removeChild(modalOverlay);
resolve(selectedGrade);
};
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(confirmButton);
modalContainer.appendChild(buttonContainer);
// ESC 키로 닫기
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
document.body.removeChild(modalOverlay);
document.removeEventListener('keydown', handleKeyDown);
resolve(null);
}
};
document.addEventListener('keydown', handleKeyDown);
modalOverlay.appendChild(modalContainer);
document.body.appendChild(modalOverlay);
// 드롭박스에 포커스
gradeSelect.focus();
});
}
// 현재 검색 결과 데이터 가져오기
function getCurrentSearchResults() {
// 현재 표시된 검색 결과 데이터를 반환
// 이 데이터는 renderDetailInfo에서 사용된 results와 동일해야 함
return window.currentSearchResults || [];
}
// 번역 결과 모달 표시
function showTranslationResults(originalText, results, userLevel) {
console.log('[content.js] 번역 결과 표시 시작');
let translationTooltip = document.getElementById("translation-tooltip");
if (!translationTooltip) {
translationTooltip = document.createElement("div");
translationTooltip.id = "translation-tooltip";
translationTooltip.style.position = "fixed";
translationTooltip.style.zIndex = "9999999";
translationTooltip.style.background = "#fff";
translationTooltip.style.border = "2px solid #3498db";
translationTooltip.style.borderRadius = "12px";
translationTooltip.style.boxShadow = "0 8px 24px rgba(0,0,0,0.2)";
translationTooltip.style.fontFamily = "'Roboto', sans-serif";
translationTooltip.style.fontSize = "14px";
translationTooltip.style.color = "#333";
translationTooltip.style.minWidth = "400px";
translationTooltip.style.maxWidth = "800px";
translationTooltip.style.maxHeight = "600px";
translationTooltip.style.display = "flex";
translationTooltip.style.flexDirection = "column";
// 헤더
const header = document.createElement("div");
header.style.background = "linear-gradient(135deg, #3498db, #2980b9)";
header.style.color = "#fff";
header.style.padding = "16px 20px";
header.style.borderRadius = "10px 10px 0 0";
header.style.display = "flex";
header.style.justifyContent = "space-between";
header.style.alignItems = "center";
// 헤더 내용
const headerContent = document.createElement("div");
const titleElem = document.createElement("h2");
titleElem.style.margin = "0";
titleElem.style.fontSize = "18px";
titleElem.textContent = "멀티번역 결과";
headerContent.appendChild(titleElem);
const subtitleElem = document.createElement("div");
subtitleElem.style.fontSize = "12px";
subtitleElem.style.opacity = "0.9";
subtitleElem.style.marginTop = "4px";
subtitleElem.textContent = `회원등급: ${userLevel || 'Basic'} | ESC키로 닫기`;
headerContent.appendChild(subtitleElem);
header.appendChild(headerContent);
// 닫기 버튼
const closeBtn = document.createElement("button");
closeBtn.textContent = "×";
closeBtn.style.background = "none";
closeBtn.style.border = "2px solid #fff";
closeBtn.style.color = "#fff";
closeBtn.style.borderRadius = "50%";
closeBtn.style.width = "30px";
closeBtn.style.height = "30px";
closeBtn.style.cursor = "pointer";
closeBtn.style.fontSize = "18px";
closeBtn.style.lineHeight = "1";
closeBtn.onclick = removeTranslationTooltip;
header.appendChild(closeBtn);
// 본문
const body = document.createElement("div");
body.id = "translation-body";
body.style.padding = "20px";
body.style.overflowY = "auto";
body.style.flex = "1 1 auto";
translationTooltip.appendChild(header);
translationTooltip.appendChild(body);
document.body.appendChild(translationTooltip);
// ESC 키로 닫기
document.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
removeTranslationTooltip();
}
});
}
// 본문 업데이트
renderTranslationResults(originalText, results, userLevel);
// 위치 설정 (화면 중앙)
translationTooltip.style.top = "50%";
translationTooltip.style.left = "50%";
translationTooltip.style.transform = "translate(-50%, -50%)";
console.log('[content.js] 번역 결과 표시 완료');
}
// 번역 결과 렌더링
function renderTranslationResults(originalText, results, userLevel) {
const body = document.getElementById("translation-body");
if (!body) return;
let html = '';
// 원문 표시
html += `
<div style="margin-bottom: 20px; padding: 16px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #3498db;">
<h3 style="margin: 0 0 8px 0; color: #2c3e50; font-size: 16px;">원문</h3>
<p style="margin: 0; font-size: 15px; line-height: 1.5; color: #333;">${originalText}</p>
</div>
`;
// 번역 결과가 있을 경우
if (results && results.length > 0) {
html += '<div style="margin-bottom: 16px;">';
results.forEach((result, index) => {
const engineName = getEngineDisplayName(result.engine);
const isSuccess = result.success;
// 각 번역 결과 카드
html += `
<div style="margin-bottom: 16px; padding: 16px; border: 1px solid ${isSuccess ? '#e1e8ed' : '#f8d7da'}; border-radius: 8px; background: ${isSuccess ? '#fff' : '#f8f9fa'};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: ${isSuccess ? '12px' : '8px'};">
<h4 style="margin: 0; color: ${isSuccess ? '#2c3e50' : '#721c24'}; font-size: 14px; font-weight: 600;">
${engineName}
</h4>
<span style="padding: 4px 8px; border-radius: 4px; font-size: 12px; background: ${isSuccess ? '#d4edda' : '#f8d7da'}; color: ${isSuccess ? '#155724' : '#721c24'};">
${isSuccess ? '성공' : '실패'}
</span>
</div>
${isSuccess ?
`<p style="margin: 0; font-size: 15px; line-height: 1.5; color: #333;">${result.translatedText}</p>` :
`<p style="margin: 0; font-size: 14px; color: #721c24;">${result.error || '번역 실패'}</p>`
}
</div>
`;
});
html += '</div>';
} else {
// 번역 결과가 없는 경우
html += `
<div style="text-align: center; padding: 40px; color: #6c757d;">
<p style="margin: 0; font-size: 16px;">번역 결과가 없습니다.</p>
<p style="margin: 8px 0 0 0; font-size: 14px;">다시 시도해 주세요.</p>
</div>
`;
}
// 회원등급별 가이드 메시지
html += `
<div style="margin-top: 20px; padding: 12px; background: #e3f2fd; border-radius: 6px; border-left: 4px solid #2196f3;">
<p style="margin: 0; font-size: 13px; color: #1565c0;">
💡 <strong>${userLevel || 'Basic'} 회원</strong>으로 이용 중입니다.
${getUserLevelGuide(userLevel)}
</p>
</div>
`;
body.innerHTML = html;
}
// 엔진 이름 표시용 변환
function getEngineDisplayName(engine) {
const displayNames = {
'google': '구글 번역',
'deepl': 'DeepL',
'openai': 'ChatGPT',
'gemini': 'Google Gemini',
'mymemory': 'MyMemory'
};
return displayNames[engine] || engine;
}
// 회원등급별 가이드 메시지
function getUserLevelGuide(userLevel) {
const normalizedLevel = (userLevel || 'basic').toLowerCase();
switch(normalizedLevel) {
case 'vip':
return '모든 번역 엔진을 이용할 수 있습니다!';
case 'premium':
return '무료 번역과 DeepL을 이용할 수 있습니다. VIP로 업그레이드하면 ChatGPT, Gemini도 이용 가능합니다.';
case 'basic':
default:
return '현재 무료 번역만 이용 가능합니다. 프리미엄 회원으로 업그레이드하면 더 많은 번역 엔진을 이용할 수 있습니다.';
}
}
// 번역 툴팁 제거
function removeTranslationTooltip() {
const existingTooltip = document.getElementById('translation-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
}
// 자동 찜하기 전역 변수들
let autoZzimState = {
isRunning: false,
maxZzim: 50,
actualZzimCount: 0,
currentPage: 1,
delay: 1000,
statusDiv: null,
startTime: null
};
// 찜하기 버튼 찾기 함수 (전역) - 정확한 선택자 사용
function findZzimButtons() {
console.log('[AutoZzim] 찜 버튼 찾기 시작...');
// 네이버 스마트스토어 정확한 선택자 사용
const zzimButtonSelector = 'div#CategoryProducts button.zzim_button[aria-pressed="false"]';
try {
const zzimButtons = document.querySelectorAll(zzimButtonSelector);
console.log(`[AutoZzim] 찜 가능한 버튼 발견: ${zzimButtons.length}`);
// 실제로 보이고 클릭 가능한 버튼만 필터링
const visibleButtons = Array.from(zzimButtons).filter(btn => {
const rect = btn.getBoundingClientRect();
const style = window.getComputedStyle(btn);
const isVisible = rect.width > 0 &&
rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
!btn.disabled;
if (isVisible) {
// 버튼 주변에 다른 클릭 가능한 요소가 있는지 확인
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// 버튼 중심 좌표에서 실제 클릭될 요소 확인
const elementAtPoint = document.elementFromPoint(centerX, centerY);
// 클릭될 요소가 찜 버튼이거나 찜 버튼의 자식 요소인지 확인
const isCorrectElement = elementAtPoint === btn || btn.contains(elementAtPoint);
if (!isCorrectElement) {
console.log('[AutoZzim] 버튼이 다른 요소에 가려짐:', {
button: btn,
elementAtPoint: elementAtPoint,
elementAtPointTag: elementAtPoint?.tagName,
elementAtPointClass: elementAtPoint?.className
});
return false;
}
// 버튼 텍스트 확인 (찜하기 관련 텍스트가 있는지)
const buttonText = btn.textContent?.trim().toLowerCase() || '';
const buttonAriaLabel = btn.getAttribute('aria-label')?.toLowerCase() || '';
const isZzimButton = buttonText.includes('찜') ||
buttonText.includes('하트') ||
buttonAriaLabel.includes('찜') ||
buttonAriaLabel.includes('wishlist') ||
btn.className.includes('zzim') ||
btn.className.includes('wish');
if (!isZzimButton) {
console.log('[AutoZzim] 찜하기 버튼이 아님:', {
text: buttonText,
ariaLabel: buttonAriaLabel,
className: btn.className
});
return false;
}
// 판매자 정보나 다른 기능 버튼이 아닌지 확인
const isNotOtherButton = !buttonText.includes('판매자') &&
!buttonText.includes('상점') &&
!buttonText.includes('문의') &&
!buttonText.includes('연락') &&
!buttonText.includes('전화') &&
!buttonText.includes('리뷰') &&
!buttonText.includes('평점') &&
!buttonAriaLabel.includes('판매자') &&
!buttonAriaLabel.includes('상점');
if (!isNotOtherButton) {
console.log('[AutoZzim] 다른 기능 버튼임:', {
text: buttonText,
ariaLabel: buttonAriaLabel
});
return false;
}
console.log('[AutoZzim] 찜 가능한 버튼 확인:', {
text: btn.textContent?.trim().substring(0, 30),
className: btn.className,
ariaPressed: btn.getAttribute('aria-pressed'),
position: `x:${Math.round(rect.x)}, y:${Math.round(rect.y)}`,
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
isCorrectElement: isCorrectElement,
isZzimButton: isZzimButton,
isNotOtherButton: isNotOtherButton
});
}
return isVisible;
});
console.log(`[AutoZzim] 최종 찜 가능한 버튼: ${visibleButtons.length}`);
return visibleButtons;
} catch (error) {
console.error('[AutoZzim] 찜 버튼 찾기 오류:', error);
return [];
}
}
// 이미 찜한 상품인지 확인 함수 (전역) - 단순화
function isAlreadyZzimed_Improved(button) {
try {
// aria-pressed 속성으로 확인 (가장 정확한 방법)
const ariaPressed = button.getAttribute('aria-pressed');
const isZzimed = ariaPressed === 'true';
if (isZzimed) {
console.log('[AutoZzim] 이미 찜됨 (aria-pressed=true)');
}
return isZzimed;
} catch (e) {
console.error('[AutoZzim] 찜 상태 확인 오류:', e);
return false;
}
}
// 찜하기 버튼 클릭 함수 (전역) - 개선된 버전
async function clickZzimButton(button) {
console.log('[AutoZzim] 찜 버튼 클릭 시작:', {
text: button.textContent?.trim().substring(0, 30),
className: button.className,
ariaPressed: button.getAttribute('aria-pressed'),
tagName: button.tagName,
id: button.id
});
return new Promise((resolve) => {
try {
// 클릭 전 상태 확인
const beforePressed = button.getAttribute('aria-pressed');
console.log('[AutoZzim] 클릭 전 aria-pressed:', beforePressed);
if (beforePressed === 'true') {
console.log('[AutoZzim] 이미 찜된 상품, 건너뛰기');
resolve(false);
return;
}
// 버튼이 클릭 가능한지 확인
if (button.disabled) {
console.log('[AutoZzim] 버튼이 비활성화되어 있음');
resolve(false);
return;
}
// 버튼 주변 요소 확인 (판매자 정보 등 다른 클릭 가능한 요소가 있는지)
const rect = button.getBoundingClientRect();
console.log('[AutoZzim] 버튼 위치 및 크기:', {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
centerX: rect.x + rect.width / 2,
centerY: rect.y + rect.height / 2
});
// 버튼이 화면에 보이도록 스크롤 (부드럽게)
button.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
setTimeout(() => {
if (!autoZzimState.isRunning) {
console.log('[AutoZzim] 찜하기가 중단됨');
resolve(false);
return;
}
// 버튼 클릭 시도 (이벤트 전파 방지)
try {
console.log('[AutoZzim] 정확한 버튼 클릭 시도 (이벤트 전파 방지)');
// 방법 1: 이벤트 전파를 막는 직접 클릭
const clickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
console.log('[AutoZzim] 클릭 이벤트 전파 차단됨');
};
// 임시 이벤트 리스너 추가 (이벤트 전파 방지)
button.addEventListener('click', clickHandler, { capture: true, once: true });
// 포커스 설정
if (button.focus) {
button.focus();
}
// 직접 클릭 (가장 안전한 방법)
button.click();
console.log('[AutoZzim] 직접 클릭 완료');
// 클릭 후 상태 변화 확인
let checkAttempts = 0;
const maxAttempts = 10; // 최대 5초간 확인
const checkStateChange = () => {
checkAttempts++;
const afterPressed = button.getAttribute('aria-pressed');
console.log(`[AutoZzim] 상태 확인 ${checkAttempts}/${maxAttempts}:`, {
before: beforePressed,
after: afterPressed,
success: beforePressed === 'false' && afterPressed === 'true'
});
// 성공 조건: false → true로 변경
if (beforePressed === 'false' && afterPressed === 'true') {
console.log('[AutoZzim] ✅ 찜하기 성공!');
autoZzimState.actualZzimCount++;
// 확인 모달 처리 (필요한 경우)
setTimeout(() => {
handleConfirmationModals();
}, 200);
resolve(true);
return;
}
// 재시도 조건
if (checkAttempts < maxAttempts) {
setTimeout(checkStateChange, 500); // 0.5초마다 재확인
} else {
console.log('[AutoZzim] ❌ 찜하기 실패 (상태 변화 없음), 대안 방법 시도');
// 대안 방법: 더 정확한 이벤트 생성
try {
console.log('[AutoZzim] 대안 방법: 정확한 좌표로 클릭 이벤트 생성');
// 버튼의 정확한 중심 좌표 계산
const updatedRect = button.getBoundingClientRect();
const centerX = updatedRect.left + updatedRect.width / 2;
const centerY = updatedRect.top + updatedRect.height / 2;
// 더 정확한 마우스 이벤트 생성 (이벤트 전파 방지)
const mouseDownEvent = new MouseEvent('mousedown', {
bubbles: false, // 이벤트 전파 방지
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0
});
const mouseUpEvent = new MouseEvent('mouseup', {
bubbles: false, // 이벤트 전파 방지
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0
});
const clickEvent = new MouseEvent('click', {
bubbles: false, // 이벤트 전파 방지
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0
});
// 순차적으로 이벤트 발생
button.dispatchEvent(mouseDownEvent);
setTimeout(() => {
button.dispatchEvent(mouseUpEvent);
setTimeout(() => {
button.dispatchEvent(clickEvent);
// 최종 상태 확인
setTimeout(() => {
const finalPressed = button.getAttribute('aria-pressed');
if (beforePressed === 'false' && finalPressed === 'true') {
console.log('[AutoZzim] ✅ 대안 방법으로 찜하기 성공!');
autoZzimState.actualZzimCount++;
resolve(true);
} else {
console.log('[AutoZzim] ❌ 대안 방법도 실패');
resolve(false);
}
}, 1000);
}, 50);
}, 50);
} catch (alternativeError) {
console.error('[AutoZzim] 대안 방법 실패:', alternativeError);
resolve(false);
}
}
};
// 첫 번째 상태 확인은 0.5초 후
setTimeout(checkStateChange, 500);
} catch (clickError) {
console.error('[AutoZzim] 버튼 클릭 오류:', clickError);
resolve(false);
}
}, 800); // 스크롤 후 충분한 대기 시간
} catch (e) {
console.error('[AutoZzim] 찜 버튼 클릭 중 예외 오류:', e);
resolve(false);
}
});
}
// 확인 모달/팝업 처리 함수 (전역) - 개선된 버전
function handleConfirmationModals() {
console.log('[AutoZzim] 확인 모달 처리 시작');
// 찜하기 관련 확인 모달만 처리하도록 제한
const confirmSelectors = [
// 네이버 스마트스토어 찜하기 전용 확인 버튼
'button[data-testid="wishlist-confirm"]',
'button[data-testid="zzim-confirm"]',
'.zzim_confirm_btn',
'.wishlist_confirm_btn',
// 일반적인 확인 버튼 (찜하기 관련 텍스트가 있는 경우만)
'button[class*="confirm"]:contains("찜")',
'button[class*="confirm"]:contains("확인")',
// 모달 내부의 확인 버튼 (찜하기 관련만)
'.modal button[class*="confirm"]',
'.popup button[class*="confirm"]',
'.dialog button[class*="confirm"]',
// 레이어 팝업 내 확인 버튼
'[class*="layer"] button[class*="confirm"]',
'[role="dialog"] button[class*="primary"]'
];
for (const selector of confirmSelectors) {
try {
let confirmButtons = [];
// :contains() 선택자 수동 처리
if (selector.includes(':contains(')) {
const [baseSelector, containsText] = selector.split(':contains(');
const searchText = containsText.replace(/[\(\)\"\']/g, '');
const candidateElements = document.querySelectorAll(baseSelector);
confirmButtons = Array.from(candidateElements).filter(el => {
const text = el.textContent && el.textContent.trim();
return text && text.includes(searchText);
});
} else {
confirmButtons = Array.from(document.querySelectorAll(selector));
}
// 보이고 클릭 가능한 버튼 중에서 찜하기 관련만 필터링
for (const btn of confirmButtons) {
if (btn && btn.offsetParent !== null && !btn.disabled) {
const rect = btn.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
// 버튼 텍스트 확인 - 찜하기 관련 텍스트가 있는지 확인
const buttonText = btn.textContent?.trim().toLowerCase() || '';
const isZzimRelated = buttonText.includes('찜') ||
buttonText.includes('확인') ||
buttonText.includes('ok') ||
buttonText.includes('예') ||
buttonText.includes('yes');
// 판매자 정보나 다른 팝업이 아닌지 확인
const isNotSellerInfo = !buttonText.includes('판매자') &&
!buttonText.includes('상점') &&
!buttonText.includes('업체') &&
!buttonText.includes('문의') &&
!buttonText.includes('연락') &&
!buttonText.includes('전화');
if (isZzimRelated && isNotSellerInfo) {
console.log('[AutoZzim] 찜하기 관련 확인 버튼 클릭:', {
selector: selector,
text: buttonText,
className: btn.className
});
// 이벤트 전파를 막고 클릭
const clickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
};
btn.addEventListener('click', clickHandler, { capture: true, once: true });
btn.click();
return; // 첫 번째 적절한 확인 버튼만 클릭하고 종료
} else {
console.log('[AutoZzim] 찜하기와 무관한 버튼 무시:', {
text: buttonText,
isZzimRelated: isZzimRelated,
isNotSellerInfo: isNotSellerInfo
});
}
}
}
}
} catch (e) {
console.log('[AutoZzim] 확인 버튼 검색 오류:', selector, e.message);
}
}
console.log('[AutoZzim] 찜하기 관련 확인 버튼을 찾지 못했습니다');
}
// 현재 페이지 찜하기 처리 함수 (전역) - 개선된 버전
async function processCurrentPage() {
if (!autoZzimState.isRunning || autoZzimState.actualZzimCount >= autoZzimState.maxZzim) {
console.log('[AutoZzim] 찜하기 중단 또는 목표 달성');
return false;
}
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 찜하기 처리 시작`);
updateZzimStatus(autoZzimState.statusDiv, `페이지 ${autoZzimState.currentPage} 분석 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
// 찜 가능한 버튼 찾기
const zzimButtons = findZzimButtons();
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에서 찾은 찜 가능한 버튼: ${zzimButtons.length}`);
if (zzimButtons.length === 0) {
console.log('[AutoZzim] 현재 페이지에 찜할 상품이 없음');
return false;
}
let processedInPage = 0;
// 각 버튼을 순차적으로 처리
for (let i = 0; i < zzimButtons.length && autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim; i++) {
const button = zzimButtons[i];
updateZzimStatus(autoZzimState.statusDiv, `찜하기 진행 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim}) - 페이지 ${autoZzimState.currentPage}`);
console.log(`[AutoZzim] ${i + 1}/${zzimButtons.length}번째 버튼 처리 중...`);
const success = await clickZzimButton(button);
if (success) {
processedInPage++;
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에서 ${processedInPage}번째 찜 성공`);
}
// 다음 버튼 처리 전 대기 (설정된 간격)
if (i < zzimButtons.length - 1 && autoZzimState.isRunning) {
console.log(`[AutoZzim] ${autoZzimState.delay}ms 대기 중...`);
await new Promise(resolve => setTimeout(resolve, autoZzimState.delay));
}
}
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 완료: ${processedInPage}개 찜함`);
return processedInPage > 0;
}
// 자동 찜하기 메인 루프 함수 (전역)
async function autoZzimMainLoop() {
console.log('[AutoZzim] 메인 루프 시작');
while (autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim && autoZzimState.currentPage <= 10) {
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 시작`);
// 페이지 로드 대기
await new Promise(resolve => setTimeout(resolve, 2000));
// 현재 페이지 처리
const hasProducts = await processCurrentPage();
if (!hasProducts) {
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에 더 이상 찜할 상품이 없음`);
// 다음 페이지로 이동 시도
updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
const moved = await goToNextPage();
if (!moved) {
console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
break;
}
} else {
// 현재 페이지에서 더 찜할 수 있는지 확인
const remainingButtons = findZzimButtons();
if (remainingButtons.length === 0) {
// 다음 페이지로 이동
updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
const moved = await goToNextPage();
if (!moved) {
console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
break;
}
}
}
// 페이지 간 대기
await new Promise(resolve => setTimeout(resolve, 2000));
}
// 완료 처리
console.log(`[AutoZzim] 찜하기 완료: 실제 ${autoZzimState.actualZzimCount}개 찜함`);
updateZzimStatus(autoZzimState.statusDiv, `찜하기 완료! (실제 ${autoZzimState.actualZzimCount}개 찜함)`);
// 상태 초기화
autoZzimState.isRunning = false;
// 5초 후 상태 UI 제거
setTimeout(() => {
if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
autoZzimState.statusDiv = null;
}
}, 5000);
}
// 자동 찜하기 기능 (개선된 버전)
function startAutoZzim(maxZzim = 50, delay = 1000) {
console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay });
// 상태 초기화
autoZzimState = {
isRunning: true,
maxZzim: maxZzim,
actualZzimCount: 0,
currentPage: 1,
delay: delay,
statusDiv: null,
startTime: Date.now()
};
// 상태 표시 UI 생성
autoZzimState.statusDiv = createZzimStatusUI();
updateZzimStatus(autoZzimState.statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`);
// 60초 후 자동 종료
setTimeout(() => {
if (autoZzimState.isRunning) {
autoZzimState.isRunning = false;
console.log('[AutoZzim] 시간 초과로 찜하기 종료');
updateZzimStatus(autoZzimState.statusDiv, `시간 초과로 종료 (실제 ${autoZzimState.actualZzimCount}개 찜함)`);
// 5초 후 상태 UI 제거
setTimeout(() => {
if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
autoZzimState.statusDiv = null;
}
}, 5000);
}
}, 60000);
// 메인 루프 시작
setTimeout(autoZzimMainLoop, 2000); // 페이지 로드 후 2초 대기
}
// 다음 페이지로 이동 함수 (기존 goToNextPage 함수 수정)
async function goToNextPage() {
try {
console.log(`[AutoZzim] 다음 페이지로 이동 시도: ${autoZzimState.currentPage}${autoZzimState.currentPage + 1}`);
// 네이버 스마트스토어 URL 분석
const urlInfo = parseNaverSmartStoreUrl(window.location.href);
if (!urlInfo) {
console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href);
return false;
}
if (!urlInfo.isProductListPage) {
console.log('[AutoZzim] 상품 목록 페이지가 아님, 올바른 페이지로 이동');
// 올바른 상품 목록 페이지 URL 생성
const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, false, 50);
console.log('[AutoZzim] 올바른 상품 목록 페이지로 이동:', correctUrl);
window.location.href = correctUrl;
return true;
}
// 1단계: 현재 페이지 번호 확인
const currentPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`);
let actualCurrentPage = 1;
if (currentPageButton) {
const currentPageId = currentPageButton.getAttribute('data-shp-contents-id');
actualCurrentPage = parseInt(currentPageId) || 1;
console.log(`[AutoZzim] 현재 페이지 확인: ${actualCurrentPage} (DOM에서 감지)`);
} else {
// aria-current 없이 data-shp-contents-id로만 확인
const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`);
console.log(`[AutoZzim] 전체 페이지 버튼 개수: ${allPageButtons.length}`);
// URL의 cp 파라미터로 현재 페이지 추정
const urlParams = new URLSearchParams(window.location.search);
const cpParam = urlParams.get('cp');
if (cpParam) {
actualCurrentPage = parseInt(cpParam) || 1;
console.log(`[AutoZzim] URL 파라미터에서 현재 페이지 확인: ${actualCurrentPage}`);
}
}
// currentPage 변수 업데이트
autoZzimState.currentPage = actualCurrentPage;
const nextPageNum = autoZzimState.currentPage + 1;
console.log(`[AutoZzim] 다음 페이지 번호: ${nextPageNum}`);
// 2단계: 다음 페이지 버튼 찾기 (네이버 스마트스토어 특화)
const nextPageSelectors = [
// 네이버 스마트스토어 페이지 버튼 (가장 정확한 방법)
`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id='${nextPageNum}']`,
// 일반적인 다음 페이지 버튼
'a.pagination_next:not(.pagination_disabled)',
'a[class*="pagination"][class*="next"]:not([class*="disabled"])',
'button[class*="pagination"][class*="next"]:not([disabled])',
'.paginate_next:not(.disabled)',
'[class*="paging"] a[class*="next"]:not([class*="disabled"])',
// aria-label 기반
'a[aria-label*="다음"]:not([aria-disabled="true"])',
'button[aria-label*="다음"]:not([disabled])',
'a[title*="다음"]:not([class*="disabled"])',
// 페이지 번호로 다음 페이지 찾기 (URL 기반)
`a[href*="cp=${nextPageNum}"]`,
`button[data-page="${nextPageNum}"]`,
`a[href*="page=${nextPageNum}"]`
];
let nextButton = null;
let selectedMethod = '';
for (const selector of nextPageSelectors) {
try {
const buttons = document.querySelectorAll(selector);
console.log(`[AutoZzim] 선택자 "${selector}" 검색 결과: ${buttons.length}`);
for (const btn of buttons) {
if (btn && btn.offsetParent !== null && !btn.disabled && !btn.classList.contains('disabled')) {
nextButton = btn;
selectedMethod = selector;
console.log(`[AutoZzim] 다음 페이지 버튼 발견:`, {
selector: selector,
text: btn.textContent?.trim(),
className: btn.className,
dataShpContentsId: btn.getAttribute('data-shp-contents-id'),
href: btn.href,
tagName: btn.tagName
});
break;
}
}
if (nextButton) break;
} catch (e) {
console.log(`[AutoZzim] 선택자 오류 (${selector}):`, e.message);
}
}
if (nextButton) {
// 다음 페이지 버튼 클릭
console.log(`[AutoZzim] 다음 페이지 버튼 클릭 시도 (방법: ${selectedMethod})`);
// 버튼이 화면에 보이도록 스크롤
nextButton.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
await new Promise(resolve => setTimeout(resolve, 1000));
// 클릭 전 상태 로그
console.log('[AutoZzim] 클릭 전 버튼 상태:', {
disabled: nextButton.disabled,
offsetParent: nextButton.offsetParent !== null,
classList: Array.from(nextButton.classList)
});
// 다양한 방식으로 클릭 시도
try {
// 방법 1: 일반 클릭
nextButton.click();
console.log('[AutoZzim] 일반 클릭 완료');
} catch (e1) {
console.log('[AutoZzim] 일반 클릭 실패, 이벤트 방식 시도:', e1.message);
try {
// 방법 2: 마우스 이벤트 시뮬레이션
const rect = nextButton.getBoundingClientRect();
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2
});
nextButton.dispatchEvent(clickEvent);
console.log('[AutoZzim] 마우스 이벤트 클릭 완료');
} catch (e2) {
console.log('[AutoZzim] 마우스 이벤트 클릭 실패:', e2.message);
// 방법 3: href가 있는 경우 직접 이동
if (nextButton.href) {
console.log('[AutoZzim] href로 직접 이동:', nextButton.href);
window.location.href = nextButton.href;
}
}
}
// 페이지 로드 대기
console.log('[AutoZzim] 페이지 로드 대기 중...');
await new Promise(resolve => setTimeout(resolve, 3000));
// 페이지 변경 확인
const newPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`);
if (newPageButton) {
const newPageId = newPageButton.getAttribute('data-shp-contents-id');
const newPageNum = parseInt(newPageId) || 1;
if (newPageNum > actualCurrentPage) {
console.log(`[AutoZzim] ✅ 페이지 이동 성공: ${actualCurrentPage}${newPageNum}`);
autoZzimState.currentPage = newPageNum;
return true;
} else {
console.log(`[AutoZzim] ❌ 페이지 이동 실패: 여전히 ${newPageNum} 페이지`);
}
} else {
// aria-current가 없어도 URL로 확인
const urlParams = new URLSearchParams(window.location.search);
const newCpParam = urlParams.get('cp');
if (newCpParam && parseInt(newCpParam) > actualCurrentPage) {
const newPageNum = parseInt(newCpParam);
console.log(`[AutoZzim] ✅ 페이지 이동 성공 (URL 확인): ${actualCurrentPage}${newPageNum}`);
autoZzimState.currentPage = newPageNum;
return true;
}
}
return false;
}
// 3단계: 버튼이 없으면 URL 직접 변경
console.log('[AutoZzim] 다음 페이지 버튼이 없음, URL 직접 변경 시도');
// 마지막 페이지 번호 확인
const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`);
const pageNumbers = Array.from(allPageButtons).map(btn => {
const id = btn.getAttribute('data-shp-contents-id');
return parseInt(id) || 0;
}).filter(num => num > 0);
const maxPage = Math.max(...pageNumbers);
console.log(`[AutoZzim] 감지된 페이지 번호들:`, pageNumbers);
console.log(`[AutoZzim] 최대 페이지 번호: ${maxPage}`);
if (nextPageNum > maxPage) {
console.log(`[AutoZzim] 다음 페이지 ${nextPageNum}이 최대 페이지 ${maxPage}를 초과함`);
return false;
}
// URL 직접 변경
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('cp', nextPageNum.toString());
console.log(`[AutoZzim] URL 직접 변경: cp=${actualCurrentPage} → cp=${nextPageNum}`);
console.log('[AutoZzim] 다음 페이지 URL:', currentUrl.href);
window.location.href = currentUrl.href;
return true;
} catch (e) {
console.error('[AutoZzim] 다음 페이지 이동 오류:', e);
return false;
}
}
function createZzimStatusUI() {
const statusDiv = document.createElement('div');
statusDiv.id = 'auto-zzim-status';
statusDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 999999;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 10px;
font-family: 'Segoe UI', sans-serif;
font-size: 14px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 200px;
text-align: center;
animation: slideIn 0.3s ease-out;
`;
// CSS 애니메이션 추가
if (!document.getElementById('auto-zzim-styles')) {
const style = document.createElement('style');
style.id = 'auto-zzim-styles';
style.textContent = `
@keyframes slideIn {
0% { transform: translateX(100%); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(statusDiv);
return statusDiv;
}
function updateZzimStatus(statusDiv, message) {
if (statusDiv) {
statusDiv.textContent = message;
}
}
// 페이지 로드 완료 감지 함수
function waitForPageLoad() {
return new Promise((resolve) => {
// 1. DOM 로드 확인
if (document.readyState === 'complete') {
console.log('[AutoZzim] DOM 이미 로드 완료');
resolve();
return;
}
// 2. 페이지 로드 이벤트 리스너
const handleLoad = () => {
console.log('[AutoZzim] 페이지 로드 완료');
window.removeEventListener('load', handleLoad);
resolve();
};
window.addEventListener('load', handleLoad);
// 3. 최대 10초 대기
setTimeout(() => {
console.log('[AutoZzim] 페이지 로드 대기 시간 초과');
window.removeEventListener('load', handleLoad);
resolve();
}, 10000);
});
}
// 페이지 로드 시 URL 파라미터 확인 (개선된 버전)
async function checkAutoZzimParam() {
const urlParams = new URLSearchParams(window.location.search);
const autoZzim = urlParams.get('auto_zzim');
const maxZzim = parseInt(urlParams.get('max_zzim')) || 50;
console.log('[AutoZzim] URL 파라미터 확인:', {
autoZzim: autoZzim,
maxZzim: maxZzim,
currentUrl: window.location.href
});
if (autoZzim === 'true') {
console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작');
// 네이버 스마트스토어 URL 분석
const urlInfo = parseNaverSmartStoreUrl(window.location.href);
if (!urlInfo) {
console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href);
return;
}
if (!urlInfo.isProductListPage) {
console.log('[AutoZzim] 현재 페이지가 상품 목록 페이지가 아님, 올바른 페이지로 이동');
// 올바른 상품 목록 페이지 URL 생성
const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, true, maxZzim);
console.log('[AutoZzim] 올바른 상품 목록 페이지로 리다이렉트:', correctUrl);
window.location.href = correctUrl;
return;
}
// 페이지 완전 로드 대기
console.log('[AutoZzim] 페이지 로드 완료 대기 중...');
await waitForPageLoad();
// 추가 로드 대기 (네이버 스마트스토어 특화)
await new Promise(resolve => setTimeout(resolve, 3000));
// 상품 목록이 로드되었는지 확인
const productCheck = await waitForProductsToLoad();
if (productCheck) {
console.log('[AutoZzim] 상품 목록 로드 완료, 찜하기 시작');
// 찜하기 시작
startAutoZzim(maxZzim);
} else {
console.log('[AutoZzim] 상품 목록 로드 실패');
}
}
}
// 상품 목록 로드 대기 함수
function waitForProductsToLoad() {
return new Promise((resolve) => {
let checkCount = 0;
const maxChecks = 20; // 최대 20번 확인 (20초)
const checkProducts = () => {
checkCount++;
// 상품 컨테이너 확인
const productContainers = document.querySelectorAll('[data-shp-contents-id], [class*="product"], [class*="item"]');
const hasProducts = productContainers.length > 0;
console.log(`[AutoZzim] 상품 로드 확인 ${checkCount}/${maxChecks}: ${productContainers.length}개 상품 발견`);
if (hasProducts) {
console.log('[AutoZzim] ✅ 상품 목록 로드 완료');
resolve(true);
return;
}
if (checkCount >= maxChecks) {
console.log('[AutoZzim] ❌ 상품 목록 로드 시간 초과');
resolve(false);
return;
}
// 1초 후 재확인
setTimeout(checkProducts, 1000);
};
// 즉시 첫 번째 확인
checkProducts();
});
}
// 페이지 로드 완료 시 자동 찜하기 파라미터 확인 (개선된 버전)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(checkAutoZzimParam, 1000);
});
} else {
setTimeout(checkAutoZzimParam, 1000);
}
// 추가: 페이지 변경 감지 (SPA 대응)
let lastUrl = window.location.href;
const urlChangeObserver = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
lastUrl = window.location.href;
console.log('[AutoZzim] URL 변경 감지:', lastUrl);
// URL 변경 후 잠시 대기 후 파라미터 확인
setTimeout(checkAutoZzimParam, 2000);
}
});
// URL 변경 감지 시작
urlChangeObserver.observe(document.body, {
childList: true,
subtree: true
});
// popstate 이벤트도 감지 (뒤로가기/앞으로가기)
window.addEventListener('popstate', () => {
console.log('[AutoZzim] popstate 이벤트 감지');
setTimeout(checkAutoZzimParam, 2000);
});
// 자동 찜하기 파라미터 확인
checkAutoZzimParam();
// 디버깅용 전역 함수들 추가
window.debugZzimButtons = function() {
console.log('=== 네이버 스마트스토어 찜 버튼 디버깅 ===');
// 정확한 선택자로 찜 버튼 찾기
const zzimButtonSelector = 'div#CategoryProducts button.zzim_button';
const allZzimButtons = document.querySelectorAll(zzimButtonSelector);
console.log(`전체 찜 버튼: ${allZzimButtons.length}`);
// 찜 가능한 버튼 (aria-pressed="false")
const availableZzimButtons = document.querySelectorAll('div#CategoryProducts button.zzim_button[aria-pressed="false"]');
console.log(`찜 가능한 버튼: ${availableZzimButtons.length}`);
// 이미 찜한 버튼 (aria-pressed="true")
const zzimmedButtons = document.querySelectorAll('div#CategoryProducts button.zzim_button[aria-pressed="true"]');
console.log(`이미 찜한 버튼: ${zzimmedButtons.length}`);
// 각 버튼 상세 정보 출력
console.log('--- 찜 가능한 버튼 상세 정보 ---');
availableZzimButtons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
console.log(`[${index + 1}] 찜 가능한 버튼:`, {
text: btn.textContent?.trim().substring(0, 30),
className: btn.className,
ariaPressed: btn.getAttribute('aria-pressed'),
isVisible: rect.width > 0 && rect.height > 0,
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
size: { width: Math.round(rect.width), height: Math.round(rect.height) },
disabled: btn.disabled,
element: btn
});
});
console.log('--- 이미 찜한 버튼 상세 정보 ---');
zzimmedButtons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
console.log(`[${index + 1}] 이미 찜한 버튼:`, {
text: btn.textContent?.trim().substring(0, 30),
className: btn.className,
ariaPressed: btn.getAttribute('aria-pressed'),
isVisible: rect.width > 0 && rect.height > 0,
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
element: btn
});
});
// 상품 컨테이너 확인
const categoryProducts = document.getElementById('CategoryProducts');
if (categoryProducts) {
console.log('CategoryProducts 컨테이너 발견:', categoryProducts);
} else {
console.log('❌ CategoryProducts 컨테이너를 찾을 수 없음');
}
return {
totalZzimButtons: allZzimButtons.length,
availableZzimButtons: availableZzimButtons.length,
zzimmedButtons: zzimmedButtons.length,
availableButtons: Array.from(availableZzimButtons),
zzimmedButtonsArray: Array.from(zzimmedButtons)
};
};
window.findCurrentZzimButtons = function() {
console.log('=== 현재 페이지 찜 버튼 찾기 ===');
const buttons = findZzimButtons();
console.log(`찾은 찜 가능한 버튼: ${buttons.length}`);
buttons.forEach((btn, index) => {
console.log(`[${index}]`, {
text: btn.textContent?.trim().substring(0, 30),
className: btn.className,
ariaPressed: btn.getAttribute('aria-pressed'),
element: btn
});
});
return buttons;
};
window.testZzimClick = function(buttonIndex = 0) {
console.log('=== 찜 버튼 클릭 테스트 ===');
const buttons = findZzimButtons();
if (buttons.length === 0) {
console.log('❌ 클릭할 찜 버튼이 없습니다.');
return;
}
if (buttonIndex >= buttons.length) {
console.log(`❌ 인덱스 ${buttonIndex}는 범위를 벗어났습니다. (최대: ${buttons.length - 1})`);
return;
}
const button = buttons[buttonIndex];
console.log(`${buttonIndex}번 버튼 클릭 테스트:`, {
text: button.textContent?.trim(),
className: button.className,
ariaPressed: button.getAttribute('aria-pressed')
});
// 전역 클릭 함수 사용
clickZzimButton(button).then(success => {
console.log('클릭 결과:', success ? '✅ 성공' : '❌ 실패');
// 클릭 후 상태 확인
setTimeout(() => {
const afterPressed = button.getAttribute('aria-pressed');
console.log('클릭 후 aria-pressed:', afterPressed);
}, 1000);
});
};
// 찜 버튼 상태 실시간 모니터링
window.monitorZzimButtons = function(duration = 10000) {
console.log(`=== 찜 버튼 상태 모니터링 (${duration/1000}초) ===`);
const startTime = Date.now();
const interval = setInterval(() => {
const debug = window.debugZzimButtons();
console.log(`[${new Date().toLocaleTimeString()}] 찜 가능: ${debug.availableZzimButtons}개, 찜 완료: ${debug.zzimmedButtons}`);
if (Date.now() - startTime >= duration) {
clearInterval(interval);
console.log('=== 모니터링 종료 ===');
}
}, 2000);
return interval;
};
// 자동 찜하기 상태 확인 함수
window.checkAutoZzimStatus = function() {
console.log('=== 자동 찜하기 상태 확인 ===');
console.log('현재 상태:', {
isRunning: autoZzimState.isRunning,
actualZzimCount: autoZzimState.actualZzimCount,
maxZzim: autoZzimState.maxZzim,
currentPage: autoZzimState.currentPage,
delay: autoZzimState.delay,
hasStatusDiv: !!autoZzimState.statusDiv,
startTime: autoZzimState.startTime ? new Date(autoZzimState.startTime).toLocaleTimeString() : null
});
return autoZzimState;
};
// 자동 찜하기 강제 중단 함수
window.stopAutoZzim = function() {
console.log('=== 자동 찜하기 강제 중단 ===');
if (autoZzimState.isRunning) {
autoZzimState.isRunning = false;
console.log('✅ 자동 찜하기가 중단되었습니다.');
if (autoZzimState.statusDiv) {
updateZzimStatus(autoZzimState.statusDiv, `수동 중단됨 (${autoZzimState.actualZzimCount}개 찜함)`);
// 3초 후 상태 UI 제거
setTimeout(() => {
if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
autoZzimState.statusDiv = null;
}
}, 3000);
}
} else {
console.log('❌ 현재 실행 중인 자동 찜하기가 없습니다.');
}
};
// 수동 찜하기 테스트 함수
window.testManualZzim = function(maxCount = 5) {
console.log(`=== 수동 찜하기 테스트 (최대 ${maxCount}개) ===`);
if (autoZzimState.isRunning) {
console.log('❌ 자동 찜하기가 실행 중입니다. 먼저 중단해주세요.');
return;
}
const buttons = findZzimButtons();
if (buttons.length === 0) {
console.log('❌ 찜할 수 있는 버튼이 없습니다.');
return;
}
console.log(`찾은 찜 버튼: ${buttons.length}`);
let count = 0;
const testCount = Math.min(maxCount, buttons.length);
const testNext = async () => {
if (count >= testCount) {
console.log(`✅ 수동 찜하기 테스트 완료: ${count}개 시도`);
return;
}
const button = buttons[count];
console.log(`[${count + 1}/${testCount}] 찜 버튼 클릭 테스트:`, {
text: button.textContent?.trim().substring(0, 30),
className: button.className.substring(0, 50)
});
const success = await clickZzimButton(button);
console.log(`결과: ${success ? '✅ 성공' : '❌ 실패'}`);
count++;
// 1초 대기 후 다음 버튼
setTimeout(testNext, 1000);
};
testNext();
};
// 페이지 로드 시 자동 디버깅 (개발 중에만)
if (window.location.href.includes('smartstore.naver.com') && window.location.search.includes('debug=true')) {
setTimeout(() => {
console.log('🔍 디버그 모드: 자동 찜 버튼 분석 시작');
window.debugZzimButtons();
window.debugPagination();
}, 2000);
}
// 네이버 스마트스토어 URL 구조 분석 함수
function parseNaverSmartStoreUrl(url) {
try {
const urlObj = new URL(url);
// 네이버 스마트스토어 도메인 확인
if (!urlObj.hostname.includes('smartstore.naver.com')) {
console.log('[AutoZzim] 네이버 스마트스토어 도메인이 아님:', urlObj.hostname);
return null;
}
// 경로 분석: /storename/... 형태
const pathParts = urlObj.pathname.split('/').filter(part => part);
console.log('[AutoZzim] 경로 분석:', pathParts);
if (pathParts.length === 0) {
console.log('[AutoZzim] 경로가 없음');
return null;
}
// 첫 번째 경로가 스토어명
const storeName = pathParts[0];
const currentSection = pathParts[1] || 'main';
// 상품 목록 페이지인지 확인
const productListSections = ['category', 'best', 'new', 'sale'];
const isProductListPage = productListSections.includes(currentSection);
console.log('[AutoZzim] URL 분석 결과:', {
storeName: storeName,
currentSection: currentSection,
isProductListPage: isProductListPage,
fullPath: urlObj.pathname
});
return {
storeName: storeName,
currentSection: currentSection,
isProductListPage: isProductListPage,
origin: urlObj.origin,
searchParams: urlObj.searchParams
};
} catch (error) {
console.error('[AutoZzim] URL 분석 오류:', error);
return null;
}
}
// 올바른 상품 목록 페이지 URL 생성 함수
function generateCorrectProductListUrl(storeName, origin, autoZzim = false, maxZzim = 50) {
const baseUrl = `${origin}/${storeName}/category/ALL`;
const params = new URLSearchParams();
params.set('cp', '1');
if (autoZzim) {
params.set('auto_zzim', 'true');
params.set('max_zzim', maxZzim.toString());
}
return `${baseUrl}?${params.toString()}`;
}
// 키보드 이벤트 리스너 추가
function addKeyboardListeners(target = document) {
target.addEventListener('keydown', function(e) {
// Ctrl+Shift+F1: 지재권검색
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText && selectedText.trim()) {
console.log('[Content Script] 지재권검색 단축키 실행:', selectedText);
chrome.runtime.sendMessage({
action: 'searchTrademark',
text: selectedText.trim()
}, (response) => {
if (response && response.success) {
console.log('[Content Script] 지재권검색 성공');
} else {
console.error('[Content Script] 지재권검색 실패:', response?.error);
}
});
} else {
alert('검색할 텍스트를 선택해주세요.');
}
}
// Ctrl+Shift+F2: 멀티번역
else if (e.ctrlKey && e.shiftKey && e.key === 'E') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText && selectedText.trim()) {
console.log('[Content Script] 멀티번역 단축키 실행:', selectedText);
chrome.runtime.sendMessage({
action: 'translateText',
text: selectedText.trim()
}, (response) => {
if (response && response.success) {
console.log('[Content Script] 멀티번역 성공');
} else {
console.error('[Content Script] 멀티번역 실패:', response?.error);
}
});
} else {
alert('번역할 텍스트를 선택해주세요.');
}
}
// Ctrl+Shift+Z: 한중번역
else if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText && selectedText.trim()) {
console.log('[Content Script] 한중번역 단축키 실행:', selectedText);
chrome.runtime.sendMessage({
action: 'handleKoreanToChinese',
text: selectedText.trim()
}, (response) => {
if (response && response.success) {
console.log('[Content Script] 한중번역 성공');
} else {
console.error('[Content Script] 한중번역 실패:', response?.error);
}
});
} else {
alert('번역할 텍스트를 선택해주세요.');
}
}
// Ctrl+Shift+K: 직번역
else if (e.ctrlKey && e.shiftKey && e.key === 'K') {
e.preventDefault();
e.stopPropagation();
const selectedText = getSelectedTextAdvanced();
if (selectedText && selectedText.trim()) {
console.log('[Content Script] 직번역 단축키 실행:', selectedText);
chrome.runtime.sendMessage({
action: 'handleDirectTranslation',
text: selectedText.trim()
}, (response) => {
if (response && response.success) {
console.log('[Content Script] 직번역 성공');
} else {
console.error('[Content Script] 직번역 실패:', response?.error);
}
});
} else {
alert('번역할 텍스트를 선택해주세요.');
}
}
}, true); // useCapture: true로 설정하여 iframe에서도 작동하도록 함
}
// 환경 감지 및 이벤트 리스너 설정
detectEnvironment();
// iframe 환경에서도 이벤트 리스너 설정
if (isIframeEnvironment) {
addKeyboardListeners(window.parent.document);
}
// iframe 이벤트 설정
setTimeout(setupIframeEventListeners, 1000);
// 동적으로 추가되는 iframe 감지
const mainObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'IFRAME') {
setTimeout(() => setupIframeEventListeners(), 500);
} else if (node.querySelectorAll && node.querySelectorAll('iframe').length > 0) {
setTimeout(() => setupIframeEventListeners(), 500);
}
}
});
});
});
mainObserver.observe(document.body, {
childList: true,
subtree: true
});
// 클릭 이벤트 모니터링 함수 추가
window.monitorClicks = function(duration = 30000) {
console.log(`=== 클릭 이벤트 모니터링 시작 (${duration/1000}초) ===`);
const startTime = Date.now();
const clickHandler = (e) => {
const target = e.target;
const currentTime = Date.now();
if (currentTime - startTime > duration) {
document.removeEventListener('click', clickHandler, true);
console.log('=== 클릭 이벤트 모니터링 종료 ===');
return;
}
console.log('[클릭 모니터링] 클릭 이벤트 감지:', {
tagName: target.tagName,
className: target.className,
id: target.id,
textContent: target.textContent?.trim().substring(0, 50),
ariaLabel: target.getAttribute('aria-label'),
dataTestId: target.getAttribute('data-testid'),
isZzimButton: target.className.includes('zzim_button'),
ariaPressed: target.getAttribute('aria-pressed'),
position: {
x: e.clientX,
y: e.clientY
},
timestamp: new Date().toLocaleTimeString()
});
// 판매자 정보나 팝업 관련 클릭 감지
const isSellerRelated = target.textContent?.includes('판매자') ||
target.textContent?.includes('상점') ||
target.textContent?.includes('업체') ||
target.className.includes('seller') ||
target.className.includes('shop');
if (isSellerRelated) {
console.warn('[클릭 모니터링] ⚠️ 판매자 관련 요소 클릭 감지!', {
element: target,
text: target.textContent?.trim()
});
}
// 팝업이나 모달 관련 클릭 감지
const isPopupRelated = target.className.includes('popup') ||
target.className.includes('modal') ||
target.className.includes('dialog') ||
target.closest('.popup') ||
target.closest('.modal') ||
target.closest('.dialog');
if (isPopupRelated) {
console.warn('[클릭 모니터링] ⚠️ 팝업/모달 관련 요소 클릭 감지!', {
element: target,
text: target.textContent?.trim()
});
}
};
document.addEventListener('click', clickHandler, true);
return () => {
document.removeEventListener('click', clickHandler, true);
console.log('=== 클릭 이벤트 모니터링 수동 종료 ===');
};
};
// 요소 겹침 확인 함수
window.checkElementOverlap = function(selector = 'div#CategoryProducts button.zzim_button') {
console.log(`=== 요소 겹침 확인: ${selector} ===`);
const elements = document.querySelectorAll(selector);
console.log(`찾은 요소: ${elements.length}`);
elements.forEach((element, index) => {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
const isCorrect = elementAtPoint === element || element.contains(elementAtPoint);
console.log(`[${index + 1}] 요소 겹침 확인:`, {
element: element,
text: element.textContent?.trim().substring(0, 30),
className: element.className,
rect: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
centerX: Math.round(centerX),
centerY: Math.round(centerY)
},
elementAtPoint: elementAtPoint,
elementAtPointTag: elementAtPoint?.tagName,
elementAtPointClass: elementAtPoint?.className,
elementAtPointText: elementAtPoint?.textContent?.trim().substring(0, 30),
isCorrect: isCorrect,
warning: !isCorrect ? '⚠️ 다른 요소가 겹쳐있음!' : '✅ 정상'
});
});
return elements;
};
// 안전한 찜하기 테스트 함수
window.testSafeZzimClick = function(buttonIndex = 0) {
console.log('=== 안전한 찜 버튼 클릭 테스트 ===');
// 클릭 모니터링 시작
const stopMonitoring = window.monitorClicks(10000);
const buttons = findZzimButtons();
if (buttons.length === 0) {
console.log('❌ 클릭할 찜 버튼이 없습니다.');
stopMonitoring();
return;
}
if (buttonIndex >= buttons.length) {
console.log(`❌ 인덱스 ${buttonIndex}는 범위를 벗어났습니다. (최대: ${buttons.length - 1})`);
stopMonitoring();
return;
}
const button = buttons[buttonIndex];
console.log(`${buttonIndex}번 버튼 안전 클릭 테스트:`, {
text: button.textContent?.trim(),
className: button.className,
ariaPressed: button.getAttribute('aria-pressed')
});
// 요소 겹침 확인
const rect = button.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
console.log('클릭 전 요소 겹침 확인:', {
targetButton: button,
elementAtPoint: elementAtPoint,
isCorrect: elementAtPoint === button || button.contains(elementAtPoint)
});
// 전역 클릭 함수 사용
clickZzimButton(button).then(success => {
console.log('클릭 결과:', success ? '✅ 성공' : '❌ 실패');
setTimeout(() => {
stopMonitoring();
const afterPressed = button.getAttribute('aria-pressed');
console.log('클릭 후 aria-pressed:', afterPressed);
}, 2000);
});
};