3001 lines
106 KiB
JavaScript
3001 lines
106 KiB
JavaScript
// 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;"> 영문: ${item.asignProductNameEn || ""}</p>`;
|
||
html += `<p style="margin-left: 20px; margin: 4px 0;"> 유사군코드: ${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);
|
||
});
|
||
};
|
||
|