From 9f81e5fb9e6a0c32675b519b11c4fc266fe68e2c Mon Sep 17 00:00:00 2001 From: Envy_PC Date: Sat, 19 Jul 2025 22:39:30 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=84=A0=ED=83=9D=20=EA=B0=90?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0,=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=B0=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B0=8F=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20UI=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 8 +- wrmc_ext/background.js | 1373 ++++++++++++++++++++++++- wrmc_ext/content.js | 2152 ++++++++++++++++++++++++++++++++++++++- wrmc_ext/manifest.json | 12 +- wrmc_ext/manual.html | 3 +- wrmc_ext/popup.html | 3 +- wrmc_ext/settings.html | 14 +- wrmc_ext/settings.js | 44 +- wrmc_ext/zzim.html | 526 +++++++++- wrmc_ext/zzim.js | 2173 ++++++++++++++++++++++++++++++++++------ 10 files changed, 5953 insertions(+), 355 deletions(-) diff --git a/main.py b/main.py index ff5fb5f..b3292eb 100644 --- a/main.py +++ b/main.py @@ -131,9 +131,9 @@ class ExtensionInstaller(QWidget): guide = QLabel( "설치 안내:
" - "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
" - "2. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
" - "3. 설치 후 브라우저별 단계별 안내를 참고하세요.
") + # "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
" + "1. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
" + "2. 설치 후 브라우저별 단계별 안내를 참고하세요.
") guide.setWordWrap(True) guide.setStyleSheet("margin-bottom:12px;") layout.addWidget(guide) @@ -256,7 +256,7 @@ class ExtensionInstaller(QWidget): row2.setTextFormat(Qt.RichText) row2.setWordWrap(True) # 3번 압축해제된 확장프로그램 로드 - row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 버튼 클릭") + row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 또는 [압축해제된 확장앱 설치] 버튼 클릭") row3.setTextFormat(Qt.RichText) row3.setWordWrap(True) # 4번 폴더 선택 및 복사 diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js index 9c53e72..45bd5f4 100644 --- a/wrmc_ext/background.js +++ b/wrmc_ext/background.js @@ -4,7 +4,7 @@ chrome.runtime.onInstalled.addListener(() => { // 컨텍스트 메뉴를 개별적으로 생성 (단축키 포함) chrome.contextMenus.create({ id: "searchTrademark", - title: "지재권 검색 (Ctrl+Shift+S)", + title: "지재권검색 (Ctrl+Shift+S)", contexts: ["selection"] }); @@ -16,8 +16,11 @@ chrome.runtime.onInstalled.addListener(() => { chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); - // 새 어록 감지 알람 생성 (1분마다) - chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); + // 새 어록 감지 알람 생성 (5분마다) + chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 }); + + // 1시간마다 자동 품앗이 찜 알람 + chrome.alarms.create("autoMutualZzim", { periodInMinutes: 60 }); // 초기 마지막 확인 시간 설정 chrome.storage.local.set({ lastSayingsCheck: Date.now() }); @@ -35,21 +38,226 @@ chrome.commands.onCommand.addListener(async (command) => { return; } - // 선택된 텍스트 가져오기 + // 선택된 텍스트 가져오기 (개선된 방식) const results = await chrome.scripting.executeScript({ target: { tabId: tab.id }, function: () => { - return window.getSelection().toString().trim(); + // 다양한 방식으로 선택된 텍스트 감지 + function getSelectedTextAdvanced() { + let selectedText = ''; + console.log('[텍스트선택] 선택된 텍스트 감지 시작'); + + // 1. 기본 window.getSelection() 시도 + const selection = window.getSelection(); + if (selection && selection.toString().trim()) { + selectedText = selection.toString().trim(); + console.log('[텍스트선택] window.getSelection()으로 감지:', selectedText); + return selectedText; + } + + // 2. document.getSelection() 시도 + if (document.getSelection) { + const docSelection = document.getSelection(); + if (docSelection && docSelection.toString().trim()) { + selectedText = docSelection.toString().trim(); + console.log('[텍스트선택] document.getSelection()으로 감지:', selectedText); + return selectedText; + } + } + + // 3. activeElement에서 선택된 텍스트 확인 (input, textarea) + const activeElement = document.activeElement; + if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { + const start = activeElement.selectionStart; + const end = activeElement.selectionEnd; + if (start !== end && start !== null && end !== null) { + selectedText = activeElement.value.substring(start, end).trim(); + console.log('[텍스트선택] activeElement에서 감지:', selectedText); + return selectedText; + } + } + + // 4. contentEditable 요소에서 선택된 텍스트 확인 + const editableElements = document.querySelectorAll('[contenteditable="true"]'); + for (const element of editableElements) { + const elementSelection = element.ownerDocument.getSelection(); + if (elementSelection && elementSelection.toString().trim()) { + selectedText = elementSelection.toString().trim(); + console.log('[텍스트선택] contentEditable에서 감지:', selectedText); + return selectedText; + } + } + + // 5. iframe 내부 확인 + const iframes = document.querySelectorAll('iframe'); + for (const iframe of iframes) { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + const iframeSelection = iframeDoc.getSelection(); + if (iframeSelection && iframeSelection.toString().trim()) { + selectedText = iframeSelection.toString().trim(); + console.log('[텍스트선택] iframe에서 감지:', selectedText); + return selectedText; + } + } catch (e) { + // 크로스 오리진 iframe은 접근 불가 + console.log('[텍스트선택] iframe 접근 제한:', e.message); + } + } + + // 6. Shadow DOM 확인 + function checkShadowDom(element) { + if (element.shadowRoot) { + const shadowSelection = element.shadowRoot.getSelection(); + if (shadowSelection && shadowSelection.toString().trim()) { + return shadowSelection.toString().trim(); + } + } + + for (const child of element.children) { + const result = checkShadowDom(child); + if (result) return result; + } + return null; + } + + const shadowResult = checkShadowDom(document.body); + if (shadowResult) { + console.log('[텍스트선택] Shadow DOM에서 감지:', shadowResult); + return shadowResult; + } + + // 7. 특정 컨테이너 내부 확인 (ice-container 등) + const containers = document.querySelectorAll('.ice-container, [class*="container"], [class*="chat"], [class*="message"], [class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], [data-testid*="message"], [role="textbox"], [contenteditable]'); + for (const container of containers) { + try { + const containerSelection = container.ownerDocument.getSelection(); + if (containerSelection && containerSelection.toString().trim()) { + // 선택 범위가 해당 컨테이너 내부인지 확인 + const range = containerSelection.getRangeAt(0); + if (range && (container.contains(range.commonAncestorContainer) || + container.contains(range.startContainer) || + container.contains(range.endContainer))) { + selectedText = containerSelection.toString().trim(); + console.log('[텍스트선택] 특정 컨테이너에서 감지:', selectedText); + return selectedText; + } + } + } catch (e) { + console.log('[텍스트선택] 컨테이너 확인 오류:', e.message); + } + } + + // 8. 모든 요소에서 선택 상태 확인 (최후의 수단) + const allElements = document.querySelectorAll('*'); + for (const element of allElements) { + try { + // 요소가 포커스되어 있고 선택된 텍스트가 있는지 확인 + if (element === document.activeElement || element.contains(document.activeElement)) { + const computedStyle = window.getComputedStyle(element); + if (computedStyle.userSelect !== 'none') { + const elementText = element.textContent || element.innerText; + if (elementText && elementText.trim()) { + // 마우스로 선택된 영역이 이 요소 내부에 있는지 확인 + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + if (element.contains(range.commonAncestorContainer)) { + selectedText = selection.toString().trim(); + if (selectedText) { + console.log('[텍스트선택] 활성 요소에서 감지:', selectedText); + return selectedText; + } + } + } + } + } + } + } catch (e) { + // 무시 + } + } + + // 9. 클립보드에서 최근 복사된 텍스트 확인 (사용자가 Ctrl+C를 눌렀을 가능성) + try { + // 참고: 보안상의 이유로 클립보드 읽기는 제한적임 + console.log('[텍스트선택] 클립보드 확인은 보안 제약으로 제한됨'); + } catch (e) { + console.log('[텍스트선택] 클립보드 접근 불가:', e.message); + } + + console.log('[텍스트선택] 선택된 텍스트를 찾을 수 없음'); + return ''; + } + + return getSelectedTextAdvanced(); } }); const selectedText = results[0]?.result; if (!selectedText) { + // 클립보드에서 텍스트 가져오기 시도 + try { + const clipboardText = await navigator.clipboard.readText(); + if (clipboardText && clipboardText.trim() && clipboardText.length < 1000) { + console.log('[단축키] 클립보드에서 텍스트 사용:', clipboardText.substring(0, 50)); + + // 클립보드 텍스트로 처리 계속 + await ensureContentScriptReady(tab.id); + + if (command === 'trademark-search') { + try { + await chrome.tabs.sendMessage(tab.id, { + action: "showLoading", + message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 지재권 검색 중... (클립보드 사용)` + }); + } catch (loadingError) { + console.log('[단축키-지재권검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message); + } + + await handleTrademarkSearch(clipboardText.trim(), tab); + + try { + await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" }); + } catch (e) { + console.log('[단축키-지재권검색] 로딩 인디케이터 제거 실패 (무시):', e.message); + } + + } else if (command === 'multi-translate') { + try { + await chrome.tabs.sendMessage(tab.id, { + action: "showLoading", + message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 번역 중... (클립보드 사용)` + }); + } catch (loadingError) { + console.log('[단축키-멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message); + } + + await handleMultiTranslate({ selectionText: clipboardText.trim() }); + + try { + await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" }); + } catch (e) { + console.log('[단축키-멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message); + } + + } else if (command === 'korean-to-chinese') { + await handleKoreanToChinese(clipboardText.trim(), tab); + } else if (command === 'direct-translate') { + await handleDirectTranslation(clipboardText.trim(), tab); + } + + return; // 클립보드 처리 완료 + } + } catch (clipboardError) { + console.log('[단축키] 클립보드 읽기 실패:', clipboardError.message); + } + chrome.notifications.create({ type: 'basic', iconUrl: 'icon.png', title: '텍스트 선택 필요', - message: '먼저 텍스트를 선택해주세요.' + message: 'AliWangWang 등의 앱에서는 텍스트를 정확히 드래그 선택 후 단축키를 눌러주세요. 선택이 안 되면 Ctrl+C로 복사 후 다시 시도해보세요.' }); return; } @@ -100,6 +308,8 @@ chrome.commands.onCommand.addListener(async (command) => { } else if (command === 'korean-to-chinese') { await handleKoreanToChinese(selectedText, tab); + } else if (command === 'direct-translate') { + await handleDirectTranslation(selectedText, tab); } } catch (error) { @@ -118,6 +328,9 @@ chrome.alarms.onAlarm.addListener((alarm) => { console.log("[background.js] 서비스 워커 유지 알람 실행됨"); } else if (alarm.name === "checkNewSayings") { checkForNewSayings(); + } else if (alarm.name === "autoMutualZzim") { + console.log("[background.js] autoMutualZzim 주기 실행"); + chrome.runtime.sendMessage({ action: "autoMutualZzim" }); } }); @@ -139,10 +352,10 @@ async function checkForNewSayings() { lastSayingsCheck = result.lastSayingsCheck; } catch (storageError) { console.error("[background.js] 스토리지 접근 오류:", storageError); - lastSayingsCheck = Date.now() - 60000; // 기본값: 1분 전 + lastSayingsCheck = Date.now() - 300000; // 기본값: 1분 전 } - const lastCheckTime = lastSayingsCheck || Date.now() - 60000; + const lastCheckTime = lastSayingsCheck || Date.now() - 300000; // 액세스 토큰 가져오기 (안전한 방식) let access_token; @@ -1762,6 +1975,7 @@ async function handleKoreanToChinese(selectedText, tab) { if (!selectedText) { chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '텍스트 선택 필요', message: '번역할 텍스트를 먼저 선택해주세요.' }); @@ -1804,6 +2018,7 @@ async function handleKoreanToChinese(selectedText, tab) { // 한국어도 중국어도 아닌 경우 chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '지원하지 않는 언어', message: '한국어 또는 중국어 텍스트를 선택해주세요.' }); @@ -1811,14 +2026,29 @@ async function handleKoreanToChinese(selectedText, tab) { } // 선택된 텍스트를 번역된 텍스트로 대체 - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - function: replaceSelectedText, - args: [translatedText] - }); + console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText); + + try { + const result = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + function: replaceSelectedText, + args: [translatedText] + }); + + console.log('[background.js] replaceSelectedText 실행 결과:', result); + + if (result && result[0] && result[0].result !== undefined) { + console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result); + } else { + console.log('[background.js] 스크립트 실행 완료, 반환값 없음'); + } + } catch (scriptError) { + console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError); + } chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '번역 완료', message: `${direction}로 변환되었습니다.` }); @@ -1827,6 +2057,7 @@ async function handleKoreanToChinese(selectedText, tab) { console.error('[background.js] 한국어↔중국어 번역 중 오류:', error); chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '번역 오류', message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' }); @@ -1835,31 +2066,363 @@ async function handleKoreanToChinese(selectedText, tab) { // 범용 번역 함수 async function translateText(text, sourceLang, targetLang) { - const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`); + console.log(`[translateText] 번역 시작: ${sourceLang} -> ${targetLang}, 텍스트: ${text}`); - if (!response.ok) { - throw new Error('Google 번역 요청 실패'); + // 1. Google 번역 시도 + try { + const googleUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`; + console.log('[translateText] Google 번역 시도:', googleUrl); + + const response = await fetch(googleUrl, { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + }); + + if (response.ok) { + const data = await response.json(); + + if (data && data[0] && data[0][0] && data[0][0][0]) { + const translatedText = data[0][0][0]; + console.log('[translateText] Google 번역 성공:', translatedText); + return translatedText; + } + } + + console.log('[translateText] Google 번역 응답 형식 오류'); + } catch (error) { + console.error('[translateText] Google 번역 실패:', error); } - const data = await response.json(); - - if (!data || !data[0] || !data[0][0] || !data[0][0][0]) { - throw new Error('Google 번역 응답 형식 오류'); + // 2. MyMemory 번역 시도 + try { + console.log('[translateText] MyMemory 번역 시도'); + const myMemoryUrl = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`; + + const response = await fetch(myMemoryUrl); + + if (response.ok) { + const data = await response.json(); + + if (data && data.responseData && data.responseData.translatedText) { + const translatedText = data.responseData.translatedText; + console.log('[translateText] MyMemory 번역 성공:', translatedText); + return translatedText; + } + } + + console.log('[translateText] MyMemory 번역 응답 형식 오류'); + } catch (error) { + console.error('[translateText] MyMemory 번역 실패:', error); } - return data[0][0][0]; + // 3. 간단한 사전 기반 번역 (한중/중한만) + if ((sourceLang === 'ko' && targetLang === 'zh') || (sourceLang === 'zh' && targetLang === 'ko')) { + try { + console.log('[translateText] 사전 기반 번역 시도'); + const dictResult = await simpleDictionaryTranslate(text, sourceLang, targetLang); + if (dictResult) { + console.log('[translateText] 사전 기반 번역 성공:', dictResult); + return dictResult; + } + } catch (error) { + console.error('[translateText] 사전 기반 번역 실패:', error); + } + } + + // 4. 모든 번역 실패 시 원본 텍스트 반환 + console.log('[translateText] 모든 번역 방법 실패, 원본 텍스트 반환'); + throw new Error('모든 번역 엔진에서 번역에 실패했습니다'); +} + +// 간단한 사전 기반 번역 함수 +async function simpleDictionaryTranslate(text, sourceLang, targetLang) { + // 기본적인 한중/중한 단어 사전 + const koreanToChinese = { + '안녕하세요': '你好', + '감사합니다': '谢谢', + '죄송합니다': '对不起', + '네': '是', + '아니요': '不是', + '좋습니다': '好的', + '플래그십': '旗舰', + '스토어': '店', + '상점': '商店', + '가게': '店铺' + }; + + const chineseToKorean = { + '你好': '안녕하세요', + '谢谢': '감사합니다', + '对不起': '죄송합니다', + '是': '네', + '不是': '아니요', + '好的': '좋습니다', + '旗舰': '플래그십', + '店': '스토어', + '商店': '상점', + '店铺': '가게', + '旗舰店': '플래그십 스토어' + }; + + if (sourceLang === 'ko' && targetLang === 'zh') { + return koreanToChinese[text.trim()] || null; + } else if (sourceLang === 'zh' && targetLang === 'ko') { + return chineseToKorean[text.trim()] || null; + } + + return null; } // 선택된 텍스트를 새로운 텍스트로 대체하는 함수 (content script에서 실행) function replaceSelectedText(newText) { + console.log('[replaceSelectedText] 텍스트 대체 시작:', newText); + console.log('[replaceSelectedText] 현재 URL:', window.location.href); + console.log('[replaceSelectedText] activeElement:', document.activeElement); + const selection = window.getSelection(); + const activeElement = document.activeElement; + let textReplaced = false; + + // 선택된 텍스트의 위치 정보 저장 + let selectionRect = null; if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); + selectionRect = range.getBoundingClientRect(); + } + + // 알리왕왕 특화 처리 - 더 광범위한 선택자 사용 + if (window.location.href.includes('aliexpress') || window.location.href.includes('1688') || + document.querySelector('.ice-container') || document.querySelector('[class*="aliwangwang"]') || + window.location.href.includes('alibaba') || document.querySelector('[class*="im-"]')) { + console.log('[replaceSelectedText] 알리왕왕/알리바바 환경 감지됨'); - // 선택된 텍스트가 input이나 textarea인지 확인 - const activeElement = document.activeElement; + // 더 광범위한 입력창 선택자 + const inputSelectors = [ + // 기본 입력 요소 + 'textarea', 'input[type="text"]', 'input:not([type])', + // contentEditable 요소 + '[contenteditable="true"]', '[contenteditable=""]', 'div[contenteditable]', + // 알리왕왕 특화 선택자 + '.ice-container textarea', '.ice-container input', '.ice-container [contenteditable]', + '.chat-input textarea', '.chat-input input', '.chat-input [contenteditable]', + '[class*="input"] textarea', '[class*="input"] input', '[class*="input"] [contenteditable]', + '[class*="chat"] textarea', '[class*="chat"] input', '[class*="chat"] [contenteditable]', + '[class*="message"] textarea', '[class*="message"] input', '[class*="message"] [contenteditable]', + '[class*="editor"] textarea', '[class*="editor"] input', '[class*="editor"] [contenteditable]', + // 역할 기반 선택자 + 'div[role="textbox"]', '[role="textbox"]', '[role="combobox"]', + // 플레이스홀더 기반 + '[placeholder*="메시지"]', '[placeholder*="message"]', '[placeholder*="输入"]', '[placeholder*="请输入"]', + // 클래스명 기반 + '.editor', '.input-box', '.text-input', '.message-input', '.chat-editor' + ]; + + const allInputs = document.querySelectorAll(inputSelectors.join(',')); + console.log('[replaceSelectedText] 찾은 모든 입력 요소 수:', allInputs.length); + + // 각 입력 요소 상세 검사 + for (let i = 0; i < allInputs.length; i++) { + const input = allInputs[i]; + const rect = input.getBoundingClientRect(); + const isVisible = rect.width > 0 && rect.height > 0; + const isFocused = input.matches(':focus') || input === activeElement; + const hasText = input.value || input.textContent || input.innerText; + + console.log(`[replaceSelectedText] 입력창 ${i+1}:`, { + tagName: input.tagName, + className: input.className, + id: input.id, + isVisible: isVisible, + isFocused: isFocused, + hasText: !!hasText, + placeholder: input.placeholder, + contentEditable: input.contentEditable + }); + + // 활성화된 또는 보이는 입력창에 텍스트 삽입 시도 + if (isVisible && (isFocused || rect.width > 100)) { + console.log(`[replaceSelectedText] 입력창 ${i+1} 처리 시도`); + + if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') { + try { + // 포커스 설정 + input.focus(); + input.click(); + + // 기존 값 백업 + const originalValue = input.value || ''; + console.log('[replaceSelectedText] 기존 값:', originalValue); + + // 선택 영역 확인 + const start = input.selectionStart || 0; + const end = input.selectionEnd || originalValue.length; + + // 텍스트 대체 - 여러 방법 시도 + + // 방법 1: 직접 value 설정 + if (start !== end && start < end) { + // 선택된 텍스트 대체 + input.value = originalValue.substring(0, start) + newText + originalValue.substring(end); + input.selectionStart = start; + input.selectionEnd = start + newText.length; + } else { + // 전체 텍스트 대체 또는 추가 + if (originalValue.trim()) { + input.value = newText; // 기존 텍스트를 새 텍스트로 완전 대체 + } else { + input.value = newText; // 빈 입력창에 새 텍스트 입력 + } + input.selectionStart = input.selectionEnd = newText.length; + } + + console.log('[replaceSelectedText] 새로운 값:', input.value); + + // 방법 2: execCommand 시도 (폴백) + if (input.value !== newText && !input.value.includes(newText)) { + input.select(); + document.execCommand('insertText', false, newText); + } + + // 다양한 이벤트 발생 + const events = [ + 'focus', 'input', 'change', 'keydown', 'keyup', 'keypress', + 'textInput', 'compositionstart', 'compositionend', 'blur' + ]; + + events.forEach(eventType => { + try { + let event; + if (eventType.includes('key')) { + event = new KeyboardEvent(eventType, { + key: 'a', + code: 'KeyA', + bubbles: true, + cancelable: true + }); + } else { + event = new Event(eventType, { + bubbles: true, + cancelable: true + }); + } + input.dispatchEvent(event); + } catch (e) { + console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e); + } + }); + + // React/Vue 등의 프레임워크를 위한 특별 처리 + if (input._valueTracker) { + input._valueTracker.setValue(originalValue); + } + + // 강제로 다시 포커스 + setTimeout(() => { + input.focus(); + input.selectionStart = input.selectionEnd = input.value.length; + }, 100); + + // 텍스트가 실제로 변경되었는지 확인 + if (input.value.includes(newText)) { + textReplaced = true; + console.log('[replaceSelectedText] INPUT/TEXTAREA 처리 완료'); + return true; + } + + } catch (error) { + console.error('[replaceSelectedText] INPUT/TEXTAREA 처리 중 오류:', error); + } + + } else if (input.contentEditable === 'true' || input.contentEditable === '' || + input.getAttribute('contenteditable') === 'true') { + try { + console.log('[replaceSelectedText] contentEditable 요소 처리'); + + // 포커스 설정 + input.focus(); + input.click(); + + // 기존 내용 백업 + const originalContent = input.textContent || input.innerHTML || ''; + console.log('[replaceSelectedText] 기존 내용:', originalContent); + + // 방법 1: 선택 후 대체 + const sel = window.getSelection(); + + // 전체 내용 선택 + const range = document.createRange(); + range.selectNodeContents(input); + sel.removeAllRanges(); + sel.addRange(range); + + // execCommand로 텍스트 삽입 + if (document.execCommand('insertText', false, newText)) { + console.log('[replaceSelectedText] execCommand insertText 성공'); + } else { + // 방법 2: 직접 DOM 조작 + input.innerHTML = ''; + input.textContent = newText; + + // 커서를 끝으로 이동 + const newRange = document.createRange(); + newRange.selectNodeContents(input); + newRange.collapse(false); + sel.removeAllRanges(); + sel.addRange(newRange); + } + + console.log('[replaceSelectedText] 새로운 내용:', input.textContent || input.innerHTML); + + // 이벤트 발생 + const events = [ + 'focus', 'input', 'textInput', 'keyup', 'compositionend', + 'DOMCharacterDataModified', 'DOMSubtreeModified', 'blur' + ]; + + events.forEach(eventType => { + try { + const event = new Event(eventType, { bubbles: true, cancelable: true }); + input.dispatchEvent(event); + } catch (e) { + console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e); + } + }); + + // 강제로 다시 포커스 + setTimeout(() => { + input.focus(); + const finalRange = document.createRange(); + finalRange.selectNodeContents(input); + finalRange.collapse(false); + const finalSel = window.getSelection(); + finalSel.removeAllRanges(); + finalSel.addRange(finalRange); + }, 100); + + // 텍스트가 실제로 변경되었는지 확인 + if (input.textContent.includes(newText) || input.innerHTML.includes(newText)) { + textReplaced = true; + console.log('[replaceSelectedText] contentEditable 처리 완료'); + return true; + } + + } catch (error) { + console.error('[replaceSelectedText] contentEditable 처리 중 오류:', error); + } + } + } + } + } + + // 일반 사이트 처리 (기존 로직) + if (!textReplaced) { + console.log('[replaceSelectedText] 일반 사이트 처리 시작'); + + // 1. input/textarea 처리 if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { - // input/textarea의 경우 + console.log('[replaceSelectedText] INPUT/TEXTAREA 요소 처리'); const start = activeElement.selectionStart; const end = activeElement.selectionEnd; const value = activeElement.value; @@ -1868,21 +2431,267 @@ function replaceSelectedText(newText) { activeElement.selectionStart = start; activeElement.selectionEnd = start + newText.length; - // 변경 이벤트 트리거 - activeElement.dispatchEvent(new Event('input', { bubbles: true })); - activeElement.dispatchEvent(new Event('change', { bubbles: true })); - } else { - // 일반 텍스트 노드의 경우 - range.deleteContents(); - range.insertNode(document.createTextNode(newText)); + // 다양한 이벤트 트리거 + ['input', 'change', 'keyup', 'blur'].forEach(eventType => { + activeElement.dispatchEvent(new Event(eventType, { bubbles: true })); + }); - // 새로운 텍스트 선택 - range.setStart(range.startContainer, range.startOffset - newText.length); - range.setEnd(range.startContainer, range.startOffset); - selection.removeAllRanges(); - selection.addRange(range); + if (activeElement.value.includes(newText)) { + textReplaced = true; + console.log('[replaceSelectedText] INPUT/TEXTAREA 텍스트 대체 완료'); + return true; + } + } + + // 2. contentEditable 요소 처리 + if (activeElement && activeElement.contentEditable === 'true') { + console.log('[replaceSelectedText] contentEditable 요소 처리'); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(document.createTextNode(newText)); + + // 커서 위치 조정 + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + + // contentEditable 이벤트 트리거 + ['input', 'textInput', 'keyup'].forEach(eventType => { + activeElement.dispatchEvent(new Event(eventType, { bubbles: true })); + }); + + textReplaced = true; + console.log('[replaceSelectedText] contentEditable 텍스트 대체 완료'); + return true; + } + } + + // 3. 일반 선택 영역 처리 + if (selection.rangeCount > 0) { + console.log('[replaceSelectedText] 일반 선택 영역 처리'); + const range = selection.getRangeAt(0); + + // 선택된 텍스트가 있는지 확인 + if (range.toString().trim()) { + range.deleteContents(); + range.insertNode(document.createTextNode(newText)); + + // 새로운 텍스트 선택 + const newRange = document.createRange(); + newRange.setStart(range.startContainer, range.startOffset - newText.length); + newRange.setEnd(range.startContainer, range.startOffset); + selection.removeAllRanges(); + selection.addRange(newRange); + + textReplaced = true; + console.log('[replaceSelectedText] 일반 텍스트 대체 완료'); + return true; + } } } + + // 텍스트 대체가 실패한 경우 팝업 표시 + if (!textReplaced) { + console.log('[replaceSelectedText] 텍스트 대체 실패, 팝업 표시'); + showTranslationPopup(newText, selectionRect); + + // 클립보드에도 복사 + try { + navigator.clipboard.writeText(newText).then(() => { + console.log('[replaceSelectedText] 클립보드에 복사 완료'); + }).catch(err => { + console.error('[replaceSelectedText] 클립보드 복사 실패:', err); + }); + } catch (error) { + console.error('[replaceSelectedText] 클립보드 접근 실패:', error); + } + } + + return textReplaced; +} + +// 번역 결과 팝업을 표시하는 함수 +function showTranslationPopup(translatedText, selectionRect) { + console.log('[showTranslationPopup] 팝업 표시 시작:', translatedText); + + // 기존 팝업이 있으면 제거 + const existingPopup = document.getElementById('translation-popup-extension'); + if (existingPopup) { + existingPopup.remove(); + } + + // 팝업 컨테이너 생성 + const popup = document.createElement('div'); + popup.id = 'translation-popup-extension'; + popup.style.cssText = ` + position: fixed; + z-index: 999999; + background: #ffffff; + border: 2px solid #4CAF50; + border-radius: 8px; + padding: 12px 16px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + font-family: 'Arial', 'Malgun Gothic', sans-serif; + font-size: 14px; + line-height: 1.4; + max-width: 300px; + word-wrap: break-word; + animation: fadeInScale 0.3s ease-out; + `; + + // 애니메이션 CSS 추가 + if (!document.getElementById('translation-popup-styles')) { + const style = document.createElement('style'); + style.id = 'translation-popup-styles'; + style.textContent = ` + @keyframes fadeInScale { + 0% { + opacity: 0; + transform: scale(0.8) translateY(-10px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } + } + @keyframes fadeOut { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.9); + } + } + `; + document.head.appendChild(style); + } + + // 팝업 내용 생성 + const content = document.createElement('div'); + content.innerHTML = ` +
+ 🌐 번역 결과 +
+
+ ${translatedText} +
+
+ + +
+ `; + + popup.appendChild(content); + + // 팝업 위치 계산 + let left = 50; + let top = 50; + + if (selectionRect && selectionRect.width > 0 && selectionRect.height > 0) { + // 선택된 텍스트 위치 기준으로 팝업 위치 설정 + left = selectionRect.left + selectionRect.width / 2; + top = selectionRect.top - 10; // 선택된 텍스트 위쪽에 표시 + + // 화면 경계 확인 및 조정 + const maxLeft = window.innerWidth - 320; // 팝업 너비 고려 + const maxTop = window.innerHeight - 150; // 팝업 높이 고려 + + if (left > maxLeft) left = maxLeft; + if (left < 10) left = 10; + + if (top < 10) { + // 위쪽 공간이 부족하면 선택된 텍스트 아래쪽에 표시 + top = selectionRect.bottom + 10; + } + if (top > maxTop) top = maxTop; + } else { + // 선택 영역 정보가 없으면 화면 중앙에 표시 + left = (window.innerWidth - 300) / 2; + top = (window.innerHeight - 150) / 2; + } + + popup.style.left = left + 'px'; + popup.style.top = top + 'px'; + + // 문서에 팝업 추가 + document.body.appendChild(popup); + + // 이벤트 리스너 추가 + const copyButton = popup.querySelector('#copy-translation'); + const closeButton = popup.querySelector('#close-popup'); + + copyButton.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(translatedText); + copyButton.textContent = '복사됨!'; + copyButton.style.background = '#2196F3'; + setTimeout(() => { + copyButton.textContent = '복사'; + copyButton.style.background = '#4CAF50'; + }, 1500); + } catch (err) { + console.error('클립보드 복사 실패:', err); + copyButton.textContent = '실패'; + copyButton.style.background = '#f44336'; + } + }); + + closeButton.addEventListener('click', () => { + popup.style.animation = 'fadeOut 0.2s ease-in'; + setTimeout(() => { + if (popup.parentNode) { + popup.remove(); + } + }, 200); + }); + + // 5초 후 자동으로 팝업 제거 + setTimeout(() => { + if (popup.parentNode) { + popup.style.animation = 'fadeOut 0.3s ease-in'; + setTimeout(() => { + if (popup.parentNode) { + popup.remove(); + } + }, 300); + } + }, 5000); + + // 팝업 외부 클릭 시 닫기 + document.addEventListener('click', function closeOnOutsideClick(e) { + if (!popup.contains(e.target)) { + popup.style.animation = 'fadeOut 0.2s ease-in'; + setTimeout(() => { + if (popup.parentNode) { + popup.remove(); + } + }, 200); + document.removeEventListener('click', closeOnOutsideClick); + } + }); + + console.log('[showTranslationPopup] 팝업 표시 완료'); } // ==================== 시간 알람 기능 ==================== @@ -1896,7 +2705,9 @@ class TimeAlarmManager { enabled: true, workTime: 60, // 분 restTime: 5, // 분 - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 // 포모도로 현재 사이클(0~3) }; this.startTime = null; } @@ -1951,9 +2762,10 @@ class TimeAlarmManager { } this.startTime = Date.now(); - const workTimeMs = this.settings.workTime * 60 * 1000; + const workMin = this.settings.pomodoro ? 35 : this.settings.workTime; + const workTimeMs = workMin * 60 * 1000; - console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}분`); + console.log(`[TimeAlarm] 작업 타이머 시작: ${workMin}분`); this.workTimer = setTimeout(() => { this.showRestModal(); @@ -1984,7 +2796,8 @@ class TimeAlarmManager { } startBreakTimer(popupId) { - const breakTimeMs = this.settings.restTime * 60 * 1000; + const breakTimeMin = this.settings.restTime; + const breakTimeMs = breakTimeMin * 60 * 1000; console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}분`); @@ -1999,6 +2812,11 @@ class TimeAlarmManager { // 작업 완료 알림 this.showWorkCompleteNotification(); + // 포모도로 모드: 사이클 증가 및 리셋 + if (this.settings.pomodoro) { + this.settings.cycle = (this.settings.cycle + 1) % 4; + } + // 다음 작업 타이머 시작 this.startWorkTimer(); @@ -2010,7 +2828,7 @@ class TimeAlarmManager { type: 'basic', iconUrl: 'icon.png', title: '휴식 시간입니다! 🧘‍♀️', - message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.` + message: `${this.settings.pomodoro ? 35 : this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.` }); } @@ -2073,7 +2891,7 @@ class TimeAlarmManager { // 남은 시간 계산 const now = Date.now(); const elapsed = now - this.startTime; - const totalWorkTime = this.settings.workTime * 60 * 1000; + const totalWorkTime = (this.settings.pomodoro ? 35 : this.settings.workTime) * 60 * 1000; const remainingTime = totalWorkTime - elapsed; if (remainingTime <= 0) { @@ -2086,7 +2904,7 @@ class TimeAlarmManager { return { isRunning: true, remainingTime: remainingTime, - workTime: this.settings.workTime, + workTime: this.settings.pomodoro ? 35 : this.settings.workTime, restTime: this.settings.restTime, startTime: this.startTime, elapsed: elapsed @@ -2216,6 +3034,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { handleAddToZzim(message, sendResponse); return true; // 비동기 응답을 위해 true 반환 } + + if (message.action === 'executeBackgroundZzim') { + handleExecuteBackgroundZzim(message, sendResponse); + return true; // 비동기 응답을 위해 true 반환 + } }); // 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거) @@ -2425,3 +3248,467 @@ function getBackendConfig() { } // 검색 결과 개선을 위한 키워드 확장 함수 + +// 백그라운드 찜하기 실행 함수 +async function handleExecuteBackgroundZzim(message, sendResponse) { + try { + console.log('[Background] 백그라운드 찜하기 시작:', message); + + const { market, zzimType, userId, accessToken } = message; + + if (!market || !market.market_url) { + throw new Error('마켓 정보가 없습니다.'); + } + + // 백그라운드 탭 생성 + const tab = await chrome.tabs.create({ + url: market.market_url, + active: false + }); + + console.log('[Background] 백그라운드 탭 생성됨:', tab.id); + + // 찜하기 스크립트 정의 + const zzimScript = function() { + return new Promise((resolve) => { + let zzimCount = 0; + const maxZzim = 50; // 최대 찜할 개수 + let isRunning = true; + + console.log('찜하기 스크립트 시작'); + + function findZzimButtons() { + // 네이버 스마트스토어의 찜 버튼 선택자들 + const selectors = [ + 'button[data-testid="wishlist-button"]:not([aria-pressed="true"])', + '.zzim_button:not(.active)', + '.wish_button:not(.active)', + 'button[class*="wish"]:not([class*="active"])', + 'button[aria-label*="찜"]:not([aria-pressed="true"])' + ]; + + let buttons = []; + for (const selector of selectors) { + buttons = document.querySelectorAll(selector); + if (buttons.length > 0) { + console.log(`찜 버튼 발견 (${selector}):`, buttons.length); + break; + } + } + + return Array.from(buttons); + } + + function clickZzimButtons() { + if (!isRunning || zzimCount >= maxZzim) { + console.log('찜하기 중단:', { isRunning, zzimCount, maxZzim }); + return false; + } + + const zzimButtons = findZzimButtons(); + console.log('찾은 찜 버튼 개수:', zzimButtons.length); + + if (zzimButtons.length === 0) { + console.log('더 이상 찜할 상품이 없습니다.'); + return false; + } + + // 최대 10개씩 찜하기 + const buttonsToClick = zzimButtons.slice(0, Math.min(10, maxZzim - zzimCount)); + + buttonsToClick.forEach((btn, index) => { + setTimeout(() => { + if (!isRunning) return; + + try { + // 버튼이 화면에 보이도록 스크롤 + btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + setTimeout(() => { + if (!isRunning) return; + + btn.click(); + zzimCount++; + console.log(`찜 버튼 클릭: ${zzimCount}개`); + + // 클릭 후 잠시 대기 + setTimeout(() => { + // 추가 확인 버튼이 있다면 클릭 + const confirmBtn = document.querySelector('button[data-testid="confirm"], .confirm_btn, button:contains("확인")'); + if (confirmBtn) { + confirmBtn.click(); + } + }, 200); + + }, 300); // 스크롤 후 클릭까지 대기 + + } catch (e) { + console.error('찜 버튼 클릭 오류:', e); + } + }, index * 800); // 0.8초 간격으로 클릭 + }); + + return buttonsToClick.length > 0; + } + + // 페이지 스크롤 및 찜하기 반복 + function scrollAndZzim() { + if (!isRunning || zzimCount >= maxZzim) { + console.log('찜하기 완료:', zzimCount); + resolve(zzimCount); + return; + } + + // 페이지 하단으로 스크롤 + window.scrollTo(0, document.body.scrollHeight); + + // 스크롤 후 잠시 대기하여 새 상품 로드 + setTimeout(() => { + if (clickZzimButtons()) { + setTimeout(scrollAndZzim, 5000); // 5초 후 다시 시도 + } else { + console.log('더 이상 찜할 상품이 없어 종료'); + resolve(zzimCount); + } + }, 2000); + } + + // 30초 후 자동 종료 + setTimeout(() => { + isRunning = false; + console.log('시간 초과로 찜하기 종료'); + resolve(zzimCount); + }, 30000); + + // 시작 + console.log('찜하기 시작'); + setTimeout(scrollAndZzim, 1000); // 페이지 로드 후 1초 대기 + }); + }; + + // 페이지 로드 완료 후 스크립트 실행 + const executeZzim = (tabId, changeInfo) => { + if (tabId === tab.id && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(executeZzim); + + console.log('[Background] 페이지 로드 완료, 찜하기 스크립트 실행'); + + chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: zzimScript + }).then((results) => { + const zzimCount = results[0]?.result || 0; + console.log('[Background] 찜하기 완료:', zzimCount); + + // 탭 닫기 + setTimeout(() => { + chrome.tabs.remove(tab.id).catch(console.error); + }, 2000); + + sendResponse({ + success: true, + zzimCount: zzimCount, + message: `${zzimCount}개 상품을 찜했습니다.` + }); + + }).catch((error) => { + console.error('[Background] 찜하기 스크립트 실행 오류:', error); + chrome.tabs.remove(tab.id).catch(console.error); + sendResponse({ + success: false, + error: '찜하기 스크립트 실행 실패: ' + error.message + }); + }); + } + }; + + chrome.tabs.onUpdated.addListener(executeZzim); + + // 타임아웃 설정 (1분) + setTimeout(() => { + chrome.tabs.onUpdated.removeListener(executeZzim); + chrome.tabs.remove(tab.id).catch(console.error); + sendResponse({ + success: false, + error: '찜하기 실행 시간 초과' + }); + }, 60000); + + } catch (error) { + console.error('[Background] 백그라운드 찜하기 오류:', error); + sendResponse({ + success: false, + error: error.message + }); + } +} + +// 메시지 리스너 추가 - content.js에서 보내는 메시지 처리 +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log('[Background] 메시지 수신:', message); + + // 지재권 검색 요청 + if (message.action === "searchTrademark") { + console.log('[Background] 지재권 검색 요청 처리:', message.keyword); + handleTrademarkSearch(message.keyword, sender.tab) + .then(() => { + sendResponse({ success: true }); + }) + .catch(error => { + console.error('[Background] 지재권 검색 처리 실패:', error); + sendResponse({ success: false, error: error.message }); + }); + return true; // 비동기 응답 + } + + // 멀티번역 요청 + if (message.action === "translateText") { + console.log('[Background] 멀티번역 요청 처리:', message.text); + handleMultiTranslate({ selectionText: message.text }) + .then(() => { + sendResponse({ success: true }); + }) + .catch(error => { + console.error('[Background] 멀티번역 처리 실패:', error); + sendResponse({ success: false, error: error.message }); + }); + return true; // 비동기 응답 + } + + // 직번역 요청 + if (message.action === "handleDirectTranslation") { + const selectedText = message.selectedText || message.text; + console.log('[Background] 직번역 요청 처리:', selectedText); + handleDirectTranslation(selectedText, sender.tab) + .then(() => { + sendResponse({ success: true }); + }) + .catch(error => { + console.error('[Background] 직번역 처리 실패:', error); + sendResponse({ success: false, error: error.message }); + }); + return true; // 비동기 응답 + } + + // 한중번역 요청 + if (message.action === "handleKoreanToChinese") { + const selectedText = message.selectedText || message.text; + console.log('[Background] 한중번역 요청 처리:', selectedText); + handleKoreanToChinese(selectedText, sender.tab) + .then(() => { + sendResponse({ success: true }); + }) + .catch(error => { + console.error('[Background] 한중번역 처리 실패:', error); + sendResponse({ success: false, error: error.message }); + }); + return true; // 비동기 응답 + } + + // 금지어 추가 요청 + if (message.action === "addBannedWord") { + handleAddBannedWord(message, sendResponse); + return true; // 비동기 응답 + } +}); + +// 직번역 처리 함수 (선택된 텍스트를 바로 번역된 텍스트로 대체) +async function handleDirectTranslation(selectedText, tab) { + if (!selectedText) { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '텍스트 선택 필요', + message: '번역할 텍스트를 먼저 선택해주세요.' + }); + return; + } + + console.log('[background.js] 직번역 요청:', selectedText); + + try { + // 언어 감지 및 번역 방향 결정 + const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(selectedText); + const isChinese = /[\u4e00-\u9fff]/.test(selectedText); + const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(selectedText.trim()); + + let translatedText; + let direction; + + if (isKorean && !isChinese) { + // 한국어 → 중국어 + translatedText = await translateText(selectedText, 'ko', 'zh'); + direction = '한국어 → 중국어'; + } else if (isChinese && !isKorean) { + // 중국어 → 한국어 + translatedText = await translateText(selectedText, 'zh', 'ko'); + direction = '중국어 → 한국어'; + } else if (isEnglish && !isKorean && !isChinese) { + // 영어 → 한국어 + translatedText = await translateText(selectedText, 'en', 'ko'); + direction = '영어 → 한국어'; + } else if (isKorean && isChinese) { + // 한국어와 중국어가 섞여있는 경우 - 한국어 비율로 판단 + const koreanChars = (selectedText.match(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g) || []).length; + const chineseChars = (selectedText.match(/[\u4e00-\u9fff]/g) || []).length; + + if (koreanChars >= chineseChars) { + // 한국어가 더 많으면 중국어로 번역 + translatedText = await translateText(selectedText, 'ko', 'zh'); + direction = '한국어 → 중국어'; + } else { + // 중국어가 더 많으면 한국어로 번역 + translatedText = await translateText(selectedText, 'zh', 'ko'); + direction = '중국어 → 한국어'; + } + } else { + // 기타 언어는 한국어로 번역 + translatedText = await translateText(selectedText, 'auto', 'ko'); + direction = '자동감지 → 한국어'; + } + + // 선택된 텍스트를 번역된 텍스트로 대체 + console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText); + + try { + const result = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + function: replaceSelectedText, + args: [translatedText] + }); + + console.log('[background.js] replaceSelectedText 실행 결과:', result); + + if (result && result[0] && result[0].result !== undefined) { + console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result); + } else { + console.log('[background.js] 스크립트 실행 완료, 반환값 없음'); + } + } catch (scriptError) { + console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError); + } + + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '직번역 완료', + message: `${direction}로 번역되어 텍스트가 대체되었습니다.` + }); + + } catch (error) { + console.error('[background.js] 직번역 중 오류:', error); + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '직번역 오류', + message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' + }); + } +} + +// ===== 메시지 리스너: 백그라운드 자동 품앗이 ===== +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message && message.action === 'autoMutualZzim') { + autoMutualZzim() + .then(() => sendResponse({ success: true })) + .catch(err => sendResponse({ success: false, error: err.message })); + return true; // async 응답 + } +}); + +// ===== 자동 품앗이 찜 메인 함수 ===== +async function autoMutualZzim() { + try { + console.log('[autoMutualZzim] 시작'); + + // 1. 기본 정보 및 설정 + const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']); + if (!access_token || !user_id) { + console.log('[autoMutualZzim] 토큰/사용자 정보 없음'); + return; + } + + const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig(); + + // 2. 내 마일리지 및 회원등급 조회 + const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=available_zzim_mile,membership_level`, { + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + if (!userRes.ok) { + console.warn('[autoMutualZzim] 사용자 조회 실패', userRes.status); + return; + } + const me = (await userRes.json())[0]; + const myAvail = me?.available_zzim_mile || 0; + const myLevel = me?.membership_level || 'basic'; + + // 2-1. 등급별 한도 확인 + const levelRes = await fetch(`${SUPABASE_URL}/rest/v1/membership_levels?level=eq.${myLevel}&select=max_zzim_mileage,mileage_per_zzim`, { + headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` } + }); + const levelConf = levelRes.ok ? (await levelRes.json())[0] : { max_zzim_mileage: 500, mileage_per_zzim: 1 }; + + if (myAvail >= levelConf.max_zzim_mileage) { + console.log('[autoMutualZzim] 마일리지 최대치 도달 – 실행 안 함'); + return; + } + + // 3. 후보 마켓 조회 (v_user_market_stats 뷰) + const candRes = await fetch(`${SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, { + headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json' } + }); + if (!candRes.ok) { + console.warn('[autoMutualZzim] 후보 조회 실패', candRes.status); + return; + } + const users = await candRes.json(); + const pool = []; + users.forEach(u => { + const markets = u.my_markets || []; + markets.forEach(m => { + if (m.is_visible !== false && m.for_mutual_zzim !== false) { + pool.push({ ...m, owner_user_id: u.user_id, owner_available_mileage: u.available_zzim_mile }); + } + }); + }); + if (pool.length === 0) { + console.log('[autoMutualZzim] 후보 마켓 없음'); + return; + } + + // 4. 랜덤 1개 선택 + const target = pool[Math.floor(Math.random() * pool.length)]; + const targetUrl = `${target.market_url.replace(/\/$/, '')}/category/ALL?cp=1&auto_zzim=true&max_zzim=50`; + + // 5. 백그라운드 찜 실행 + const msg = { + action: 'executeBackgroundZzim', + market: { ...target, target_url: targetUrl }, + zzimType: 'mutual', + userId: user_id, + accessToken: access_token, + settings: { totalDelay: 1000, latestFirst: false, backgroundMode: true } + }; + + chrome.runtime.sendMessage(msg, (res) => { + if (chrome.runtime.lastError) { + console.error('[autoMutualZzim] runtime error', chrome.runtime.lastError.message); + } else if (res && res.success) { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icon.png', + title: '🎉 자동 품앗이 완료', + message: `${target.market_nickname || '마켓'}에 찜 50개 완료!` + }); + } else { + console.warn('[autoMutualZzim] 실행 실패', res?.error); + } + }); + } catch (e) { + console.error('[autoMutualZzim] 오류', e); + } +} diff --git a/wrmc_ext/content.js b/wrmc_ext/content.js index 9699c14..0e5f15e 100644 --- a/wrmc_ext/content.js +++ b/wrmc_ext/content.js @@ -4,6 +4,8 @@ let lastContextMenuPos = null; let tooltipEl = null; let currentKeyword = null; // 현재 검색 키워드 저장 let loadingIndicator = null; // 로딩 인디케이터 요소 +let isIframeEnvironment = false; // iframe 환경 감지 +let customContextMenu = null; // 커스텀 컨텍스트 메뉴 // 마우스 위치 추적 let currentMousePos = { x: 0, y: 0 }; @@ -11,17 +13,530 @@ document.addEventListener('mousemove', (e) => { currentMousePos = { x: e.pageX, y: e.pageY }; }); -document.addEventListener("contextmenu", (e) => { - lastContextMenuPos = { x: e.pageX, y: e.pageY }; -}); +// iframe 환경 감지 +function detectEnvironment() { + try { + // 1. iframe 내부인지 확인 + isIframeEnvironment = window.self !== window.top; + + // 2. 특별한 컨테이너 확인 + const specialContainers = [ + '.ice-container', + '[class*="aliwangwang"]', + '[class*="chat-container"]', + '[class*="message-container"]', + '[data-testid*="chat"]', + '[role="application"]', + 'webview', + 'embed' + ]; + + const hasSpecialContainer = specialContainers.some(selector => { + return document.querySelector(selector) !== null; + }); + + console.log('[content.js] 환경 감지:', { + isIframe: isIframeEnvironment, + hasSpecialContainer: hasSpecialContainer, + userAgent: navigator.userAgent.includes('AliWangWang') ? 'AliWangWang' : 'Other' + }); + + return isIframeEnvironment || hasSpecialContainer; + } catch (e) { + console.log('[content.js] 환경 감지 오류:', e); + return false; + } +} -// ESC 키로 모달 닫기 +// 향상된 텍스트 선택 감지 +function getSelectedTextAdvanced() { + let selectedText = ''; + + try { + // 1. 기본 window.getSelection() 확인 + const selection = window.getSelection(); + if (selection && selection.toString().trim()) { + selectedText = selection.toString().trim(); + console.log('[content.js] 기본 선택에서 감지:', selectedText); + return selectedText; + } + + // 2. document.getSelection() 확인 + if (document.getSelection) { + const docSelection = document.getSelection(); + if (docSelection && docSelection.toString().trim()) { + selectedText = docSelection.toString().trim(); + console.log('[content.js] document 선택에서 감지:', selectedText); + return selectedText; + } + } + + // 3. 활성 요소에서 선택된 텍스트 확인 + const activeElement = document.activeElement; + if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { + const start = activeElement.selectionStart; + const end = activeElement.selectionEnd; + if (start !== end && start !== null && end !== null) { + selectedText = activeElement.value.substring(start, end).trim(); + console.log('[content.js] 입력 요소에서 감지:', selectedText); + return selectedText; + } + } + + // 4. contentEditable 요소 확인 + const editableElements = document.querySelectorAll('[contenteditable="true"]'); + for (const element of editableElements) { + try { + const elementSelection = element.ownerDocument.getSelection(); + if (elementSelection && elementSelection.toString().trim()) { + selectedText = elementSelection.toString().trim(); + console.log('[content.js] contentEditable에서 감지:', selectedText); + return selectedText; + } + } catch (e) { + continue; + } + } + + // 5. iframe 내부 확인 + const iframes = document.querySelectorAll('iframe'); + for (const iframe of iframes) { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + const iframeSelection = iframeDoc.getSelection(); + if (iframeSelection && iframeSelection.toString().trim()) { + selectedText = iframeSelection.toString().trim(); + console.log('[content.js] iframe에서 감지:', selectedText); + return selectedText; + } + } catch (e) { + // 크로스 오리진 iframe은 접근 불가 + console.log('[content.js] iframe 접근 제한:', e.message); + } + } + + // 6. 특별한 컨테이너 내부 확인 (알리왕왕, ice-container 등) + const containers = document.querySelectorAll( + '.ice-container, [class*="container"], [class*="chat"], [class*="message"], ' + + '[class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], ' + + '[data-testid*="message"], [role="textbox"], [contenteditable], [class*="aliwangwang"], ' + + '[class*="input"], [class*="text"], webview, embed' + ); + + for (const container of containers) { + try { + const containerSelection = container.ownerDocument.getSelection(); + if (containerSelection && containerSelection.toString().trim()) { + // 선택 범위가 해당 컨테이너 내부인지 확인 + const range = containerSelection.getRangeAt(0); + if (range && (container.contains(range.commonAncestorContainer) || + container.contains(range.startContainer) || + container.contains(range.endContainer))) { + selectedText = containerSelection.toString().trim(); + console.log('[content.js] 특별 컨테이너에서 감지:', selectedText); + return selectedText; + } + } + } catch (e) { + continue; + } + } + + // 7. Shadow DOM 확인 + function checkShadowDom(element) { + if (element.shadowRoot) { + try { + const shadowSelection = element.shadowRoot.getSelection(); + if (shadowSelection && shadowSelection.toString().trim()) { + return shadowSelection.toString().trim(); + } + } catch (e) { + // 무시 + } + } + + for (const child of element.children) { + const result = checkShadowDom(child); + if (result) return result; + } + return null; + } + + const shadowResult = checkShadowDom(document.body); + if (shadowResult) { + console.log('[content.js] Shadow DOM에서 감지:', shadowResult); + return shadowResult; + } + + console.log('[content.js] 선택된 텍스트를 찾을 수 없음'); + return ''; + + } catch (e) { + console.error('[content.js] 텍스트 선택 감지 오류:', e); + return ''; + } +} + +// 커스텀 컨텍스트 메뉴 생성 +function createCustomContextMenu(e) { + // 기존 커스텀 메뉴 제거 + removeCustomContextMenu(); + + const selectedText = getSelectedTextAdvanced(); + if (!selectedText) return; + + customContextMenu = document.createElement('div'); + customContextMenu.id = 'custom-context-menu'; + customContextMenu.style.cssText = ` + position: fixed; + z-index: 999999; + background: white; + border: 1px solid #ccc; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + min-width: 200px; + padding: 4px 0; + `; + + // 메뉴 항목들 + const menuItems = [ + { + text: '🔍 지재권검색(내차확장기)', + action: () => { + console.log('[CustomMenu] 지재권 검색 클릭:', selectedText); + chrome.runtime.sendMessage({ + action: "searchTrademark", + keyword: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[CustomMenu] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[CustomMenu] 지재권 검색 메시지 전송 성공:', response); + } + }); + removeCustomContextMenu(); + } + }, + { + text: '🌐 멀티번역하기(내차확장기)', + action: () => { + console.log('[CustomMenu] 멀티번역 클릭:', selectedText); + chrome.runtime.sendMessage({ + action: "translateText", + text: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[CustomMenu] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[CustomMenu] 멀티번역 메시지 전송 성공:', response); + } + }); + removeCustomContextMenu(); + } + }, + { + text: '⚡ 직번역(내차확장기)', + action: () => { + console.log('[CustomMenu] 직번역 클릭:', selectedText); + chrome.runtime.sendMessage({ + action: "handleDirectTranslation", + selectedText: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[CustomMenu] 직번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[CustomMenu] 직번역 메시지 전송 성공:', response); + } + }); + removeCustomContextMenu(); + } + }, + { + text: '📋 복사', + action: () => { + console.log('[CustomMenu] 복사 클릭:', selectedText); + navigator.clipboard.writeText(selectedText).then(() => { + console.log('[CustomMenu] 복사 성공'); + // 복사 성공 알림 (선택사항) + showTemporaryMessage('복사되었습니다!'); + }).catch(err => { + console.error('[CustomMenu] 복사 실패:', err); + // 폴백: execCommand 사용 + try { + const textArea = document.createElement('textarea'); + textArea.value = selectedText; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + console.log('[CustomMenu] 폴백 복사 성공'); + showTemporaryMessage('복사되었습니다!'); + } catch (fallbackErr) { + console.error('[CustomMenu] 폴백 복사도 실패:', fallbackErr); + } + }); + removeCustomContextMenu(); + } + } + ]; + + menuItems.forEach(item => { + const menuItem = document.createElement('div'); + menuItem.textContent = item.text; + menuItem.style.cssText = ` + padding: 8px 16px; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; + `; + + menuItem.addEventListener('mouseenter', () => { + menuItem.style.backgroundColor = '#f0f0f0'; + }); + + menuItem.addEventListener('mouseleave', () => { + menuItem.style.backgroundColor = 'transparent'; + }); + + menuItem.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + console.log('[CustomMenu] 메뉴 항목 클릭:', item.text); + item.action(); + }); + + customContextMenu.appendChild(menuItem); + }); + + document.body.appendChild(customContextMenu); + + // 위치 설정 + const rect = customContextMenu.getBoundingClientRect(); + let x = e.clientX; + let y = e.clientY; + + // 화면 경계 체크 + if (x + rect.width > window.innerWidth) { + x = window.innerWidth - rect.width - 10; + } + if (y + rect.height > window.innerHeight) { + y = window.innerHeight - rect.height - 10; + } + + customContextMenu.style.left = x + 'px'; + customContextMenu.style.top = y + 'px'; + + // 외부 클릭시 메뉴 제거 + setTimeout(() => { + const handleClickOutside = (event) => { + if (!customContextMenu.contains(event.target)) { + removeCustomContextMenu(); + document.removeEventListener('click', handleClickOutside); + } + }; + document.addEventListener('click', handleClickOutside); + }, 100); + + console.log('[CustomMenu] 커스텀 컨텍스트 메뉴 생성 완료:', selectedText); +} + +// 임시 메시지 표시 함수 +function showTemporaryMessage(message) { + const messageDiv = document.createElement('div'); + messageDiv.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 9999999; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px 20px; + border-radius: 6px; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + pointer-events: none; + `; + messageDiv.textContent = message; + + document.body.appendChild(messageDiv); + + setTimeout(() => { + if (messageDiv.parentNode) { + messageDiv.parentNode.removeChild(messageDiv); + } + }, 1500); +} + +// 커스텀 컨텍스트 메뉴 제거 +function removeCustomContextMenu() { + if (customContextMenu) { + customContextMenu.remove(); + customContextMenu = null; + } +} + +// // 향상된 컨텍스트 메뉴 이벤트 +// document.addEventListener("contextmenu", (e) => { +// lastContextMenuPos = { x: e.pageX, y: e.pageY }; + +// // 특별한 환경에서는 커스텀 컨텍스트 메뉴도 표시 +// if (detectEnvironment()) { +// const selectedText = getSelectedTextAdvanced(); +// if (selectedText) { +// // 기본 컨텍스트 메뉴를 막지 않고 추가로 커스텀 메뉴 표시 +// setTimeout(() => createCustomContextMenu(e), 50); +// } +// } +// }); + +// 향상된 단축키 처리 document.addEventListener("keydown", (e) => { + // ESC 키 처리 if (e.key === "Escape") { if (tooltipEl) removeTooltip(); if (loadingIndicator) removeLoadingIndicator(); + if (customContextMenu) removeCustomContextMenu(); + return; } -}); + + // Ctrl+Shift+F: 지재권 검색 + if (e.ctrlKey && e.shiftKey && e.key === 'S') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + console.log('[Shortcut] Ctrl+Shift+F - 지재권 검색:', selectedText); + chrome.runtime.sendMessage({ + action: "searchTrademark", + keyword: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[Shortcut] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[Shortcut] 지재권 검색 메시지 전송 성공:', response); + } + }); + } else { + console.log('[Shortcut] 선택된 텍스트가 없어 지재권 검색을 실행할 수 없습니다.'); + showTemporaryMessage('텍스트를 선택해주세요'); + } + return; + } + + // Ctrl+Shift+T: 멀티번역 + if (e.ctrlKey && e.shiftKey && e.key === 'E') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + console.log('[Shortcut] Ctrl+Shift+T - 멀티번역:', selectedText); + chrome.runtime.sendMessage({ + action: "translateText", + text: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[Shortcut] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[Shortcut] 멀티번역 메시지 전송 성공:', response); + } + }); + } else { + console.log('[Shortcut] 선택된 텍스트가 없어 멀티번역을 실행할 수 없습니다.'); + showTemporaryMessage('텍스트를 선택해주세요'); + } + return; + } + + // Ctrl+Shift+K: 한중 번역 (기존 기능 유지) + if (e.ctrlKey && e.shiftKey && e.key === 'K') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + console.log('[Shortcut] Ctrl+Shift+K - 한중번역:', selectedText); + chrome.runtime.sendMessage({ + action: "handleKoreanToChinese", + selectedText: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[Shortcut] 한중번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[Shortcut] 한중번역 메시지 전송 성공:', response); + } + }); + } else { + console.log('[Shortcut] 선택된 텍스트가 없어 한중번역을 실행할 수 없습니다.'); + showTemporaryMessage('텍스트를 선택해주세요'); + } + return; + } + + // Ctrl+Shift+Z: 직번역 (새로 추가) + if (e.ctrlKey && e.shiftKey && e.key === 'Z') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + console.log('[Shortcut] Ctrl+Shift+Z - 직번역:', selectedText); + chrome.runtime.sendMessage({ + action: "handleDirectTranslation", + selectedText: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[Shortcut] 직번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[Shortcut] 직번역 메시지 전송 성공:', response); + } + }); + } else { + console.log('[Shortcut] 선택된 텍스트가 없어 직번역을 실행할 수 없습니다.'); + showTemporaryMessage('텍스트를 선택해주세요'); + } + return; + } +}, true); // useCapture를 true로 설정하여 이벤트를 먼저 캐치 + +// iframe 내부에서도 이벤트 감지 +function setupIframeEventListeners() { + const iframes = document.querySelectorAll('iframe'); + + iframes.forEach(iframe => { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + + // iframe 내부에서 컨텍스트 메뉴 이벤트 + iframeDoc.addEventListener("contextmenu", (e) => { + lastContextMenuPos = { + x: e.pageX + iframe.offsetLeft, + y: e.pageY + iframe.offsetTop + }; + + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + console.log('[iframe] 컨텍스트 메뉴에서 선택된 텍스트:', selectedText); + setTimeout(() => createCustomContextMenu(e), 50); + } + }); + + // iframe 내부에서 키보드 이벤트 리스너 추가 + addKeyboardListeners(iframeDoc); + + } catch (e) { + console.log('[content.js] iframe 이벤트 설정 실패:', e.message); + } + }); +} + +// 페이지 로드 시 iframe 이벤트 설정 +setTimeout(setupIframeEventListeners, 1000); + // 로딩 인디케이터 생성 및 표시 function showLoadingIndicator(message, position = null) { @@ -163,6 +678,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; } + // 자동 찜하기 시작 + if (message.action === "startAutoZzim") { + console.log('[AutoZzim] 자동 찜하기 시작:', message); + startAutoZzim(message.maxZzim || 50, message.delay || 1000); + sendResponse({ success: true }); + return true; + } + // 로딩 인디케이터 표시 요청 if (message.action === "showLoading") { showLoadingIndicator(message.message, message.position); @@ -170,6 +693,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; } + // 선택 영역 교체 요청 + if (message.action === 'replaceSelectedText' && message.text) { + // 선택 영역이 있으면 해당 부분만 교체, 아니면 포커스된 엘리먼트 전체 교체 + if (!replaceSelectedText(message.text)) { + // selection이 없다면, 포커스된 .edit 등 target element 전체 교체 시도 + const activeElem = document.activeElement; + if (activeElem && activeElem.classList.contains('edit')) { + activeElem.innerText = message.text; + } else { + // 특정 요소를 찾고 싶으면 아래처럼 직접 지정 + const target = document.querySelector('.edit[contenteditable="false"]'); + if (target) { + target.innerText = message.text; + } + } + } + sendResponse({success: true}); + return true; + } + // 로딩 인디케이터 제거 요청 if (message.action === "hideLoading") { removeLoadingIndicator(); @@ -355,6 +898,18 @@ function positionTooltip(tooltip, pos) { } } +// selection 영역 교체 함수 (간단 버전) +function replaceSelectedText(newText) { + const selection = window.getSelection(); + if (selection && selection.rangeCount) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(document.createTextNode(newText)); + return true; + } + return false; +} + function removeTooltip() { if (tooltipEl && tooltipEl.parentNode) { tooltipEl.parentNode.removeChild(tooltipEl); @@ -858,9 +1413,1588 @@ function getUserLevelGuide(userLevel) { // 번역 툴팁 제거 function removeTranslationTooltip() { - const translationTooltip = document.getElementById("translation-tooltip"); - if (translationTooltip) { - translationTooltip.remove(); - console.log('[content.js] 번역 툴팁 제거됨'); + const existingTooltip = document.getElementById('translation-tooltip'); + if (existingTooltip) { + existingTooltip.remove(); } } + +// 자동 찜하기 전역 변수들 +let autoZzimState = { + isRunning: false, + maxZzim: 50, + actualZzimCount: 0, + currentPage: 1, + delay: 1000, + statusDiv: null, + startTime: null +}; + +// 찜하기 버튼 찾기 함수 (전역) - 정확한 선택자 사용 + function findZzimButtons() { + console.log('[AutoZzim] 찜 버튼 찾기 시작...'); + + // 네이버 스마트스토어 정확한 선택자 사용 + const zzimButtonSelector = 'div#CategoryProducts button.zzim_button[aria-pressed="false"]'; + + try { + const zzimButtons = document.querySelectorAll(zzimButtonSelector); + console.log(`[AutoZzim] 찜 가능한 버튼 발견: ${zzimButtons.length}개`); + + // 실제로 보이고 클릭 가능한 버튼만 필터링 + const visibleButtons = Array.from(zzimButtons).filter(btn => { + const rect = btn.getBoundingClientRect(); + const style = window.getComputedStyle(btn); + + const isVisible = rect.width > 0 && + rect.height > 0 && + style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + !btn.disabled; + + if (isVisible) { + // 버튼 주변에 다른 클릭 가능한 요소가 있는지 확인 + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + // 버튼 중심 좌표에서 실제 클릭될 요소 확인 + const elementAtPoint = document.elementFromPoint(centerX, centerY); + + // 클릭될 요소가 찜 버튼이거나 찜 버튼의 자식 요소인지 확인 + const isCorrectElement = elementAtPoint === btn || btn.contains(elementAtPoint); + + if (!isCorrectElement) { + console.log('[AutoZzim] 버튼이 다른 요소에 가려짐:', { + button: btn, + elementAtPoint: elementAtPoint, + elementAtPointTag: elementAtPoint?.tagName, + elementAtPointClass: elementAtPoint?.className + }); + return false; + } + + // 버튼 텍스트 확인 (찜하기 관련 텍스트가 있는지) + const buttonText = btn.textContent?.trim().toLowerCase() || ''; + const buttonAriaLabel = btn.getAttribute('aria-label')?.toLowerCase() || ''; + + const isZzimButton = buttonText.includes('찜') || + buttonText.includes('하트') || + buttonAriaLabel.includes('찜') || + buttonAriaLabel.includes('wishlist') || + btn.className.includes('zzim') || + btn.className.includes('wish'); + + if (!isZzimButton) { + console.log('[AutoZzim] 찜하기 버튼이 아님:', { + text: buttonText, + ariaLabel: buttonAriaLabel, + className: btn.className + }); + return false; + } + + // 판매자 정보나 다른 기능 버튼이 아닌지 확인 + const isNotOtherButton = !buttonText.includes('판매자') && + !buttonText.includes('상점') && + !buttonText.includes('문의') && + !buttonText.includes('연락') && + !buttonText.includes('전화') && + !buttonText.includes('리뷰') && + !buttonText.includes('평점') && + !buttonAriaLabel.includes('판매자') && + !buttonAriaLabel.includes('상점'); + + if (!isNotOtherButton) { + console.log('[AutoZzim] 다른 기능 버튼임:', { + text: buttonText, + ariaLabel: buttonAriaLabel + }); + return false; + } + + console.log('[AutoZzim] 찜 가능한 버튼 확인:', { + text: btn.textContent?.trim().substring(0, 30), + className: btn.className, + ariaPressed: btn.getAttribute('aria-pressed'), + position: `x:${Math.round(rect.x)}, y:${Math.round(rect.y)}`, + size: `${Math.round(rect.width)}x${Math.round(rect.height)}`, + isCorrectElement: isCorrectElement, + isZzimButton: isZzimButton, + isNotOtherButton: isNotOtherButton + }); + } + + return isVisible; + }); + + console.log(`[AutoZzim] 최종 찜 가능한 버튼: ${visibleButtons.length}개`); + return visibleButtons; + + } catch (error) { + console.error('[AutoZzim] 찜 버튼 찾기 오류:', error); + return []; + } +} + +// 이미 찜한 상품인지 확인 함수 (전역) - 단순화 +function isAlreadyZzimed_Improved(button) { + try { + // aria-pressed 속성으로 확인 (가장 정확한 방법) + const ariaPressed = button.getAttribute('aria-pressed'); + const isZzimed = ariaPressed === 'true'; + + if (isZzimed) { + console.log('[AutoZzim] 이미 찜됨 (aria-pressed=true)'); + } + + return isZzimed; + + } catch (e) { + console.error('[AutoZzim] 찜 상태 확인 오류:', e); + return false; + } + } + +// 찜하기 버튼 클릭 함수 (전역) - 개선된 버전 + async function clickZzimButton(button) { + console.log('[AutoZzim] 찜 버튼 클릭 시작:', { + text: button.textContent?.trim().substring(0, 30), + className: button.className, + ariaPressed: button.getAttribute('aria-pressed'), + tagName: button.tagName, + id: button.id + }); + + return new Promise((resolve) => { + try { + // 클릭 전 상태 확인 + const beforePressed = button.getAttribute('aria-pressed'); + console.log('[AutoZzim] 클릭 전 aria-pressed:', beforePressed); + + if (beforePressed === 'true') { + console.log('[AutoZzim] 이미 찜된 상품, 건너뛰기'); + resolve(false); + return; + } + + // 버튼이 클릭 가능한지 확인 + if (button.disabled) { + console.log('[AutoZzim] 버튼이 비활성화되어 있음'); + resolve(false); + return; + } + + // 버튼 주변 요소 확인 (판매자 정보 등 다른 클릭 가능한 요소가 있는지) + const rect = button.getBoundingClientRect(); + console.log('[AutoZzim] 버튼 위치 및 크기:', { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + centerX: rect.x + rect.width / 2, + centerY: rect.y + rect.height / 2 + }); + + // 버튼이 화면에 보이도록 스크롤 (부드럽게) + button.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + + setTimeout(() => { + if (!autoZzimState.isRunning) { + console.log('[AutoZzim] 찜하기가 중단됨'); + resolve(false); + return; + } + + // 버튼 클릭 시도 (이벤트 전파 방지) + try { + console.log('[AutoZzim] 정확한 버튼 클릭 시도 (이벤트 전파 방지)'); + + // 방법 1: 이벤트 전파를 막는 직접 클릭 + const clickHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + console.log('[AutoZzim] 클릭 이벤트 전파 차단됨'); + }; + + // 임시 이벤트 리스너 추가 (이벤트 전파 방지) + button.addEventListener('click', clickHandler, { capture: true, once: true }); + + // 포커스 설정 + if (button.focus) { + button.focus(); + } + + // 직접 클릭 (가장 안전한 방법) + button.click(); + + console.log('[AutoZzim] 직접 클릭 완료'); + + // 클릭 후 상태 변화 확인 + let checkAttempts = 0; + const maxAttempts = 10; // 최대 5초간 확인 + + const checkStateChange = () => { + checkAttempts++; + const afterPressed = button.getAttribute('aria-pressed'); + + console.log(`[AutoZzim] 상태 확인 ${checkAttempts}/${maxAttempts}:`, { + before: beforePressed, + after: afterPressed, + success: beforePressed === 'false' && afterPressed === 'true' + }); + + // 성공 조건: false → true로 변경 + if (beforePressed === 'false' && afterPressed === 'true') { + console.log('[AutoZzim] ✅ 찜하기 성공!'); + autoZzimState.actualZzimCount++; + + // 확인 모달 처리 (필요한 경우) + setTimeout(() => { + handleConfirmationModals(); + }, 200); + + resolve(true); + return; + } + + // 재시도 조건 + if (checkAttempts < maxAttempts) { + setTimeout(checkStateChange, 500); // 0.5초마다 재확인 + } else { + console.log('[AutoZzim] ❌ 찜하기 실패 (상태 변화 없음), 대안 방법 시도'); + + // 대안 방법: 더 정확한 이벤트 생성 + try { + console.log('[AutoZzim] 대안 방법: 정확한 좌표로 클릭 이벤트 생성'); + + // 버튼의 정확한 중심 좌표 계산 + const updatedRect = button.getBoundingClientRect(); + const centerX = updatedRect.left + updatedRect.width / 2; + const centerY = updatedRect.top + updatedRect.height / 2; + + // 더 정확한 마우스 이벤트 생성 (이벤트 전파 방지) + const mouseDownEvent = new MouseEvent('mousedown', { + bubbles: false, // 이벤트 전파 방지 + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + }); + + const mouseUpEvent = new MouseEvent('mouseup', { + bubbles: false, // 이벤트 전파 방지 + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + }); + + const clickEvent = new MouseEvent('click', { + bubbles: false, // 이벤트 전파 방지 + cancelable: true, + view: window, + clientX: centerX, + clientY: centerY, + button: 0 + }); + + // 순차적으로 이벤트 발생 + button.dispatchEvent(mouseDownEvent); + setTimeout(() => { + button.dispatchEvent(mouseUpEvent); + setTimeout(() => { + button.dispatchEvent(clickEvent); + + // 최종 상태 확인 + setTimeout(() => { + const finalPressed = button.getAttribute('aria-pressed'); + if (beforePressed === 'false' && finalPressed === 'true') { + console.log('[AutoZzim] ✅ 대안 방법으로 찜하기 성공!'); + autoZzimState.actualZzimCount++; + resolve(true); + } else { + console.log('[AutoZzim] ❌ 대안 방법도 실패'); + resolve(false); + } + }, 1000); + }, 50); + }, 50); + + } catch (alternativeError) { + console.error('[AutoZzim] 대안 방법 실패:', alternativeError); + resolve(false); + } + } + }; + + // 첫 번째 상태 확인은 0.5초 후 + setTimeout(checkStateChange, 500); + + } catch (clickError) { + console.error('[AutoZzim] 버튼 클릭 오류:', clickError); + resolve(false); + } + + }, 800); // 스크롤 후 충분한 대기 시간 + + } catch (e) { + console.error('[AutoZzim] 찜 버튼 클릭 중 예외 오류:', e); + resolve(false); + } + }); +} + +// 확인 모달/팝업 처리 함수 (전역) - 개선된 버전 +function handleConfirmationModals() { + console.log('[AutoZzim] 확인 모달 처리 시작'); + + // 찜하기 관련 확인 모달만 처리하도록 제한 + const confirmSelectors = [ + // 네이버 스마트스토어 찜하기 전용 확인 버튼 + 'button[data-testid="wishlist-confirm"]', + 'button[data-testid="zzim-confirm"]', + '.zzim_confirm_btn', + '.wishlist_confirm_btn', + + // 일반적인 확인 버튼 (찜하기 관련 텍스트가 있는 경우만) + 'button[class*="confirm"]:contains("찜")', + 'button[class*="confirm"]:contains("확인")', + + // 모달 내부의 확인 버튼 (찜하기 관련만) + '.modal button[class*="confirm"]', + '.popup button[class*="confirm"]', + '.dialog button[class*="confirm"]', + + // 레이어 팝업 내 확인 버튼 + '[class*="layer"] button[class*="confirm"]', + '[role="dialog"] button[class*="primary"]' + ]; + + for (const selector of confirmSelectors) { + try { + let confirmButtons = []; + + // :contains() 선택자 수동 처리 + if (selector.includes(':contains(')) { + const [baseSelector, containsText] = selector.split(':contains('); + const searchText = containsText.replace(/[\(\)\"\']/g, ''); + const candidateElements = document.querySelectorAll(baseSelector); + + confirmButtons = Array.from(candidateElements).filter(el => { + const text = el.textContent && el.textContent.trim(); + return text && text.includes(searchText); + }); + } else { + confirmButtons = Array.from(document.querySelectorAll(selector)); + } + + // 보이고 클릭 가능한 버튼 중에서 찜하기 관련만 필터링 + for (const btn of confirmButtons) { + if (btn && btn.offsetParent !== null && !btn.disabled) { + const rect = btn.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + + // 버튼 텍스트 확인 - 찜하기 관련 텍스트가 있는지 확인 + const buttonText = btn.textContent?.trim().toLowerCase() || ''; + const isZzimRelated = buttonText.includes('찜') || + buttonText.includes('확인') || + buttonText.includes('ok') || + buttonText.includes('예') || + buttonText.includes('yes'); + + // 판매자 정보나 다른 팝업이 아닌지 확인 + const isNotSellerInfo = !buttonText.includes('판매자') && + !buttonText.includes('상점') && + !buttonText.includes('업체') && + !buttonText.includes('문의') && + !buttonText.includes('연락') && + !buttonText.includes('전화'); + + if (isZzimRelated && isNotSellerInfo) { + console.log('[AutoZzim] 찜하기 관련 확인 버튼 클릭:', { + selector: selector, + text: buttonText, + className: btn.className + }); + + // 이벤트 전파를 막고 클릭 + const clickHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + }; + + btn.addEventListener('click', clickHandler, { capture: true, once: true }); + btn.click(); + + return; // 첫 번째 적절한 확인 버튼만 클릭하고 종료 + } else { + console.log('[AutoZzim] 찜하기와 무관한 버튼 무시:', { + text: buttonText, + isZzimRelated: isZzimRelated, + isNotSellerInfo: isNotSellerInfo + }); + } + } + } + } + } catch (e) { + console.log('[AutoZzim] 확인 버튼 검색 오류:', selector, e.message); + } + } + + console.log('[AutoZzim] 찜하기 관련 확인 버튼을 찾지 못했습니다'); +} + +// 현재 페이지 찜하기 처리 함수 (전역) - 개선된 버전 + async function processCurrentPage() { + if (!autoZzimState.isRunning || autoZzimState.actualZzimCount >= autoZzimState.maxZzim) { + console.log('[AutoZzim] 찜하기 중단 또는 목표 달성'); + return false; + } + + console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 찜하기 처리 시작`); + updateZzimStatus(autoZzimState.statusDiv, `페이지 ${autoZzimState.currentPage} 분석 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`); + + // 찜 가능한 버튼 찾기 + const zzimButtons = findZzimButtons(); + console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에서 찾은 찜 가능한 버튼: ${zzimButtons.length}개`); + + if (zzimButtons.length === 0) { + console.log('[AutoZzim] 현재 페이지에 찜할 상품이 없음'); + return false; + } + + let processedInPage = 0; + + // 각 버튼을 순차적으로 처리 + for (let i = 0; i < zzimButtons.length && autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim; i++) { + const button = zzimButtons[i]; + + updateZzimStatus(autoZzimState.statusDiv, `찜하기 진행 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim}) - 페이지 ${autoZzimState.currentPage}`); + + console.log(`[AutoZzim] ${i + 1}/${zzimButtons.length}번째 버튼 처리 중...`); + + const success = await clickZzimButton(button); + if (success) { + processedInPage++; + console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에서 ${processedInPage}번째 찜 성공`); + } + + // 다음 버튼 처리 전 대기 (설정된 간격) + if (i < zzimButtons.length - 1 && autoZzimState.isRunning) { + console.log(`[AutoZzim] ${autoZzimState.delay}ms 대기 중...`); + await new Promise(resolve => setTimeout(resolve, autoZzimState.delay)); + } + } + + console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 완료: ${processedInPage}개 찜함`); + return processedInPage > 0; + } + +// 자동 찜하기 메인 루프 함수 (전역) +async function autoZzimMainLoop() { + console.log('[AutoZzim] 메인 루프 시작'); + + while (autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim && autoZzimState.currentPage <= 10) { + console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 시작`); + + // 페이지 로드 대기 + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 현재 페이지 처리 + const hasProducts = await processCurrentPage(); + + if (!hasProducts) { + console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에 더 이상 찜할 상품이 없음`); + + // 다음 페이지로 이동 시도 + updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`); + const moved = await goToNextPage(); + if (!moved) { + console.log('[AutoZzim] 더 이상 다음 페이지가 없음'); + break; + } + } else { + // 현재 페이지에서 더 찜할 수 있는지 확인 + const remainingButtons = findZzimButtons(); + if (remainingButtons.length === 0) { + // 다음 페이지로 이동 + updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`); + const moved = await goToNextPage(); + if (!moved) { + console.log('[AutoZzim] 더 이상 다음 페이지가 없음'); + break; + } + } + } + + // 페이지 간 대기 + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + // 완료 처리 + console.log(`[AutoZzim] 찜하기 완료: 실제 ${autoZzimState.actualZzimCount}개 찜함`); + updateZzimStatus(autoZzimState.statusDiv, `찜하기 완료! (실제 ${autoZzimState.actualZzimCount}개 찜함)`); + + // 상태 초기화 + autoZzimState.isRunning = false; + + // 5초 후 상태 UI 제거 + setTimeout(() => { + if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) { + autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv); + autoZzimState.statusDiv = null; + } + }, 5000); + } + +// 자동 찜하기 기능 (개선된 버전) +function startAutoZzim(maxZzim = 50, delay = 1000) { + console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay }); + + // 상태 초기화 + autoZzimState = { + isRunning: true, + maxZzim: maxZzim, + actualZzimCount: 0, + currentPage: 1, + delay: delay, + statusDiv: null, + startTime: Date.now() + }; + + // 상태 표시 UI 생성 + autoZzimState.statusDiv = createZzimStatusUI(); + updateZzimStatus(autoZzimState.statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`); + + // 60초 후 자동 종료 + setTimeout(() => { + if (autoZzimState.isRunning) { + autoZzimState.isRunning = false; + console.log('[AutoZzim] 시간 초과로 찜하기 종료'); + updateZzimStatus(autoZzimState.statusDiv, `시간 초과로 종료 (실제 ${autoZzimState.actualZzimCount}개 찜함)`); + + // 5초 후 상태 UI 제거 + setTimeout(() => { + if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) { + autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv); + autoZzimState.statusDiv = null; + } + }, 5000); + } + }, 60000); + + // 메인 루프 시작 + setTimeout(autoZzimMainLoop, 2000); // 페이지 로드 후 2초 대기 +} + +// 다음 페이지로 이동 함수 (기존 goToNextPage 함수 수정) +async function goToNextPage() { + try { + console.log(`[AutoZzim] 다음 페이지로 이동 시도: ${autoZzimState.currentPage} → ${autoZzimState.currentPage + 1}`); + + // 네이버 스마트스토어 URL 분석 + const urlInfo = parseNaverSmartStoreUrl(window.location.href); + + if (!urlInfo) { + console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href); + return false; + } + + if (!urlInfo.isProductListPage) { + console.log('[AutoZzim] 상품 목록 페이지가 아님, 올바른 페이지로 이동'); + + // 올바른 상품 목록 페이지 URL 생성 + const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, false, 50); + + console.log('[AutoZzim] 올바른 상품 목록 페이지로 이동:', correctUrl); + window.location.href = correctUrl; + return true; + } + + // 1단계: 현재 페이지 번호 확인 + const currentPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`); + let actualCurrentPage = 1; + + if (currentPageButton) { + const currentPageId = currentPageButton.getAttribute('data-shp-contents-id'); + actualCurrentPage = parseInt(currentPageId) || 1; + console.log(`[AutoZzim] 현재 페이지 확인: ${actualCurrentPage} (DOM에서 감지)`); + } else { + // aria-current 없이 data-shp-contents-id로만 확인 + const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`); + console.log(`[AutoZzim] 전체 페이지 버튼 개수: ${allPageButtons.length}`); + + // URL의 cp 파라미터로 현재 페이지 추정 + const urlParams = new URLSearchParams(window.location.search); + const cpParam = urlParams.get('cp'); + if (cpParam) { + actualCurrentPage = parseInt(cpParam) || 1; + console.log(`[AutoZzim] URL 파라미터에서 현재 페이지 확인: ${actualCurrentPage}`); + } + } + + // currentPage 변수 업데이트 + autoZzimState.currentPage = actualCurrentPage; + const nextPageNum = autoZzimState.currentPage + 1; + + console.log(`[AutoZzim] 다음 페이지 번호: ${nextPageNum}`); + + // 2단계: 다음 페이지 버튼 찾기 (네이버 스마트스토어 특화) + const nextPageSelectors = [ + // 네이버 스마트스토어 페이지 버튼 (가장 정확한 방법) + `div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id='${nextPageNum}']`, + + // 일반적인 다음 페이지 버튼 + 'a.pagination_next:not(.pagination_disabled)', + 'a[class*="pagination"][class*="next"]:not([class*="disabled"])', + 'button[class*="pagination"][class*="next"]:not([disabled])', + '.paginate_next:not(.disabled)', + '[class*="paging"] a[class*="next"]:not([class*="disabled"])', + + // aria-label 기반 + 'a[aria-label*="다음"]:not([aria-disabled="true"])', + 'button[aria-label*="다음"]:not([disabled])', + 'a[title*="다음"]:not([class*="disabled"])', + + // 페이지 번호로 다음 페이지 찾기 (URL 기반) + `a[href*="cp=${nextPageNum}"]`, + `button[data-page="${nextPageNum}"]`, + `a[href*="page=${nextPageNum}"]` + ]; + + let nextButton = null; + let selectedMethod = ''; + + for (const selector of nextPageSelectors) { + try { + const buttons = document.querySelectorAll(selector); + console.log(`[AutoZzim] 선택자 "${selector}" 검색 결과: ${buttons.length}개`); + + for (const btn of buttons) { + if (btn && btn.offsetParent !== null && !btn.disabled && !btn.classList.contains('disabled')) { + nextButton = btn; + selectedMethod = selector; + console.log(`[AutoZzim] 다음 페이지 버튼 발견:`, { + selector: selector, + text: btn.textContent?.trim(), + className: btn.className, + dataShpContentsId: btn.getAttribute('data-shp-contents-id'), + href: btn.href, + tagName: btn.tagName + }); + break; + } + } + if (nextButton) break; + } catch (e) { + console.log(`[AutoZzim] 선택자 오류 (${selector}):`, e.message); + } + } + + if (nextButton) { + // 다음 페이지 버튼 클릭 + console.log(`[AutoZzim] 다음 페이지 버튼 클릭 시도 (방법: ${selectedMethod})`); + + // 버튼이 화면에 보이도록 스크롤 + nextButton.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 클릭 전 상태 로그 + console.log('[AutoZzim] 클릭 전 버튼 상태:', { + disabled: nextButton.disabled, + offsetParent: nextButton.offsetParent !== null, + classList: Array.from(nextButton.classList) + }); + + // 다양한 방식으로 클릭 시도 + try { + // 방법 1: 일반 클릭 + nextButton.click(); + console.log('[AutoZzim] 일반 클릭 완료'); + } catch (e1) { + console.log('[AutoZzim] 일반 클릭 실패, 이벤트 방식 시도:', e1.message); + + try { + // 방법 2: 마우스 이벤트 시뮬레이션 + const rect = nextButton.getBoundingClientRect(); + const clickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: rect.left + rect.width / 2, + clientY: rect.top + rect.height / 2 + }); + nextButton.dispatchEvent(clickEvent); + console.log('[AutoZzim] 마우스 이벤트 클릭 완료'); + } catch (e2) { + console.log('[AutoZzim] 마우스 이벤트 클릭 실패:', e2.message); + + // 방법 3: href가 있는 경우 직접 이동 + if (nextButton.href) { + console.log('[AutoZzim] href로 직접 이동:', nextButton.href); + window.location.href = nextButton.href; + } + } + } + + // 페이지 로드 대기 + console.log('[AutoZzim] 페이지 로드 대기 중...'); + await new Promise(resolve => setTimeout(resolve, 3000)); + + // 페이지 변경 확인 + const newPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`); + if (newPageButton) { + const newPageId = newPageButton.getAttribute('data-shp-contents-id'); + const newPageNum = parseInt(newPageId) || 1; + + if (newPageNum > actualCurrentPage) { + console.log(`[AutoZzim] ✅ 페이지 이동 성공: ${actualCurrentPage} → ${newPageNum}`); + autoZzimState.currentPage = newPageNum; + return true; + } else { + console.log(`[AutoZzim] ❌ 페이지 이동 실패: 여전히 ${newPageNum} 페이지`); + } + } else { + // aria-current가 없어도 URL로 확인 + const urlParams = new URLSearchParams(window.location.search); + const newCpParam = urlParams.get('cp'); + if (newCpParam && parseInt(newCpParam) > actualCurrentPage) { + const newPageNum = parseInt(newCpParam); + console.log(`[AutoZzim] ✅ 페이지 이동 성공 (URL 확인): ${actualCurrentPage} → ${newPageNum}`); + autoZzimState.currentPage = newPageNum; + return true; + } + } + + return false; + } + + // 3단계: 버튼이 없으면 URL 직접 변경 + console.log('[AutoZzim] 다음 페이지 버튼이 없음, URL 직접 변경 시도'); + + // 마지막 페이지 번호 확인 + const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`); + const pageNumbers = Array.from(allPageButtons).map(btn => { + const id = btn.getAttribute('data-shp-contents-id'); + return parseInt(id) || 0; + }).filter(num => num > 0); + + const maxPage = Math.max(...pageNumbers); + console.log(`[AutoZzim] 감지된 페이지 번호들:`, pageNumbers); + console.log(`[AutoZzim] 최대 페이지 번호: ${maxPage}`); + + if (nextPageNum > maxPage) { + console.log(`[AutoZzim] 다음 페이지 ${nextPageNum}이 최대 페이지 ${maxPage}를 초과함`); + return false; + } + + // URL 직접 변경 + const currentUrl = new URL(window.location.href); + currentUrl.searchParams.set('cp', nextPageNum.toString()); + + console.log(`[AutoZzim] URL 직접 변경: cp=${actualCurrentPage} → cp=${nextPageNum}`); + console.log('[AutoZzim] 다음 페이지 URL:', currentUrl.href); + + window.location.href = currentUrl.href; + return true; + + } catch (e) { + console.error('[AutoZzim] 다음 페이지 이동 오류:', e); + return false; + } +} + +function createZzimStatusUI() { + const statusDiv = document.createElement('div'); + statusDiv.id = 'auto-zzim-status'; + statusDiv.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 999999; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 15px 20px; + border-radius: 10px; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + font-weight: 600; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 200px; + text-align: center; + animation: slideIn 0.3s ease-out; + `; + + // CSS 애니메이션 추가 + if (!document.getElementById('auto-zzim-styles')) { + const style = document.createElement('style'); + style.id = 'auto-zzim-styles'; + style.textContent = ` + @keyframes slideIn { + 0% { transform: translateX(100%); opacity: 0; } + 100% { transform: translateX(0); opacity: 1; } + } + `; + document.head.appendChild(style); + } + + document.body.appendChild(statusDiv); + return statusDiv; +} + +function updateZzimStatus(statusDiv, message) { + if (statusDiv) { + statusDiv.textContent = message; + } +} + +// 페이지 로드 완료 감지 함수 +function waitForPageLoad() { + return new Promise((resolve) => { + // 1. DOM 로드 확인 + if (document.readyState === 'complete') { + console.log('[AutoZzim] DOM 이미 로드 완료'); + resolve(); + return; + } + + // 2. 페이지 로드 이벤트 리스너 + const handleLoad = () => { + console.log('[AutoZzim] 페이지 로드 완료'); + window.removeEventListener('load', handleLoad); + resolve(); + }; + + window.addEventListener('load', handleLoad); + + // 3. 최대 10초 대기 + setTimeout(() => { + console.log('[AutoZzim] 페이지 로드 대기 시간 초과'); + window.removeEventListener('load', handleLoad); + resolve(); + }, 10000); + }); +} + +// 페이지 로드 시 URL 파라미터 확인 (개선된 버전) +async function checkAutoZzimParam() { + const urlParams = new URLSearchParams(window.location.search); + const autoZzim = urlParams.get('auto_zzim'); + const maxZzim = parseInt(urlParams.get('max_zzim')) || 50; + + console.log('[AutoZzim] URL 파라미터 확인:', { + autoZzim: autoZzim, + maxZzim: maxZzim, + currentUrl: window.location.href + }); + + if (autoZzim === 'true') { + console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작'); + + // 네이버 스마트스토어 URL 분석 + const urlInfo = parseNaverSmartStoreUrl(window.location.href); + + if (!urlInfo) { + console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href); + return; + } + + if (!urlInfo.isProductListPage) { + console.log('[AutoZzim] 현재 페이지가 상품 목록 페이지가 아님, 올바른 페이지로 이동'); + + // 올바른 상품 목록 페이지 URL 생성 + const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, true, maxZzim); + + console.log('[AutoZzim] 올바른 상품 목록 페이지로 리다이렉트:', correctUrl); + window.location.href = correctUrl; + return; + } + + // 페이지 완전 로드 대기 + console.log('[AutoZzim] 페이지 로드 완료 대기 중...'); + await waitForPageLoad(); + + // 추가 로드 대기 (네이버 스마트스토어 특화) + await new Promise(resolve => setTimeout(resolve, 3000)); + + // 상품 목록이 로드되었는지 확인 + const productCheck = await waitForProductsToLoad(); + + if (productCheck) { + console.log('[AutoZzim] 상품 목록 로드 완료, 찜하기 시작'); + + // 찜하기 시작 + startAutoZzim(maxZzim); + } else { + console.log('[AutoZzim] 상품 목록 로드 실패'); + } + } +} + +// 상품 목록 로드 대기 함수 +function waitForProductsToLoad() { + return new Promise((resolve) => { + let checkCount = 0; + const maxChecks = 20; // 최대 20번 확인 (20초) + + const checkProducts = () => { + checkCount++; + + // 상품 컨테이너 확인 + const productContainers = document.querySelectorAll('[data-shp-contents-id], [class*="product"], [class*="item"]'); + const hasProducts = productContainers.length > 0; + + console.log(`[AutoZzim] 상품 로드 확인 ${checkCount}/${maxChecks}: ${productContainers.length}개 상품 발견`); + + if (hasProducts) { + console.log('[AutoZzim] ✅ 상품 목록 로드 완료'); + resolve(true); + return; + } + + if (checkCount >= maxChecks) { + console.log('[AutoZzim] ❌ 상품 목록 로드 시간 초과'); + resolve(false); + return; + } + + // 1초 후 재확인 + setTimeout(checkProducts, 1000); + }; + + // 즉시 첫 번째 확인 + checkProducts(); + }); +} + +// 페이지 로드 완료 시 자동 찜하기 파라미터 확인 (개선된 버전) +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + setTimeout(checkAutoZzimParam, 1000); + }); +} else { + setTimeout(checkAutoZzimParam, 1000); +} + +// 추가: 페이지 변경 감지 (SPA 대응) +let lastUrl = window.location.href; +const urlChangeObserver = new MutationObserver(() => { + if (window.location.href !== lastUrl) { + lastUrl = window.location.href; + console.log('[AutoZzim] URL 변경 감지:', lastUrl); + + // URL 변경 후 잠시 대기 후 파라미터 확인 + setTimeout(checkAutoZzimParam, 2000); + } +}); + +// URL 변경 감지 시작 +urlChangeObserver.observe(document.body, { + childList: true, + subtree: true +}); + +// popstate 이벤트도 감지 (뒤로가기/앞으로가기) +window.addEventListener('popstate', () => { + console.log('[AutoZzim] popstate 이벤트 감지'); + setTimeout(checkAutoZzimParam, 2000); +}); + +// 자동 찜하기 파라미터 확인 + checkAutoZzimParam(); + +// 디버깅용 전역 함수들 추가 +window.debugZzimButtons = function() { + console.log('=== 네이버 스마트스토어 찜 버튼 디버깅 ==='); + + // 정확한 선택자로 찜 버튼 찾기 + const zzimButtonSelector = 'div#CategoryProducts button.zzim_button'; + const allZzimButtons = document.querySelectorAll(zzimButtonSelector); + console.log(`전체 찜 버튼: ${allZzimButtons.length}개`); + + // 찜 가능한 버튼 (aria-pressed="false") + const availableZzimButtons = document.querySelectorAll('div#CategoryProducts button.zzim_button[aria-pressed="false"]'); + console.log(`찜 가능한 버튼: ${availableZzimButtons.length}개`); + + // 이미 찜한 버튼 (aria-pressed="true") + const zzimmedButtons = document.querySelectorAll('div#CategoryProducts button.zzim_button[aria-pressed="true"]'); + console.log(`이미 찜한 버튼: ${zzimmedButtons.length}개`); + + // 각 버튼 상세 정보 출력 + console.log('--- 찜 가능한 버튼 상세 정보 ---'); + availableZzimButtons.forEach((btn, index) => { + const rect = btn.getBoundingClientRect(); + console.log(`[${index + 1}] 찜 가능한 버튼:`, { + text: btn.textContent?.trim().substring(0, 30), + className: btn.className, + ariaPressed: btn.getAttribute('aria-pressed'), + isVisible: rect.width > 0 && rect.height > 0, + position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + size: { width: Math.round(rect.width), height: Math.round(rect.height) }, + disabled: btn.disabled, + element: btn + }); + }); + + console.log('--- 이미 찜한 버튼 상세 정보 ---'); + zzimmedButtons.forEach((btn, index) => { + const rect = btn.getBoundingClientRect(); + console.log(`[${index + 1}] 이미 찜한 버튼:`, { + text: btn.textContent?.trim().substring(0, 30), + className: btn.className, + ariaPressed: btn.getAttribute('aria-pressed'), + isVisible: rect.width > 0 && rect.height > 0, + position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + element: btn + }); + }); + + // 상품 컨테이너 확인 + const categoryProducts = document.getElementById('CategoryProducts'); + if (categoryProducts) { + console.log('CategoryProducts 컨테이너 발견:', categoryProducts); + } else { + console.log('❌ CategoryProducts 컨테이너를 찾을 수 없음'); + } + + return { + totalZzimButtons: allZzimButtons.length, + availableZzimButtons: availableZzimButtons.length, + zzimmedButtons: zzimmedButtons.length, + availableButtons: Array.from(availableZzimButtons), + zzimmedButtonsArray: Array.from(zzimmedButtons) + }; +}; + +window.findCurrentZzimButtons = function() { + console.log('=== 현재 페이지 찜 버튼 찾기 ==='); + + const buttons = findZzimButtons(); + console.log(`찾은 찜 가능한 버튼: ${buttons.length}개`); + + buttons.forEach((btn, index) => { + console.log(`[${index}]`, { + text: btn.textContent?.trim().substring(0, 30), + className: btn.className, + ariaPressed: btn.getAttribute('aria-pressed'), + element: btn + }); + }); + + return buttons; +}; + +window.testZzimClick = function(buttonIndex = 0) { + console.log('=== 찜 버튼 클릭 테스트 ==='); + + const buttons = findZzimButtons(); + if (buttons.length === 0) { + console.log('❌ 클릭할 찜 버튼이 없습니다.'); + return; + } + + if (buttonIndex >= buttons.length) { + console.log(`❌ 인덱스 ${buttonIndex}는 범위를 벗어났습니다. (최대: ${buttons.length - 1})`); + return; + } + + const button = buttons[buttonIndex]; + console.log(`${buttonIndex}번 버튼 클릭 테스트:`, { + text: button.textContent?.trim(), + className: button.className, + ariaPressed: button.getAttribute('aria-pressed') + }); + + // 전역 클릭 함수 사용 + clickZzimButton(button).then(success => { + console.log('클릭 결과:', success ? '✅ 성공' : '❌ 실패'); + + // 클릭 후 상태 확인 + setTimeout(() => { + const afterPressed = button.getAttribute('aria-pressed'); + console.log('클릭 후 aria-pressed:', afterPressed); + }, 1000); + }); +}; + +// 찜 버튼 상태 실시간 모니터링 +window.monitorZzimButtons = function(duration = 10000) { + console.log(`=== 찜 버튼 상태 모니터링 (${duration/1000}초) ===`); + + const startTime = Date.now(); + const interval = setInterval(() => { + const debug = window.debugZzimButtons(); + console.log(`[${new Date().toLocaleTimeString()}] 찜 가능: ${debug.availableZzimButtons}개, 찜 완료: ${debug.zzimmedButtons}개`); + + if (Date.now() - startTime >= duration) { + clearInterval(interval); + console.log('=== 모니터링 종료 ==='); + } + }, 2000); + + return interval; +}; + +// 자동 찜하기 상태 확인 함수 +window.checkAutoZzimStatus = function() { + console.log('=== 자동 찜하기 상태 확인 ==='); + console.log('현재 상태:', { + isRunning: autoZzimState.isRunning, + actualZzimCount: autoZzimState.actualZzimCount, + maxZzim: autoZzimState.maxZzim, + currentPage: autoZzimState.currentPage, + delay: autoZzimState.delay, + hasStatusDiv: !!autoZzimState.statusDiv, + startTime: autoZzimState.startTime ? new Date(autoZzimState.startTime).toLocaleTimeString() : null + }); + + return autoZzimState; +}; + +// 자동 찜하기 강제 중단 함수 +window.stopAutoZzim = function() { + console.log('=== 자동 찜하기 강제 중단 ==='); + + if (autoZzimState.isRunning) { + autoZzimState.isRunning = false; + console.log('✅ 자동 찜하기가 중단되었습니다.'); + + if (autoZzimState.statusDiv) { + updateZzimStatus(autoZzimState.statusDiv, `수동 중단됨 (${autoZzimState.actualZzimCount}개 찜함)`); + + // 3초 후 상태 UI 제거 + setTimeout(() => { + if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) { + autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv); + autoZzimState.statusDiv = null; + } + }, 3000); + } + } else { + console.log('❌ 현재 실행 중인 자동 찜하기가 없습니다.'); + } +}; + +// 수동 찜하기 테스트 함수 +window.testManualZzim = function(maxCount = 5) { + console.log(`=== 수동 찜하기 테스트 (최대 ${maxCount}개) ===`); + + if (autoZzimState.isRunning) { + console.log('❌ 자동 찜하기가 실행 중입니다. 먼저 중단해주세요.'); + return; + } + + const buttons = findZzimButtons(); + if (buttons.length === 0) { + console.log('❌ 찜할 수 있는 버튼이 없습니다.'); + return; + } + + console.log(`찾은 찜 버튼: ${buttons.length}개`); + + let count = 0; + const testCount = Math.min(maxCount, buttons.length); + + const testNext = async () => { + if (count >= testCount) { + console.log(`✅ 수동 찜하기 테스트 완료: ${count}개 시도`); + return; + } + + const button = buttons[count]; + console.log(`[${count + 1}/${testCount}] 찜 버튼 클릭 테스트:`, { + text: button.textContent?.trim().substring(0, 30), + className: button.className.substring(0, 50) + }); + + const success = await clickZzimButton(button); + console.log(`결과: ${success ? '✅ 성공' : '❌ 실패'}`); + + count++; + + // 1초 대기 후 다음 버튼 + setTimeout(testNext, 1000); + }; + + testNext(); +}; + +// 페이지 로드 시 자동 디버깅 (개발 중에만) +if (window.location.href.includes('smartstore.naver.com') && window.location.search.includes('debug=true')) { + setTimeout(() => { + console.log('🔍 디버그 모드: 자동 찜 버튼 분석 시작'); + window.debugZzimButtons(); + window.debugPagination(); + }, 2000); +} + +// 네이버 스마트스토어 URL 구조 분석 함수 +function parseNaverSmartStoreUrl(url) { + try { + const urlObj = new URL(url); + + // 네이버 스마트스토어 도메인 확인 + if (!urlObj.hostname.includes('smartstore.naver.com')) { + console.log('[AutoZzim] 네이버 스마트스토어 도메인이 아님:', urlObj.hostname); + return null; + } + + // 경로 분석: /storename/... 형태 + const pathParts = urlObj.pathname.split('/').filter(part => part); + console.log('[AutoZzim] 경로 분석:', pathParts); + + if (pathParts.length === 0) { + console.log('[AutoZzim] 경로가 없음'); + return null; + } + + // 첫 번째 경로가 스토어명 + const storeName = pathParts[0]; + const currentSection = pathParts[1] || 'main'; + + // 상품 목록 페이지인지 확인 + const productListSections = ['category', 'best', 'new', 'sale']; + const isProductListPage = productListSections.includes(currentSection); + + console.log('[AutoZzim] URL 분석 결과:', { + storeName: storeName, + currentSection: currentSection, + isProductListPage: isProductListPage, + fullPath: urlObj.pathname + }); + + return { + storeName: storeName, + currentSection: currentSection, + isProductListPage: isProductListPage, + origin: urlObj.origin, + searchParams: urlObj.searchParams + }; + + } catch (error) { + console.error('[AutoZzim] URL 분석 오류:', error); + return null; + } +} + +// 올바른 상품 목록 페이지 URL 생성 함수 +function generateCorrectProductListUrl(storeName, origin, autoZzim = false, maxZzim = 50) { + const baseUrl = `${origin}/${storeName}/category/ALL`; + const params = new URLSearchParams(); + params.set('cp', '1'); + + if (autoZzim) { + params.set('auto_zzim', 'true'); + params.set('max_zzim', maxZzim.toString()); + } + + return `${baseUrl}?${params.toString()}`; +} + +// 키보드 이벤트 리스너 추가 +function addKeyboardListeners(target = document) { + target.addEventListener('keydown', function(e) { + // Ctrl+Shift+F1: 지재권검색 + if (e.ctrlKey && e.shiftKey && e.key === 'S') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText && selectedText.trim()) { + console.log('[Content Script] 지재권검색 단축키 실행:', selectedText); + + chrome.runtime.sendMessage({ + action: 'searchTrademark', + text: selectedText.trim() + }, (response) => { + if (response && response.success) { + console.log('[Content Script] 지재권검색 성공'); + } else { + console.error('[Content Script] 지재권검색 실패:', response?.error); + } + }); + } else { + alert('검색할 텍스트를 선택해주세요.'); + } + } + + // Ctrl+Shift+F2: 멀티번역 + else if (e.ctrlKey && e.shiftKey && e.key === 'E') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText && selectedText.trim()) { + console.log('[Content Script] 멀티번역 단축키 실행:', selectedText); + + chrome.runtime.sendMessage({ + action: 'translateText', + text: selectedText.trim() + }, (response) => { + if (response && response.success) { + console.log('[Content Script] 멀티번역 성공'); + } else { + console.error('[Content Script] 멀티번역 실패:', response?.error); + } + }); + } else { + alert('번역할 텍스트를 선택해주세요.'); + } + } + + // Ctrl+Shift+Z: 한중번역 + else if (e.ctrlKey && e.shiftKey && e.key === 'Z') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText && selectedText.trim()) { + console.log('[Content Script] 한중번역 단축키 실행:', selectedText); + + chrome.runtime.sendMessage({ + action: 'handleKoreanToChinese', + text: selectedText.trim() + }, (response) => { + if (response && response.success) { + console.log('[Content Script] 한중번역 성공'); + } else { + console.error('[Content Script] 한중번역 실패:', response?.error); + } + }); + } else { + alert('번역할 텍스트를 선택해주세요.'); + } + } + + // Ctrl+Shift+K: 직번역 + else if (e.ctrlKey && e.shiftKey && e.key === 'K') { + e.preventDefault(); + e.stopPropagation(); + + const selectedText = getSelectedTextAdvanced(); + if (selectedText && selectedText.trim()) { + console.log('[Content Script] 직번역 단축키 실행:', selectedText); + + chrome.runtime.sendMessage({ + action: 'handleDirectTranslation', + text: selectedText.trim() + }, (response) => { + if (response && response.success) { + console.log('[Content Script] 직번역 성공'); + } else { + console.error('[Content Script] 직번역 실패:', response?.error); + } + }); + } else { + alert('번역할 텍스트를 선택해주세요.'); + } + } + }, true); // useCapture: true로 설정하여 iframe에서도 작동하도록 함 +} + +// 환경 감지 및 이벤트 리스너 설정 +detectEnvironment(); + +// iframe 환경에서도 이벤트 리스너 설정 +if (isIframeEnvironment) { + addKeyboardListeners(window.parent.document); +} + +// iframe 이벤트 설정 +setTimeout(setupIframeEventListeners, 1000); + +// 동적으로 추가되는 iframe 감지 +const mainObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.tagName === 'IFRAME') { + setTimeout(() => setupIframeEventListeners(), 500); + } else if (node.querySelectorAll && node.querySelectorAll('iframe').length > 0) { + setTimeout(() => setupIframeEventListeners(), 500); + } + } + }); + }); +}); + +mainObserver.observe(document.body, { + childList: true, + subtree: true +}); + +// 클릭 이벤트 모니터링 함수 추가 +window.monitorClicks = function(duration = 30000) { + console.log(`=== 클릭 이벤트 모니터링 시작 (${duration/1000}초) ===`); + + const startTime = Date.now(); + + const clickHandler = (e) => { + const target = e.target; + const currentTime = Date.now(); + + if (currentTime - startTime > duration) { + document.removeEventListener('click', clickHandler, true); + console.log('=== 클릭 이벤트 모니터링 종료 ==='); + return; + } + + console.log('[클릭 모니터링] 클릭 이벤트 감지:', { + tagName: target.tagName, + className: target.className, + id: target.id, + textContent: target.textContent?.trim().substring(0, 50), + ariaLabel: target.getAttribute('aria-label'), + dataTestId: target.getAttribute('data-testid'), + isZzimButton: target.className.includes('zzim_button'), + ariaPressed: target.getAttribute('aria-pressed'), + position: { + x: e.clientX, + y: e.clientY + }, + timestamp: new Date().toLocaleTimeString() + }); + + // 판매자 정보나 팝업 관련 클릭 감지 + const isSellerRelated = target.textContent?.includes('판매자') || + target.textContent?.includes('상점') || + target.textContent?.includes('업체') || + target.className.includes('seller') || + target.className.includes('shop'); + + if (isSellerRelated) { + console.warn('[클릭 모니터링] ⚠️ 판매자 관련 요소 클릭 감지!', { + element: target, + text: target.textContent?.trim() + }); + } + + // 팝업이나 모달 관련 클릭 감지 + const isPopupRelated = target.className.includes('popup') || + target.className.includes('modal') || + target.className.includes('dialog') || + target.closest('.popup') || + target.closest('.modal') || + target.closest('.dialog'); + + if (isPopupRelated) { + console.warn('[클릭 모니터링] ⚠️ 팝업/모달 관련 요소 클릭 감지!', { + element: target, + text: target.textContent?.trim() + }); + } + }; + + document.addEventListener('click', clickHandler, true); + + return () => { + document.removeEventListener('click', clickHandler, true); + console.log('=== 클릭 이벤트 모니터링 수동 종료 ==='); + }; +}; + +// 요소 겹침 확인 함수 +window.checkElementOverlap = function(selector = 'div#CategoryProducts button.zzim_button') { + console.log(`=== 요소 겹침 확인: ${selector} ===`); + + const elements = document.querySelectorAll(selector); + console.log(`찾은 요소: ${elements.length}개`); + + elements.forEach((element, index) => { + const rect = element.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const elementAtPoint = document.elementFromPoint(centerX, centerY); + const isCorrect = elementAtPoint === element || element.contains(elementAtPoint); + + console.log(`[${index + 1}] 요소 겹침 확인:`, { + element: element, + text: element.textContent?.trim().substring(0, 30), + className: element.className, + rect: { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: Math.round(rect.height), + centerX: Math.round(centerX), + centerY: Math.round(centerY) + }, + elementAtPoint: elementAtPoint, + elementAtPointTag: elementAtPoint?.tagName, + elementAtPointClass: elementAtPoint?.className, + elementAtPointText: elementAtPoint?.textContent?.trim().substring(0, 30), + isCorrect: isCorrect, + warning: !isCorrect ? '⚠️ 다른 요소가 겹쳐있음!' : '✅ 정상' + }); + }); + + return elements; +}; + +// 안전한 찜하기 테스트 함수 +window.testSafeZzimClick = function(buttonIndex = 0) { + console.log('=== 안전한 찜 버튼 클릭 테스트 ==='); + + // 클릭 모니터링 시작 + const stopMonitoring = window.monitorClicks(10000); + + const buttons = findZzimButtons(); + if (buttons.length === 0) { + console.log('❌ 클릭할 찜 버튼이 없습니다.'); + stopMonitoring(); + return; + } + + if (buttonIndex >= buttons.length) { + console.log(`❌ 인덱스 ${buttonIndex}는 범위를 벗어났습니다. (최대: ${buttons.length - 1})`); + stopMonitoring(); + return; + } + + const button = buttons[buttonIndex]; + console.log(`${buttonIndex}번 버튼 안전 클릭 테스트:`, { + text: button.textContent?.trim(), + className: button.className, + ariaPressed: button.getAttribute('aria-pressed') + }); + + // 요소 겹침 확인 + const rect = button.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const elementAtPoint = document.elementFromPoint(centerX, centerY); + + console.log('클릭 전 요소 겹침 확인:', { + targetButton: button, + elementAtPoint: elementAtPoint, + isCorrect: elementAtPoint === button || button.contains(elementAtPoint) + }); + + // 전역 클릭 함수 사용 + clickZzimButton(button).then(success => { + console.log('클릭 결과:', success ? '✅ 성공' : '❌ 실패'); + + setTimeout(() => { + stopMonitoring(); + const afterPressed = button.getAttribute('aria-pressed'); + console.log('클릭 후 aria-pressed:', afterPressed); + }, 2000); + }); +}; + diff --git a/wrmc_ext/manifest.json b/wrmc_ext/manifest.json index a7e63c0..7b88985 100644 --- a/wrmc_ext/manifest.json +++ b/wrmc_ext/manifest.json @@ -11,15 +11,16 @@ "notifications", "alarms", "tabs", - "scripting" + "scripting", + "clipboardRead" ], "host_permissions": [ "*://markinfo.kr/*", "http://146.56.101.199:8000/*", - "https://oci1ckh08045.duckdns.org:8000/*", "*://smartstore.naver.com/*", "*://translate.googleapis.com/*", "*://api.mymemory.translated.net/*", + "https://market.m.taobao.com/*", "*://openapi.naver.com/*", "*://api-free.deepl.com/*", "*://api.deepl.com/*", @@ -33,6 +34,7 @@ { "matches": [""], "js": ["content.js"], + "all_frames": true, "run_at": "document_end" } ], @@ -59,6 +61,12 @@ "default": "Ctrl+Shift+Z" }, "description": "한국어↔중국어 양방향 번역" + }, + "direct-translate": { + "suggested_key": { + "default": "Ctrl+Shift+K" + }, + "description": "직번역 (텍스트 바로 대체)" } }, "web_accessible_resources": [ diff --git a/wrmc_ext/manual.html b/wrmc_ext/manual.html index 607b0b7..334bf28 100644 --- a/wrmc_ext/manual.html +++ b/wrmc_ext/manual.html @@ -330,7 +330,7 @@ @@ -343,6 +343,7 @@
  • 휴식시간 추천 활동
  • 실시간 타이머 알림
  • 개인화된 시간 설정
  • +
  • [포모도로 타이머가 활성화 되면 35분/5분으로 기본설정되며 4사이클 총180분(3시간) 휴식시간 추천 활동 제공] diff --git a/wrmc_ext/popup.html b/wrmc_ext/popup.html index 5af4375..985f6d1 100644 --- a/wrmc_ext/popup.html +++ b/wrmc_ext/popup.html @@ -427,7 +427,8 @@ - + + diff --git a/wrmc_ext/settings.html b/wrmc_ext/settings.html index f801582..32eb3e0 100644 --- a/wrmc_ext/settings.html +++ b/wrmc_ext/settings.html @@ -542,21 +542,29 @@
    알람 활성화/비활성화 + + +
    + +
    + 35분 작업 / 5분 휴식 (4회) 고정 +
    - + 분 (작업 후 휴식 알림)
    - + 분 (휴식 시간 길이)
    - + +
    diff --git a/wrmc_ext/settings.js b/wrmc_ext/settings.js index 8a7288a..28f81cf 100644 --- a/wrmc_ext/settings.js +++ b/wrmc_ext/settings.js @@ -236,7 +236,9 @@ class SettingsManager { enabled: true, workTime: 60, // 분 restTime: 5, // 분 - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 }; console.log('시간 알람 설정 로드 완료:', this.timeAlarmSettings); } catch (error) { @@ -245,7 +247,9 @@ class SettingsManager { enabled: true, workTime: 60, restTime: 5, - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 }; } } @@ -321,6 +325,7 @@ class SettingsManager { const workTimeInput = document.getElementById('workTimeInput'); const restTimeInput = document.getElementById('restTimeInput'); const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); + const pomodoroToggle = document.getElementById('pomodoroToggle'); if (workTimeInput) { workTimeInput.addEventListener('change', (e) => { @@ -339,6 +344,18 @@ class SettingsManager { this.timeAlarmSettings.autoZzim = e.target.checked; }); } + + if (pomodoroToggle) { + pomodoroToggle.addEventListener('click', () => { + this.timeAlarmSettings.pomodoro = !this.timeAlarmSettings.pomodoro; + // 포모도로가 활성화되면 고정 값 적용 + if (this.timeAlarmSettings.pomodoro) { + this.timeAlarmSettings.workTime = 35; + this.timeAlarmSettings.restTime = 5; + } + this.updateTimeAlarmUI(); + }); + } } updateUI() { @@ -388,12 +405,23 @@ class SettingsManager { } } + const pomodoroToggle = document.getElementById('pomodoroToggle'); + if (pomodoroToggle) { + if (this.timeAlarmSettings.pomodoro) { + pomodoroToggle.classList.add('active'); + } else { + pomodoroToggle.classList.remove('active'); + } + } + if (workTimeInput) { workTimeInput.value = this.timeAlarmSettings.workTime; + workTimeInput.disabled = this.timeAlarmSettings.pomodoro; } if (restTimeInput) { restTimeInput.value = this.timeAlarmSettings.restTime; + restTimeInput.disabled = this.timeAlarmSettings.pomodoro; } if (autoZzimCheckbox) { @@ -412,6 +440,12 @@ class SettingsManager { this.showLoading(true); // 시간 알람 설정 업데이트 + if (this.timeAlarmSettings.pomodoro) { + // 강제 고정 값 + this.timeAlarmSettings.workTime = 35; + this.timeAlarmSettings.restTime = 5; + } + const workTimeInput = document.getElementById('workTimeInput'); const restTimeInput = document.getElementById('restTimeInput'); const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); @@ -484,10 +518,12 @@ class SettingsManager { // 시간 알람 설정 초기화 this.timeAlarmSettings = { - enabled: false, + enabled: true, workTime: 60, restTime: 5, - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 }; // 저장소에서 시간 알람 설정 제거 diff --git a/wrmc_ext/zzim.html b/wrmc_ext/zzim.html index e9d39ae..eb1e835 100644 --- a/wrmc_ext/zzim.html +++ b/wrmc_ext/zzim.html @@ -140,45 +140,168 @@ } .market-item { - border: 1px solid #ddd; - border-radius: 6px; - padding: 15px; - margin-bottom: 10px; - background: #f9f9f9; + border: 1px solid #e1e8ed; + border-radius: 12px; + padding: 20px; + margin-bottom: 15px; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .market-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + border-color: #667eea; + } + + .market-item::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea, #764ba2); + opacity: 0; + transition: opacity 0.3s ease; + } + + .market-item:hover::before { + opacity: 1; } .market-header { display: flex; justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; + } + + .market-checkbox { + display: flex; align-items: center; + gap: 8px; margin-bottom: 10px; } + .market-checkboxes { + display: flex; + flex-direction: column; + gap: 5px; + } + + .market-checkbox-input { + width: 18px; + height: 18px; + accent-color: #667eea; + cursor: pointer; + } + + .market-checkbox-label { + font-size: 12px; + font-weight: 600; + color: #2c3e50; + cursor: pointer; + user-select: none; + } + .market-info { flex: 1; + margin: 0 15px; } .market-name { font-weight: bold; color: #2c3e50; margin-bottom: 5px; + font-size: 16px; } .market-nickname { color: #666; - font-size: 12px; - margin-bottom: 5px; + font-size: 14px; + margin-bottom: 8px; + font-weight: 500; } .market-url { color: #888; font-size: 12px; word-break: break-all; + background: #f8f9fa; + padding: 6px 10px; + border-radius: 6px; + border: 1px solid #e1e8ed; + margin-bottom: 10px; + font-family: 'Courier New', monospace; + } + + .market-stats { + display: flex; + gap: 15px; + flex-wrap: wrap; + align-items: center; + } + + .zzim-count { + font-size: 12px; + color: #666; + background: #e8f4f8; + padding: 4px 8px; + border-radius: 12px; + font-weight: 500; + } + + .visibility-status { + font-size: 12px; + padding: 4px 10px; + border-radius: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .visibility-status.visible { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + .visibility-status.hidden { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + + .created-date { + font-size: 12px; + color: #888; + background: #f8f9fa; + padding: 4px 8px; + border-radius: 12px; } .market-actions { display: flex; - gap: 5px; + gap: 8px; + flex-direction: column; + } + + .market-actions .btn { + padding: 8px 12px; + font-size: 12px; + border-radius: 6px; + min-width: 70px; + font-weight: 600; + transition: all 0.2s ease; + } + + .market-actions .btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } .action-buttons { @@ -222,6 +345,257 @@ border-radius: 0 4px 4px 0 !important; border-left: none !important; } + + /* 모달 스타일 */ + .modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(3px); + animation: fadeIn 0.3s ease; + } + + .modal-content { + background-color: white; + margin: 5% auto; + padding: 0; + border: none; + border-radius: 12px; + width: 90%; + max-width: 500px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + animation: slideIn 0.3s ease; + overflow: hidden; + } + + .modal-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px 25px; + border-bottom: none; + position: relative; + } + + .modal-title { + margin: 0; + font-size: 20px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + } + + .modal-close { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: white; + font-size: 24px; + cursor: pointer; + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; + } + + .modal-close:hover { + background-color: rgba(255, 255, 255, 0.2); + } + + .modal-body { + padding: 25px; + } + + .form-group { + margin-bottom: 20px; + } + + .form-label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #2c3e50; + font-size: 14px; + } + + .form-input { + width: 100%; + padding: 12px 15px; + border: 2px solid #e1e8ed; + border-radius: 8px; + font-size: 14px; + transition: all 0.3s ease; + box-sizing: border-box; + } + + .form-input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + } + + .form-textarea { + min-height: 80px; + resize: vertical; + font-family: inherit; + } + + .modal-footer { + padding: 20px 25px; + border-top: 1px solid #e1e8ed; + display: flex; + justify-content: flex-end; + gap: 10px; + background-color: #f8f9fa; + } + + .btn-modal { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + min-width: 80px; + } + + .btn-modal-cancel { + background-color: #6c757d; + color: white; + } + + .btn-modal-cancel:hover { + background-color: #5a6268; + } + + .btn-modal-save { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + + .btn-modal-save:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + } + + .btn-modal-save:disabled { + background: #cccccc; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + /* 체크박스 스타일링 */ + .checkbox-container { + display: flex; + align-items: center; + gap: 10px; + margin-top: 15px; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border: 2px solid #e1e8ed; + transition: border-color 0.3s ease; + } + + .checkbox-container:hover { + border-color: #667eea; + } + + .custom-checkbox { + position: relative; + display: inline-block; + width: 20px; + height: 20px; + } + + .custom-checkbox input { + opacity: 0; + width: 0; + height: 0; + } + + .checkbox-checkmark { + position: absolute; + top: 0; + left: 0; + height: 20px; + width: 20px; + background-color: white; + border: 2px solid #ddd; + border-radius: 4px; + transition: all 0.3s ease; + } + + .custom-checkbox:hover input ~ .checkbox-checkmark { + border-color: #667eea; + } + + .custom-checkbox input:checked ~ .checkbox-checkmark { + background-color: #667eea; + border-color: #667eea; + } + + .checkbox-checkmark:after { + content: ""; + position: absolute; + display: none; + } + + .custom-checkbox input:checked ~ .checkbox-checkmark:after { + display: block; + } + + .custom-checkbox .checkbox-checkmark:after { + left: 6px; + top: 2px; + width: 6px; + height: 10px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + + .checkbox-label { + font-weight: 600; + color: #2c3e50; + cursor: pointer; + user-select: none; + } + + .checkbox-description { + font-size: 12px; + color: #666; + margin-top: 5px; + } @@ -268,8 +642,65 @@
    - 🎯 찜하기 작업 + 💝 찜하기 작업
    + + +
    + +
    + 체크하면 새 탭에서 백그라운드로 찜하기를 실행합니다. +
    +
    + + +
    +

    ⚙️ 찜하기 옵션

    + + +
    + +
    +
    + 기본 간격: + + ms +
    +
    + + 추가 간격: + + ms +
    +
    + = 총 간격: + 1000ms +
    +
    +
    + 💡 찜과 찜 사이의 대기 시간을 설정합니다. (1초~10초) +
    +
    + + +
    + +
    + 체크하면 전체상품(/category/ALL) 대신 최신상품(/best)부터 찜합니다. +
    +
    +
    +
    +
    + + + +
    + + + \ No newline at end of file diff --git a/wrmc_ext/zzim.js b/wrmc_ext/zzim.js index b800658..eab0d8c 100644 --- a/wrmc_ext/zzim.js +++ b/wrmc_ext/zzim.js @@ -7,6 +7,7 @@ class ZzimManager { this.access_token = null; this.user_id = null; this.zzimInProgress = false; + this.currentEditIndex = null; } async init() { @@ -21,14 +22,14 @@ class ZzimManager { this.access_token = config.zzim_config.ACCESS_TOKEN; this.user_id = config.zzim_config.USER_ID; this.SUPABASE_URL = config.zzim_config.SUPABASE_URL || "http://146.56.101.199:8000"; - this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; } else { // 기존 방식으로 폴백 const fallbackConfig = await chrome.storage.local.get(['access_token', 'user_id', 'SUPABASE_URL', 'SUPABASE_ANON_KEY']); this.access_token = fallbackConfig.access_token; this.user_id = fallbackConfig.user_id; this.SUPABASE_URL = fallbackConfig.SUPABASE_URL || "http://146.56.101.199:8000"; - this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; } if (!this.access_token || !this.user_id) { @@ -57,6 +58,12 @@ class ZzimManager { // 이벤트 바인딩 this.bindEvents(); + // 버튼 상태 초기화 + this.updateButtonStates(); + + // 총 찜하기 간격 초기화 + this.updateTotalDelay(); + console.log('ZzimManager 초기화 완료'); } catch (error) { console.error('ZzimManager 초기화 실패:', error); @@ -66,21 +73,117 @@ class ZzimManager { async initializeUserZzimSettings() { try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/rpc/initialize_user_zzim_settings`, { - method: 'POST', + console.log('사용자 찜 설정 초기화 시작:', { + user_id: this.user_id, + supabase_url: this.SUPABASE_URL + }); + + // user_markets 테이블에 사용자 레코드가 없으면 생성 + const userMarketsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' - }, - body: JSON.stringify({ user_uuid: this.user_id }) + } }); - if (!response.ok) { - console.error('찜 설정 초기화 실패:', response.status); + console.log('user_markets 조회 응답:', { + status: userMarketsResponse.status, + ok: userMarketsResponse.ok + }); + + if (userMarketsResponse.ok) { + const userMarkets = await userMarketsResponse.json(); + console.log('기존 user_markets 데이터:', userMarkets); + + if (userMarkets.length === 0) { + console.log('user_markets 초기 레코드 생성 중...'); + + // user_markets 테이블에 초기 레코드 생성 + const createResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + }, + body: JSON.stringify({ + user_id: this.user_id, + my_markets: [] + }) + }); + + console.log('user_markets 생성 응답:', { + status: createResponse.status, + ok: createResponse.ok + }); + + if (!createResponse.ok) { + const errorText = await createResponse.text(); + console.error('user_markets 생성 실패:', errorText); + + // 409 Conflict (이미 존재)는 무시하고 계속 진행 + if (createResponse.status !== 409) { + throw new Error('user_markets 레코드 생성 실패: ' + errorText); + } else { + console.log('user_markets 레코드가 이미 존재함 (409 Conflict 무시)'); + } + } else { + const createResult = await createResponse.json(); + console.log('user_markets 초기 레코드 생성 완료:', createResult); + } + } else { + console.log('기존 user_markets 레코드 존재함'); + } + } else { + console.error('user_markets 조회 실패:', userMarketsResponse.status); + const errorText = await userMarketsResponse.text(); + console.error('user_markets 조회 오류 상세:', errorText); + + // 404 Not Found인 경우 테이블이 없을 수 있으므로 계속 진행 + if (userMarketsResponse.status !== 404) { + throw new Error('user_markets 조회 실패: ' + errorText); + } } + + console.log('users 테이블 필드 초기화 중...'); + + // users 테이블의 my_zzim, zzim_mile 필드가 null이면 초기화 + const usersUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + }, + body: JSON.stringify({ + my_zzim: 0, // COALESCE 대신 직접 0으로 설정 + zzim_mile: 0 + }) + }); + + console.log('users 테이블 업데이트 응답:', { + status: usersUpdateResponse.status, + ok: usersUpdateResponse.ok + }); + + if (!usersUpdateResponse.ok) { + const errorText = await usersUpdateResponse.text(); + console.error('users 테이블 업데이트 실패:', errorText); + // users 테이블 업데이트 실패는 치명적이지 않으므로 경고만 출력 + console.warn('users 테이블 업데이트 실패, 계속 진행'); + } else { + const usersResult = await usersUpdateResponse.json(); + console.log('users 테이블 필드 초기화 완료:', usersResult); + } + + console.log('사용자 찜 설정 초기화 완료'); + } catch (error) { console.error('찜 설정 초기화 오류:', error); + this.showError('찜 설정 초기화 중 오류가 발생했습니다: ' + error.message); } } @@ -103,6 +206,30 @@ class ZzimManager { mutualZzimBtn.addEventListener('click', () => this.startMutualZzim()); } + // 찜하기 중단 버튼 + const stopZzimBtn = document.getElementById('stop-zzim-btn'); + if (stopZzimBtn) { + stopZzimBtn.addEventListener('click', () => this.stopZzim()); + } + + // 모달 취소 버튼 + const modalCancelBtn = document.getElementById('modal-cancel-btn'); + if (modalCancelBtn) { + modalCancelBtn.addEventListener('click', () => this.closeEditModal()); + } + + // 모달 저장 버튼 + const modalSaveBtn = document.getElementById('modal-save-btn'); + if (modalSaveBtn) { + modalSaveBtn.addEventListener('click', () => this.saveMarketEdit()); + } + + // 모달 닫기 버튼 (×) + const modalCloseBtn = document.getElementById('modal-close-btn'); + if (modalCloseBtn) { + modalCloseBtn.addEventListener('click', () => this.closeEditModal()); + } + // 마켓 URL 입력 시 자동 변환 const marketUrlInput = document.getElementById('market-url'); if (marketUrlInput) { @@ -117,11 +244,87 @@ class ZzimManager { } }); } + + // 찜하기 속도 설정 이벤트 + const userDelayInput = document.getElementById('user-delay'); + if (userDelayInput) { + userDelayInput.addEventListener('input', () => this.updateTotalDelay()); + } + + // 디버깅용 테스트 버튼 (있는 경우에만) + const testBtn = document.getElementById('test-debug-btn'); + if (testBtn) { + testBtn.addEventListener('click', () => this.testDebug()); + } + } + + // 총 찜하기 간격 업데이트 + updateTotalDelay() { + const baseDelay = parseInt(document.getElementById('base-delay')?.value || 1000); + const userDelay = parseInt(document.getElementById('user-delay')?.value || 0); + const totalDelay = baseDelay + userDelay; + + const totalDelayEl = document.getElementById('total-delay'); + if (totalDelayEl) { + totalDelayEl.textContent = `${totalDelay}ms`; + } + } + + // 찜하기 설정 가져오기 + getZzimSettings() { + const baseDelay = parseInt(document.getElementById('base-delay')?.value || 1000); + const userDelay = parseInt(document.getElementById('user-delay')?.value || 0); + const latestFirst = document.getElementById('latest-first')?.checked || false; + const backgroundMode = document.getElementById('background-mode')?.checked || false; + + return { + totalDelay: baseDelay + userDelay, + latestFirst: latestFirst, + backgroundMode: backgroundMode + }; + } + + // 마켓 URL 생성 (최신상품 우선 옵션 고려) + generateMarketUrl(marketUrl, latestFirst = false) { + try { + // 기본 마켓 URL에서 마지막 슬래시 제거 + const baseUrl = marketUrl.replace(/\/$/, ''); + + console.log('[ZzimManager] 마켓 URL 생성:', { + baseUrl: baseUrl, + latestFirst: latestFirst + }); + + // 네이버 스마트스토어 URL 구조 분석 + const urlParts = baseUrl.split('/'); + const storeName = urlParts[urlParts.length - 1]; + + let targetUrl; + + if (latestFirst) { + // 최신상품 우선: /best?cp=1 + targetUrl = `${baseUrl}/best?cp=1`; + } else { + // 전체상품: /category/ALL?cp=1 + targetUrl = `${baseUrl}/category/ALL?cp=1`; + } + + console.log('[ZzimManager] 생성된 마켓 URL:', targetUrl); + return targetUrl; + + } catch (error) { + console.error('[ZzimManager] 마켓 URL 생성 오류:', error); + // 오류 발생 시 기본 URL 반환 + return marketUrl + (latestFirst ? '/best?cp=1' : '/category/ALL?cp=1'); + } } async loadZzimStats() { try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { + console.log('찜 통계 로드 시작...'); + + // 1. 사용자 기본 정보 및 회원등급 조회 (users 테이블) + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=my_zzim,zzim_mile,available_zzim_mile,membership_level,today_zzim_count,today_zzim_date`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -129,38 +332,371 @@ class ZzimManager { } }); - if (!response.ok) { - throw new Error(`찜 통계 로드 실패: ${response.status}`); + if (!userRes.ok) { + throw new Error('사용자 통계 정보를 불러올 수 없습니다.'); } - const data = await response.json(); - const stats = data[0] || { - current_daily_count: 0, - daily_zzim_limit: 100, - current_mileage: 0, - zzim_mileage_limit: 1000 + const userData = await userRes.json(); + const userStats = userData[0] || {}; + console.log('users 테이블에서 로드된 통계:', userStats); + + // 2. 회원등급별 제한 조회 + const membershipLevel = userStats.membership_level || 'basic'; + + const limitsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/membership_levels?level=eq.${membershipLevel}`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + let limits = { + daily_zzim_limit: 50, + max_zzim_mileage: 500, + mileage_per_zzim: 1, + max_markets: 5, + mutual_zzim_enabled: true, + background_zzim_enabled: false }; - // UI 업데이트 - HTML의 실제 ID와 일치하도록 수정 + if (limitsResponse.ok) { + const limitsData = await limitsResponse.json(); + if (limitsData.length > 0) { + // membership_levels 테이블에는 제한 필드만 있으므로 기본 limits 객체와 병합 + limits = { ...limits, ...limitsData[0] }; + } + } else { + console.warn('회원등급별 제한 조회 실패, 기본값 사용'); + } + + // 3. 오늘 찜한 개수 확인 및 리셋 + const today = new Date().toISOString().split('T')[0]; + const lastZzimDate = userStats.today_zzim_date; + let todayZzimCount = userStats.today_zzim_count || 0; + + // 날짜가 바뀌었으면 리셋 + if (lastZzimDate !== today) { + console.log('날짜 변경 감지, 일일 찜 카운트 리셋'); + + // users 테이블에 오늘 카운트를 0으로 초기화 + const resetUsersResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + today_zzim_count: 0, + today_zzim_date: today + }) + }); + + if (resetUsersResponse.ok) { + todayZzimCount = 0; + console.log('일일 찜 카운트 리셋 완료'); + } + } + + // 4. 사용 가능한 찜 마일리지 계산 + const totalZzimMile = userStats.zzim_mile || 0; + const availableZzimMile = userStats.available_zzim_mile || totalZzimMile; + + // 5. UI 업데이트 const todayCountEl = document.getElementById('today-zzim-count'); const todayLimitEl = document.getElementById('today-zzim-limit'); const mileageCountEl = document.getElementById('zzim-mileage'); const mileageLimitEl = document.getElementById('zzim-mileage-limit'); + const totalZzimEl = document.getElementById('total-zzim-received'); - if (todayCountEl) todayCountEl.textContent = stats.current_daily_count; - if (todayLimitEl) todayLimitEl.textContent = stats.daily_zzim_limit; - if (mileageCountEl) mileageCountEl.textContent = stats.current_mileage; - if (mileageLimitEl) mileageLimitEl.textContent = stats.zzim_mileage_limit; + if (todayCountEl) todayCountEl.textContent = todayZzimCount; + if (todayLimitEl) todayLimitEl.textContent = limits.daily_zzim_limit; + if (mileageCountEl) mileageCountEl.textContent = availableZzimMile; + if (mileageLimitEl) mileageLimitEl.textContent = limits.max_zzim_mileage; + if (totalZzimEl) totalZzimEl.textContent = userStats.my_zzim || 0; + + // 6. 진행률 표시 + const todayProgress = (todayZzimCount / limits.daily_zzim_limit) * 100; + const mileageProgress = (availableZzimMile / limits.max_zzim_mileage) * 100; + + // 진행률 바 업데이트 (있는 경우) + const todayProgressBar = document.getElementById('today-progress-bar'); + const mileageProgressBar = document.getElementById('mileage-progress-bar'); + + if (todayProgressBar) { + todayProgressBar.style.width = `${Math.min(todayProgress, 100)}%`; + todayProgressBar.className = `progress-bar ${todayProgress >= 100 ? 'full' : todayProgress >= 80 ? 'warning' : 'normal'}`; + } + + if (mileageProgressBar) { + mileageProgressBar.style.width = `${Math.min(mileageProgress, 100)}%`; + mileageProgressBar.className = `progress-bar ${mileageProgress >= 100 ? 'full' : mileageProgress >= 80 ? 'warning' : 'normal'}`; + } + + // 7. 제한 상태 표시 + const todayLimitReached = todayZzimCount >= limits.daily_zzim_limit; + const mileageLimitReached = availableZzimMile >= limits.max_zzim_mileage; + + // 버튼 상태 업데이트 + const myMarketBtn = document.getElementById('my-market-zzim-btn'); + const mutualZzimBtn = document.getElementById('mutual-zzim-btn'); + const backgroundModeCheckbox = document.getElementById('background-mode'); + + if (myMarketBtn) { + myMarketBtn.disabled = todayLimitReached; + myMarketBtn.title = todayLimitReached ? `일일 찜 제한 도달 (${todayZzimCount}/${limits.daily_zzim_limit})` : ''; + } + + if (mutualZzimBtn) { + mutualZzimBtn.disabled = !limits.mutual_zzim_enabled || mileageLimitReached; + mutualZzimBtn.title = !limits.mutual_zzim_enabled + ? '품앗이 기능이 비활성화되어 있습니다.' + : mileageLimitReached + ? `마일리지 한도 도달 (${availableZzimMile}/${limits.max_zzim_mileage})` + : ''; + } + + if (backgroundModeCheckbox) { + backgroundModeCheckbox.disabled = !limits.background_zzim_enabled; + if (!limits.background_zzim_enabled) { + backgroundModeCheckbox.checked = false; + backgroundModeCheckbox.title = '백그라운드 모드는 프리미엄/VIP 회원만 사용 가능합니다.'; + } + } + + // 8. 상태 메시지 표시 + const statusMessage = document.getElementById('zzim-status-message'); + if (statusMessage) { + if (todayLimitReached) { + statusMessage.innerHTML = `⚠️ 오늘 찜 제한에 도달했습니다. (${todayZzimCount}/${limits.daily_zzim_limit})`; + statusMessage.className = 'status-warning'; + } else if (mileageLimitReached) { + statusMessage.innerHTML = `⚠️ 찜 마일리지 한도에 도달했습니다. (${availableZzimMile}/${limits.max_zzim_mileage})`; + statusMessage.className = 'status-warning'; + } else { + statusMessage.innerHTML = `✅ 찜하기 가능 (오늘: ${todayZzimCount}/${limits.daily_zzim_limit}, 마일리지: ${availableZzimMile}/${limits.max_zzim_mileage})`; + statusMessage.className = 'status-success'; + } + } + + console.log('찜 통계 로드 완료:', { + membershipLevel, + todayZzimCount, + dailyLimit: limits.daily_zzim_limit, + availableZzimMile, + maxZzimMileage: limits.max_zzim_mileage, + totalReceived: userStats.my_zzim, + mutualEnabled: limits.mutual_zzim_enabled, + backgroundEnabled: limits.background_zzim_enabled + }); + + // 현재 제한 정보를 인스턴스 변수에 저장 + this.currentLimits = limits; + this.currentStats = { + todayZzimCount, + availableZzimMile, + totalReceived: userStats.my_zzim || 0 + }; } catch (error) { console.error('찜 통계 로드 오류:', error); - this.showError('찜 통계를 불러올 수 없습니다.'); + this.showError('찜 통계를 불러올 수 없습니다: ' + error.message); } } async loadMyMarkets() { try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true&order=created_at.desc`, { + console.log('마켓 목록 로드 시작:', { + user_id: this.user_id, + supabase_url: this.SUPABASE_URL, + has_token: !!this.access_token + }); + + // user_markets 테이블에서 my_markets JSONB 필드 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + console.log('마켓 목록 로드 응답:', { + status: response.status, + ok: response.ok, + statusText: response.statusText + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('마켓 목록 로드 실패 상세:', errorText); + throw new Error(`마켓 목록 로드 실패: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + console.log('마켓 목록 로드 데이터:', data); + + const markets = data[0]?.my_markets || []; + console.log('파싱된 마켓 목록:', markets); + + // 찜관리 창에서는 모든 마켓을 보여줌 (노출여부 관계없이) + // 생성일 기준으로 최신순 정렬 + const sortedMarkets = markets.sort((a, b) => { + const dateA = new Date(a.created_at || 0); + const dateB = new Date(b.created_at || 0); + return dateB - dateA; + }); + + console.log('정렬된 마켓 목록:', sortedMarkets); + + this.renderMarketsList(sortedMarkets); + + } catch (error) { + console.error('마켓 목록 로드 오류:', error); + this.showError('마켓 목록을 불러올 수 없습니다: ' + error.message); + } + } + + renderMarketsList(markets) { + console.log('마켓 목록 렌더링 시작:', markets); + + const marketsList = document.getElementById('my-markets-list'); + if (!marketsList) { + console.error('my-markets-list 엘리먼트를 찾을 수 없습니다.'); + return; + } + + if (markets.length === 0) { + console.log('등록된 마켓이 없음'); + marketsList.innerHTML = '

    등록된 마켓이 없습니다.

    '; + return; + } + + console.log('마켓 목록 HTML 생성 중...'); + + const marketHTML = markets.map((market, index) => { + console.log(`마켓 ${index} 렌더링:`, market); + + // 고유 식별자 생성 (마켓 URL + 생성일시) + const marketId = `${market.market_url}_${market.created_at}`; + + return ` +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    ${market.market_name || '마켓명 없음'}
    +
    ${market.market_nickname || '별명 없음'}
    +
    ${this.truncateUrl(market.market_url || '')}
    +
    + 찜받은수: ${market.zzim_received_count || 0}개 + + ${market.is_visible !== false ? '노출중' : '숨김'} + + 등록일: ${this.formatDate(market.created_at)} +
    +
    +
    + + + +
    +
    +
    + `; + }).join(''); + + marketsList.innerHTML = marketHTML; + + // 이벤트 리스너 바인딩 + this.bindMarketEvents(); + + console.log('마켓 목록 렌더링 완료'); + } + + // 마켓 관련 이벤트 바인딩 + bindMarketEvents() { + // 체크박스 이벤트 + const checkboxes = document.querySelectorAll('.market-checkbox-input'); + checkboxes.forEach(checkbox => { + checkbox.addEventListener('change', (e) => { + const marketId = e.target.dataset.marketId; + const checkboxType = e.target.dataset.checkboxType; + this.toggleMarketCheckbox(marketId, checkboxType, e.target.checked); + }); + }); + + // 수정 버튼 이벤트 + const editBtns = document.querySelectorAll('.market-edit-btn'); + editBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + const marketId = e.target.dataset.marketId; + this.editMarket(marketId); + }); + }); + + // 노출/숨김 버튼 이벤트 + const visibilityBtns = document.querySelectorAll('.market-visibility-btn'); + visibilityBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + const marketId = e.target.dataset.marketId; + this.toggleMarketVisibility(marketId); + }); + }); + + // 삭제 버튼 이벤트 + const deleteBtns = document.querySelectorAll('.market-delete-btn'); + deleteBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + const marketId = e.target.dataset.marketId; + this.deleteMarket(marketId); + }); + }); + } + + // URL을 적절한 길이로 자르기 + truncateUrl(url) { + if (!url) return ''; + if (url.length <= 50) return url; + return url.substring(0, 47) + '...'; + } + + // 날짜 포맷팅 + formatDate(dateString) { + if (!dateString) return '알 수 없음'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + } catch (error) { + return '알 수 없음'; + } + } + + // 마켓 체크박스 토글 (통합) + async toggleMarketCheckbox(marketId, checkboxType, isChecked) { + try { + // 기존 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -169,49 +705,75 @@ class ZzimManager { }); if (!response.ok) { - throw new Error(`마켓 목록 로드 실패: ${response.status}`); + throw new Error('마켓 목록을 가져올 수 없습니다.'); } - const markets = await response.json(); - this.renderMarketsList(markets); + const data = await response.json(); + const markets = data[0]?.my_markets || []; + + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); + return; + } + + // 체크박스 타입에 따라 해당 필드 업데이트 + if (checkboxType === 'for_zzim') { + market.for_zzim = isChecked; + } else if (checkboxType === 'for_mutual_zzim') { + market.for_mutual_zzim = isChecked; + } + + market.updated_at = new Date().toISOString(); + + // user_markets 테이블 업데이트 + const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + my_markets: markets + }) + }); + + if (!updateResponse.ok) { + throw new Error('마켓 설정 변경 실패'); + } + + const statusMessage = checkboxType === 'for_zzim' + ? (isChecked ? '내 찜하기 대상에 포함' : '내 찜하기 대상에서 제외') + : (isChecked ? '품앗이 대상에 포함' : '품앗이 대상에서 제외'); + + this.showSuccess(`마켓이 ${statusMessage}되었습니다.`); } catch (error) { - console.error('마켓 목록 로드 오류:', error); - this.showError('마켓 목록을 불러올 수 없습니다.'); + console.error('마켓 설정 변경 오류:', error); + this.showError('마켓 설정 변경 중 오류가 발생했습니다.'); + + // 오류 발생 시 체크박스 상태 원복 + const checkbox = document.querySelector(`[data-market-id="${marketId}"][data-checkbox-type="${checkboxType}"]`); + if (checkbox) { + checkbox.checked = !isChecked; + } } } - renderMarketsList(markets) { - const marketsList = document.getElementById('my-markets-list'); - if (!marketsList) return; - - if (markets.length === 0) { - marketsList.innerHTML = '

    등록된 마켓이 없습니다.

    '; - return; - } - - marketsList.innerHTML = markets.map(market => ` -
    -
    -
    -
    ${market.market_name}
    -
    ${market.market_nickname}
    -
    ${market.market_url}
    -
    -
    - - -
    -
    -
    - `).join(''); - } - async addMarket() { const marketUrl = document.getElementById('market-url').value.trim(); const marketName = document.getElementById('market-name').value.trim(); const marketNickname = document.getElementById('market-nickname').value.trim(); + console.log('마켓 추가 시작:', { + marketUrl, + marketName, + marketNickname + }); + if (!marketUrl || !marketName || !marketNickname) { this.showError('모든 필드를 입력해주세요.'); return; @@ -223,7 +785,39 @@ class ZzimManager { } try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets`, { + console.log('기존 마켓 목록 조회 중...'); + + // 기존 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + console.log('기존 마켓 목록 조회 응답:', { + status: response.status, + ok: response.ok + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('기존 마켓 목록 조회 실패:', errorText); + throw new Error('기존 마켓 목록을 가져올 수 없습니다: ' + errorText); + } + + const data = await response.json(); + console.log('기존 마켓 데이터:', data); + + let existingMarkets = []; + + // 데이터가 없는 경우 처리 + if (!data || data.length === 0) { + console.log('기존 user_markets 레코드가 없음, 새로 생성 필요'); + + // user_markets 레코드가 없으면 먼저 생성 + const createResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.access_token}`, @@ -232,25 +826,109 @@ class ZzimManager { }, body: JSON.stringify({ user_id: this.user_id, + my_markets: [] + }) + }); + + console.log('user_markets 레코드 생성 응답:', { + status: createResponse.status, + ok: createResponse.ok + }); + + if (!createResponse.ok) { + const createErrorText = await createResponse.text(); + console.error('user_markets 레코드 생성 실패:', createErrorText); + throw new Error('사용자 마켓 레코드 생성 실패: ' + createErrorText); + } + + existingMarkets = []; + } else { + existingMarkets = data[0]?.my_markets || []; + } + + console.log('기존 마켓 목록:', existingMarkets); + + // 새 마켓 객체 생성 + const newMarket = { market_url: marketUrl, market_name: marketName, - market_nickname: marketNickname + market_nickname: marketNickname, + is_visible: true, // 다른 사람에게 노출 (기본값: 노출) + for_zzim: true, // 내가 찜하기 할 때 포함 (기본값: 포함) + for_mutual_zzim: true, // 품앗이 대상 여부 (기본값: 포함) + zzim_received_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + console.log('새 마켓 객체:', newMarket); + + // 중복 URL 확인 + const isDuplicate = existingMarkets.some(market => market.market_url === marketUrl); + if (isDuplicate) { + console.log('중복 URL 발견'); + this.showError('이미 등록된 마켓 URL입니다.'); + return; + } + + // 새 마켓 추가 + const updatedMarkets = [...existingMarkets, newMarket]; + console.log('업데이트된 마켓 목록:', updatedMarkets); + + // user_markets 테이블 업데이트 + console.log('마켓 목록 업데이트 요청 중...'); + + const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + }, + body: JSON.stringify({ + my_markets: updatedMarkets }) }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || '마켓 추가 실패'); + console.log('마켓 목록 업데이트 응답:', { + status: updateResponse.status, + ok: updateResponse.ok, + statusText: updateResponse.statusText + }); + + if (!updateResponse.ok) { + const errorData = await updateResponse.text(); + console.error('마켓 추가 실패 상세:', errorData); + throw new Error('마켓 추가 실패: ' + errorData); } + // 응답 데이터 확인 + const updateResult = await updateResponse.json(); + console.log('업데이트 결과:', updateResult); + + console.log('마켓 추가 성공, 입력 필드 초기화 중...'); + // 입력 필드 초기화 document.getElementById('market-url').value = ''; document.getElementById('market-name').value = ''; document.getElementById('market-nickname').value = ''; - // 마켓 목록 다시 로드 + console.log('마켓 목록 다시 로드 중...'); + + // 약간의 지연 후 마켓 목록 다시 로드 (DB 반영 시간 고려) + setTimeout(async () => { await this.loadMyMarkets(); this.showSuccess('마켓이 추가되었습니다.'); + + // 새로 추가된 마켓을 위로 스크롤 + setTimeout(() => { + const marketsList = document.getElementById('my-markets-list'); + if (marketsList) { + marketsList.scrollTop = 0; + } + }, 100); + }, 500); } catch (error) { console.error('마켓 추가 오류:', error); @@ -259,47 +937,193 @@ class ZzimManager { } async editMarket(marketId) { - // 편집 모달을 표시하거나 인라인 편집 구현 - const newName = prompt('새로운 마켓 이름을 입력하세요:'); - if (!newName) return; - - const newNickname = prompt('새로운 마켓 별명을 입력하세요:'); - if (!newNickname) return; - try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, { - method: 'PATCH', + // 기존 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - market_name: newName, - market_nickname: newNickname - }) + } }); if (!response.ok) { - throw new Error('마켓 수정 실패'); + throw new Error('마켓 목록을 가져올 수 없습니다.'); } - await this.loadMyMarkets(); - this.showSuccess('마켓이 수정되었습니다.'); + const data = await response.json(); + const markets = data[0]?.my_markets || []; + + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); + return; + } + + // 모달에 기존 데이터 채우기 + this.currentEditIndex = markets.indexOf(market); + this.openEditModal(market); } catch (error) { console.error('마켓 수정 오류:', error); - this.showError('마켓 수정 중 오류가 발생했습니다.'); + this.showError('마켓 정보를 불러올 수 없습니다.'); } } - async deleteMarket(marketId) { - if (!confirm('정말로 이 마켓을 삭제하시겠습니까?')) { + // 모달 열기 + openEditModal(market) { + const modal = document.getElementById('edit-market-modal'); + const urlInput = document.getElementById('edit-market-url'); + const nameInput = document.getElementById('edit-market-name'); + const nicknameInput = document.getElementById('edit-market-nickname'); + const forZzimCheckbox = document.getElementById('edit-market-for-zzim'); + const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim'); + const visibleCheckbox = document.getElementById('edit-market-visible'); + + // 기존 데이터로 폼 채우기 + if (urlInput) urlInput.value = market.market_url || ''; + if (nameInput) nameInput.value = market.market_name || ''; + if (nicknameInput) nicknameInput.value = market.market_nickname || ''; + if (forZzimCheckbox) forZzimCheckbox.checked = market.for_zzim !== false; + if (forMutualZzimCheckbox) forMutualZzimCheckbox.checked = market.for_mutual_zzim !== false; + if (visibleCheckbox) visibleCheckbox.checked = market.is_visible !== false; + + // 모달 표시 + if (modal) { + modal.style.display = 'block'; + + // 첫 번째 입력 필드에 포커스 + setTimeout(() => { + if (nameInput) nameInput.focus(); + }, 100); + } + + // ESC 키로 모달 닫기 + this.modalEscapeHandler = (e) => { + if (e.key === 'Escape') { + this.closeEditModal(); + } + }; + document.addEventListener('keydown', this.modalEscapeHandler); + + // 모달 배경 클릭으로 닫기 + this.modalClickHandler = (e) => { + if (e.target === modal) { + this.closeEditModal(); + } + }; + modal.addEventListener('click', this.modalClickHandler); + } + + // 모달 닫기 + closeEditModal() { + const modal = document.getElementById('edit-market-modal'); + + if (modal) { + modal.style.display = 'none'; + } + + // 이벤트 리스너 제거 + if (this.modalEscapeHandler) { + document.removeEventListener('keydown', this.modalEscapeHandler); + this.modalEscapeHandler = null; + } + + if (this.modalClickHandler) { + modal.removeEventListener('click', this.modalClickHandler); + this.modalClickHandler = null; + } + + // 편집 인덱스 초기화 + this.currentEditIndex = null; + } + + // 마켓 수정 저장 + async saveMarketEdit() { + const urlInput = document.getElementById('edit-market-url'); + const nameInput = document.getElementById('edit-market-name'); + const nicknameInput = document.getElementById('edit-market-nickname'); + const forZzimCheckbox = document.getElementById('edit-market-for-zzim'); + const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim'); + const visibleCheckbox = document.getElementById('edit-market-visible'); + const saveBtn = document.querySelector('.btn-modal-save'); + + const marketUrl = urlInput?.value.trim() || ''; + const marketName = nameInput?.value.trim() || ''; + const marketNickname = nicknameInput?.value.trim() || ''; + const forZzim = forZzimCheckbox?.checked || false; + const forMutualZzim = forMutualZzimCheckbox?.checked || false; + const isVisible = visibleCheckbox?.checked || false; + + // 유효성 검사 + if (!marketUrl || !marketName || !marketNickname) { + this.showError('모든 필드를 입력해주세요.'); + return; + } + + if (!marketUrl.startsWith('https://smartstore.naver.com/')) { + this.showError('올바른 네이버 스마트스토어 URL을 입력해주세요.'); + return; + } + + if (this.currentEditIndex === null) { + this.showError('편집할 마켓 정보를 찾을 수 없습니다.'); return; } try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, { + // 저장 버튼 비활성화 + if (saveBtn) { + saveBtn.disabled = true; + saveBtn.textContent = '저장 중...'; + } + + // 기존 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('마켓 목록을 가져올 수 없습니다.'); + } + + const data = await response.json(); + const markets = data[0]?.my_markets || []; + + if (this.currentEditIndex < 0 || this.currentEditIndex >= markets.length) { + throw new Error('잘못된 마켓 인덱스입니다.'); + } + + // URL 중복 검사 (자기 자신 제외) + const isDuplicate = markets.some((market, index) => + index !== this.currentEditIndex && market.market_url === marketUrl + ); + + if (isDuplicate) { + throw new Error('이미 등록된 마켓 URL입니다.'); + } + + // 마켓 정보 업데이트 + const existingMarket = markets[this.currentEditIndex]; + markets[this.currentEditIndex] = { + ...existingMarket, + market_url: marketUrl, + market_name: marketName, + market_nickname: marketNickname, + for_zzim: forZzim, + for_mutual_zzim: forMutualZzim, + is_visible: isVisible, + updated_at: new Date().toISOString() + }; + + // user_markets 테이블 업데이트 + const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, @@ -307,11 +1131,135 @@ class ZzimManager { 'Content-Type': 'application/json' }, body: JSON.stringify({ - is_active: false + my_markets: markets }) }); + if (!updateResponse.ok) { + const errorText = await updateResponse.text(); + throw new Error('마켓 수정 실패: ' + errorText); + } + + // 성공 처리 + this.closeEditModal(); + await this.loadMyMarkets(); + this.showSuccess('마켓 정보가 성공적으로 수정되었습니다.'); + + } catch (error) { + console.error('마켓 수정 저장 오류:', error); + this.showError('마켓 수정 중 오류가 발생했습니다: ' + error.message); + } finally { + // 저장 버튼 복원 + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.textContent = '저장'; + } + } + } + + async toggleMarketVisibility(marketId) { + try { + // 기존 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + if (!response.ok) { + throw new Error('마켓 목록을 가져올 수 없습니다.'); + } + + const data = await response.json(); + const markets = data[0]?.my_markets || []; + + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); + return; + } + + // 노출 상태 토글 + market.is_visible = !market.is_visible; + market.updated_at = new Date().toISOString(); + + // user_markets 테이블 업데이트 + const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + my_markets: markets + }) + }); + + if (!updateResponse.ok) { + throw new Error('마켓 노출 설정 변경 실패'); + } + + await this.loadMyMarkets(); + this.showSuccess(`마켓이 ${market.is_visible ? '노출' : '숨김'} 상태로 변경되었습니다.`); + + } catch (error) { + console.error('마켓 노출 설정 변경 오류:', error); + this.showError('마켓 노출 설정 변경 중 오류가 발생했습니다.'); + } + } + + async deleteMarket(marketId) { + try { + // 기존 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('마켓 목록을 가져올 수 없습니다.'); + } + + const data = await response.json(); + const markets = data[0]?.my_markets || []; + + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); + return; + } + + if (!confirm(`정말로 "${market.market_nickname}" 마켓을 삭제하시겠습니까?`)) { + return; + } + + // 마켓 삭제 (배열에서 제거) + markets.splice(markets.indexOf(market), 1); + + // user_markets 테이블 업데이트 + const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + my_markets: markets + }) + }); + + if (!updateResponse.ok) { throw new Error('마켓 삭제 실패'); } @@ -326,13 +1274,644 @@ class ZzimManager { async startMyMarketZzim() { if (this.zzimInProgress) { - this.showError('찜하기가 이미 진행 중입니다.'); + this.showError('이미 찜하기가 진행 중입니다.'); return; } try { - // 내 마켓 목록 가져오기 - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true`, { + this.zzimInProgress = true; + this.updateButtonStates(); + + const markets = await this.getActiveMarkets(); + + if (markets.length === 0) { + this.showError('등록된 마켓이 없습니다. 먼저 마켓을 등록해주세요.'); + return; + } + + const settings = this.getZzimSettings(); + this.showProgress(0, markets.length, '내 마켓 찜하기 준비 중...'); + + for (let i = 0; i < markets.length; i++) { + if (!this.zzimInProgress) { + this.showError('찜하기가 중단되었습니다.'); + break; + } + + const market = markets[i]; + this.showProgress(i, markets.length, `${market.market_nickname} 찜하기 중...`); + + await this.executeZzim(market, 'my_market', settings); + + this.showProgress(i + 1, markets.length, `${market.market_nickname} 완료`); + + // 마켓 간 대기 시간 (설정된 속도 적용) + if (i < markets.length - 1 && this.zzimInProgress) { + this.showProgress(i + 1, markets.length, `다음 마켓 대기 중... (${settings.totalDelay}ms)`); + await new Promise(resolve => setTimeout(resolve, settings.totalDelay)); + } + } + + if (this.zzimInProgress) { + this.hideProgress(); + this.showSuccess(`내 마켓 찜하기 완료! ${markets.length}개 마켓 처리됨`); + setTimeout(() => { + window.close(); + }, 2000); // 2초 후 창 닫기 + } + + } catch (error) { + console.error('내 마켓 찜하기 오류:', error); + this.hideProgress(); + this.showError('내 마켓 찜하기 중 오류가 발생했습니다.'); + } finally { + this.zzimInProgress = false; + this.updateButtonStates(); + } + } + + async startMutualZzim() { + if (this.zzimInProgress) { + this.showError('이미 찜하기가 진행 중입니다.'); + return; + } + + try { + this.zzimInProgress = true; + this.updateButtonStates(); + + // 품앗이 마켓 목록 가져오기 + const mutualMarkets = await this.getMutualMarkets(); + + if (mutualMarkets.length === 0) { + this.showError('품앗이 마켓이 없습니다.'); + return; + } + + const settings = this.getZzimSettings(); + this.showProgress(0, mutualMarkets.length, '품앗이 찜하기 준비 중...'); + + for (let i = 0; i < mutualMarkets.length; i++) { + if (!this.zzimInProgress) { + this.showError('찜하기가 중단되었습니다.'); + break; + } + + const market = mutualMarkets[i]; + this.showProgress(i, mutualMarkets.length, `${market.market_nickname} 품앗이 중...`); + + await this.executeZzim(market, 'mutual', settings); + + this.showProgress(i + 1, mutualMarkets.length, `${market.market_nickname} 완료`); + + // 마켓 간 대기 시간 (설정된 속도 적용) + if (i < mutualMarkets.length - 1 && this.zzimInProgress) { + this.showProgress(i + 1, mutualMarkets.length, `다음 마켓 대기 중... (${settings.totalDelay}ms)`); + await new Promise(resolve => setTimeout(resolve, settings.totalDelay)); + } + } + + if (this.zzimInProgress) { + this.hideProgress(); + this.showSuccess(`품앗이 찜하기 완료! ${mutualMarkets.length}개 마켓 처리됨`); + setTimeout(() => { + window.close(); + }, 2000); // 2초 후 창 닫기 + } + + } catch (error) { + console.error('품앗이 찜하기 오류:', error); + this.hideProgress(); + this.showError('품앗이 찜하기 중 오류가 발생했습니다.'); + } finally { + this.zzimInProgress = false; + this.updateButtonStates(); + } + } + + async executeZzim(market, zzimType, settings) { + this.zzimInProgress = true; + + try { + // 마켓 URL 생성 (최신상품 우선 옵션 적용) + const targetUrl = this.generateMarketUrl(market.market_url, settings.latestFirst); + + if (settings.backgroundMode) { + // 백그라운드 실행을 위해 background.js로 메시지 전송 + const response = await chrome.runtime.sendMessage({ + action: 'executeBackgroundZzim', + market: { + ...market, + target_url: targetUrl + }, + zzimType: zzimType, + userId: this.user_id, + accessToken: this.access_token, + settings: settings + }); + + if (response.success) { + this.showSuccess(`백그라운드 찜하기가 시작되었습니다. (${market.market_nickname})`); + await this.recordZzim(market, zzimType); + } else { + throw new Error(response.error || '백그라운드 찜하기 실행 실패'); + } + } else { + // 포그라운드에서 실행 - 로그인 상태 확인 후 처리 + await this.checkLoginAndExecute(market, targetUrl, settings, zzimType); + } + + } catch (error) { + console.error('찜하기 실행 오류:', error); + this.showError('찜하기 실행 중 오류가 발생했습니다: ' + error.message); + } finally { + this.zzimInProgress = false; + } + } + + // 로그인 상태 확인 및 찜하기 실행 + async checkLoginAndExecute(market, targetUrl, settings, zzimType) { + try { + // 먼저 마켓 페이지에 접근해서 로그인 상태 확인 + const testUrl = market.market_url; + + // 현재 탭에서 마켓으로 이동하여 로그인 상태 확인 + const urlType = settings.latestFirst ? '최신상품' : '전체상품'; + + // 로그인 확인을 위한 모달 표시 + this.showLoginCheckModal(market, targetUrl, settings, zzimType, urlType); + + } catch (error) { + console.error('로그인 확인 오류:', error); + this.showError('로그인 상태 확인 중 오류가 발생했습니다.'); + } + } + + // 로그인 확인 모달 표시 + showLoginCheckModal(market, targetUrl, settings, zzimType, urlType) { + // 기존 모달이 있으면 제거 + this.removeLoginModal(); + + // 모달 HTML 생성 + const modalHTML = ` +
    +
    +

    🔐 로그인 확인

    + +

    + ${market.market_nickname} 마켓에서 찜하기를 실행합니다.
    + (${urlType} 모드) +

    + +
    +

    + 네이버에 로그인되어 있지 않으면 자동으로 로그인 페이지로 이동됩니다.
    + 로그인 후 다시 찜하기를 실행해 주세요. +

    +
    + +
    + + + +
    +
    +
    + `; + + // 모달을 body에 추가 + document.body.insertAdjacentHTML('beforeend', modalHTML); + + // 이벤트 리스너 추가 + const proceedBtn = document.getElementById('login-proceed-btn'); + const cancelBtn = document.getElementById('login-cancel-btn'); + const modal = document.getElementById('login-check-modal'); + + if (proceedBtn) { + proceedBtn.addEventListener('click', () => { + this.removeLoginModal(); + this.proceedWithZzim(market, targetUrl, settings, zzimType); + }); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + this.removeLoginModal(); + this.showStatus('찜하기가 취소되었습니다.'); + }); + } + + // ESC 키로 모달 닫기 + const escapeHandler = (e) => { + if (e.key === 'Escape') { + this.removeLoginModal(); + this.showStatus('찜하기가 취소되었습니다.'); + document.removeEventListener('keydown', escapeHandler); + } + }; + document.addEventListener('keydown', escapeHandler); + + // 모달 배경 클릭으로 닫기 + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + this.removeLoginModal(); + this.showStatus('찜하기가 취소되었습니다.'); + } + }); + } + } + + // 로그인 모달 제거 + removeLoginModal() { + const modal = document.getElementById('login-check-modal'); + if (modal) { + modal.remove(); + } + } + + // 찜하기 진행 (로그인 리다이렉트 포함) + async proceedWithZzim(market, targetUrl, settings, zzimType) { + try { + // 마켓 URL에서 스토어 ID 추출 + const storeId = market.market_url.split('/').pop(); + + // 올바른 네이버 스마트스토어 URL 생성 + let finalTargetUrl = targetUrl; + + // URL에 찜하기 파라미터 추가 + const urlObj = new URL(finalTargetUrl); + urlObj.searchParams.set('auto_zzim', 'true'); + urlObj.searchParams.set('max_zzim', '50'); + + // 네이버 스마트스토어는 cp 파라미터를 사용 (page가 아님) + if (!urlObj.searchParams.has('cp')) { + urlObj.searchParams.set('cp', '1'); // 첫 번째 페이지부터 시작 + } + + finalTargetUrl = urlObj.toString(); + + console.log('[ZzimManager] 최종 찜하기 URL:', finalTargetUrl); + + // 로그인 리다이렉트 URL 생성 + const loginRedirectUrl = `https://nid.naver.com/nidlogin.login?url=${encodeURIComponent(finalTargetUrl)}`; + + this.showSuccess(`찜하기 페이지로 이동합니다. (${market.market_nickname})`); + + // 현재 탭에서 로그인 리다이렉트 URL로 이동 + window.location.href = loginRedirectUrl; + + // 찜 기록 (실제 찜하기는 새 페이지에서 실행됨) + await this.recordZzim(market, zzimType); + + } catch (error) { + console.error('찜하기 진행 오류:', error); + this.showError('찜하기 진행 중 오류가 발생했습니다: ' + error.message); + } + } + + async recordZzim(market, zzimType) { + try { + // 찜 기록을 별도 테이블에 저장하지 않고 통계만 업데이트 + // (필요시 나중에 zzim_history 테이블 추가 가능) + + // 통계 업데이트 (실제 찜한 개수는 스크립트 결과를 받아야 하지만 임시로 50개로 설정) + const estimatedCount = 50; + await this.updateZzimStats(estimatedCount, zzimType, market); + + // UI 새로고침 + await Promise.all([ + this.loadZzimStats(), + this.loadMyMarkets() + ]); + + } catch (error) { + console.error('찜 기록 저장 오류:', error); + } + } + + async updateZzimStats(count, zzimType, targetMarket = null) { + try { + console.log('찜 통계 업데이트 시작:', { count, zzimType, targetMarket }); + + // 현재 제한 정보 확인 + if (!this.currentLimits) { + console.warn('제한 정보가 없음, 통계 다시 로드'); + await this.loadZzimStats(); + } + + const limits = this.currentLimits || { mileage_per_zzim: 1 }; + const today = new Date().toISOString().split('T')[0]; + + if (zzimType === 'my_market') { + // 내 마켓 찜하기: 오늘 찜한 개수 증가 + 받은 찜 개수 증가 + console.log('내 마켓 찜하기 통계 업데이트'); + + // users 테이블 업데이트 + const myMarketUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + today_zzim_count: `today_zzim_count + ${count}`, + today_zzim_date: today, + my_zzim: `my_zzim + ${count}` + }) + }); + + if (!myMarketUpdateResponse.ok) { + throw new Error('내 마켓 찜하기 통계 업데이트 실패'); + } + + // user_markets 테이블의 집계 필드는 제거되었으므로 별도 업데이트 필요 없음 + + // 내 마켓의 찜받은 개수도 증가 + if (targetMarket) { + await this.updateMarketZzimCount(targetMarket.market_url, count); + } + + // 찜 기록 저장 + await this.saveZzimRecord(zzimType, targetMarket, count, 0); + + } else if (zzimType === 'mutual') { + // 품앗이 찜하기: 내 마일리지 차감 + 상대방 마일리지 증가 + 상대방 받은 찜 개수 증가 + console.log('품앗이 찜하기 통계 업데이트'); + + const mileageUsed = count * limits.mileage_per_zzim; + + // 내 마일리지 차감 (users 테이블) + const myMileageUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + available_zzim_mile: `available_zzim_mile - ${mileageUsed}` + }) + }); + + if (!myMileageUpdateResponse.ok) { + throw new Error('내 마일리지 차감 실패'); + } + + // 상대방 마일리지 증가 및 받은 찜 개수 증가 + if (targetMarket && targetMarket.owner_user_id) { + // 상대방 users 테이블 업데이트 + const targetUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${targetMarket.owner_user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + zzim_mile: `zzim_mile + ${mileageUsed}`, + available_zzim_mile: `available_zzim_mile + ${mileageUsed}`, + my_zzim: `my_zzim + ${count}` + }) + }); + + if (!targetUpdateResponse.ok) { + console.warn('상대방 마일리지 및 받은 찜 개수 업데이트 실패'); + } + + // (중복 패치 제거: targetUpdateResponse 에서 이미 users 테이블을 갱신했습니다.) + // 상대방 users 테이블도 업데이트 (호환성 유지) + const targetUsersUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${targetMarket.owner_user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + zzim_mile: `zzim_mile + ${mileageUsed}`, + available_zzim_mile: `available_zzim_mile + ${mileageUsed}`, + my_zzim: `my_zzim + ${count}` + }) + }); + + if (!targetUsersUpdateResponse.ok) { + console.warn('상대방 users 테이블 업데이트 실패'); + } + + // 상대방 마켓의 찜받은 개수도 증가 + await this.updateMarketZzimCount(targetMarket.market_url, count, targetMarket.owner_user_id); + } + + // 찜 기록 저장 + await this.saveZzimRecord(zzimType, targetMarket, count, -mileageUsed); + + console.log(`품앗이 완료: 마일리지 ${mileageUsed} 사용, 찜 ${count}개 제공`); + } + + // 통계 다시 로드하여 UI 업데이트 + await this.loadZzimStats(); + + console.log('찜 통계 업데이트 완료'); + + } catch (error) { + console.error('찜 통계 업데이트 오류:', error); + throw error; + } + } + + // 마켓별 찜받은 개수 업데이트 + async updateMarketZzimCount(marketUrl, count, userId = null) { + try { + const targetUserId = userId || this.user_id; + + const marketsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetUserId}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (marketsResponse.ok) { + const marketsData = await marketsResponse.json(); + const markets = marketsData[0]?.my_markets || []; + + // 해당 마켓 찾아서 찜받은 개수 증가 + const updatedMarkets = markets.map(market => { + if (market.market_url === marketUrl) { + return { + ...market, + zzim_received_count: (market.zzim_received_count || 0) + count, + updated_at: new Date().toISOString() + }; + } + return market; + }); + + // 업데이트된 마켓 목록 저장 + await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetUserId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + my_markets: updatedMarkets + }) + }); + + console.log(`마켓 ${marketUrl} 찜받은 개수 ${count} 증가`); + } + } catch (error) { + console.error('마켓 찜받은 개수 업데이트 오류:', error); + } + } + + // 찜 기록 저장 + async saveZzimRecord(zzimType, targetMarket, zzimCount, mileageEarned) { + try { + const record = { + user_id: this.user_id, + market_url: targetMarket?.market_url || '', + market_name: targetMarket?.market_name || '', + market_nickname: targetMarket?.market_nickname || '', + zzim_type: zzimType, + zzim_count: zzimCount, + mileage_earned: mileageEarned, + target_user_id: targetMarket?.owner_user_id || null, + created_at: new Date().toISOString() + }; + + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/jjim`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(record) + }); + + if (!response.ok) { + console.warn('찜 기록 저장 실패:', response.status); + } else { + console.log('찜 기록 저장 완료'); + } + } catch (error) { + console.error('찜 기록 저장 오류:', error); + } + } + + // 찜하기 가능 여부 확인 + canZzim(zzimType) { + if (!this.currentStats || !this.currentLimits) { + return { canZzim: false, reason: '통계 정보를 불러오는 중입니다.' }; + } + + if (zzimType === 'my_market') { + // 내 마켓 찜하기: 오늘 찜한 개수 확인 + const todayCount = this.currentStats.todayZzimCount; + const dailyLimit = this.currentLimits.daily_zzim_limit; + + if (todayCount >= dailyLimit) { + return { + canZzim: false, + reason: `오늘 찜 제한에 도달했습니다. (${todayCount}/${dailyLimit})` + }; + } + + return { canZzim: true, remaining: dailyLimit - todayCount }; + + } else if (zzimType === 'mutual') { + // 품앗이 찜하기: 사용 가능한 마일리지 확인 + const availableMileage = this.currentStats.availableZzimMile; + const requiredMileage = 50; // 마켓당 50개 찜 가정 + + if (availableMileage < requiredMileage) { + return { + canZzim: false, + reason: `품앗이에 필요한 마일리지가 부족합니다. (보유: ${availableMileage}, 필요: ${requiredMileage})` + }; + } + + return { canZzim: true, remaining: Math.floor(availableMileage / requiredMileage) }; + } + + return { canZzim: false, reason: '알 수 없는 찜 타입입니다.' }; + } + + async getActiveMarkets() { + try { + // user_markets 테이블에서 내 마켓 목록 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -344,33 +1923,45 @@ class ZzimManager { throw new Error('마켓 목록을 가져올 수 없습니다.'); } - const markets = await response.json(); - if (markets.length === 0) { - this.showError('등록된 마켓이 없습니다.'); - return; - } + const data = await response.json(); + const markets = data[0]?.my_markets || []; - // 랜덤 마켓 선택 - const randomMarket = markets[Math.floor(Math.random() * markets.length)]; - this.showStatus(`"${randomMarket.market_nickname}" 마켓에서 찜하기를 시작합니다...`); - - await this.executeZzim(randomMarket, 'my_market', false); // 포그라운드에서 실행 + // 찜하기 대상으로 체크된 마켓만 반환 + return markets.filter(market => market.for_zzim !== false); } catch (error) { - console.error('내 마켓 찜하기 오류:', error); - this.showError('내 마켓 찜하기 중 오류가 발생했습니다.'); + console.error('활성 마켓 목록 조회 오류:', error); + throw error; } } - async startMutualZzim() { - if (this.zzimInProgress) { - this.showError('찜하기가 이미 진행 중입니다.'); - return; - } - + async getMutualMarkets() { try { - // 다른 사용자의 마켓 목록 가져오기 (품앗이용) - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=neq.${this.user_id}&is_active=eq.true`, { + console.log('[품앗이] 품앗이 마켓 목록 조회 시작'); + + // 내 현재 마일리지 확인 (users 테이블) + const myStatsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=available_zzim_mile`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + let myAvailableMileage = 0; + if (myStatsResponse.ok) { + const myData = await myStatsResponse.json(); + myAvailableMileage = myData[0]?.available_zzim_mile || 0; + } + + console.log('[품앗이] 내 사용 가능한 마일리지:', myAvailableMileage); + + if (myAvailableMileage <= 0) { + throw new Error('사용 가능한 마일리지가 없습니다. 먼저 다른 사람의 마켓에 찜을 받아 마일리지를 적립하세요.'); + } + + // 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - v_user_market_stats 뷰 사용 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${this.user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -382,187 +1973,72 @@ class ZzimManager { throw new Error('품앗이 마켓 목록을 가져올 수 없습니다.'); } - const markets = await response.json(); - if (markets.length === 0) { - this.showError('품앗이할 마켓이 없습니다.'); - return; - } + const data = await response.json(); + console.log('[품앗이] 조회된 사용자 데이터:', data.length); - // 랜덤 마켓 선택 - const randomMarket = markets[Math.floor(Math.random() * markets.length)]; - this.showStatus(`품앗이 마켓 "${randomMarket.market_nickname}"에서 찜하기를 시작합니다...`); + const mutualMarkets = []; - await this.executeZzim(randomMarket, 'mutual', true); // 백그라운드에서 실행 - - } catch (error) { - console.error('품앗이 찜하기 오류:', error); - this.showError('품앗이 찜하기 중 오류가 발생했습니다.'); - } - } - - async executeZzim(market, zzimType, inBackground = false) { - this.zzimInProgress = true; - - try { - // 찜하기 스크립트 - const zzimScript = ` - (function() { - let zzimCount = 0; - const maxZzim = 50; // 최대 찜할 개수 - - function clickZzimButtons() { - const zzimButtons = document.querySelectorAll('.zzim_button:not(.active)'); - console.log('찾은 찜 버튼 개수:', zzimButtons.length); - - if (zzimButtons.length === 0) { - console.log('더 이상 찜할 상품이 없습니다.'); - return false; - } - - // 최대 10개씩 찜하기 - const buttonsToClick = Array.from(zzimButtons).slice(0, Math.min(10, maxZzim - zzimCount)); - - buttonsToClick.forEach((btn, index) => { - setTimeout(() => { - try { - btn.click(); - zzimCount++; - console.log(\`찜 버튼 클릭: \${zzimCount}개\`); - } catch (e) { - console.error('찜 버튼 클릭 오류:', e); - } - }, index * 500); // 0.5초 간격 - }); - - return buttonsToClick.length > 0 && zzimCount < maxZzim; - } - - // 페이지 스크롤 및 찜하기 반복 - function scrollAndZzim() { - if (zzimCount >= maxZzim) { - console.log('최대 찜하기 완료:', zzimCount); - return; - } - - // 페이지 하단으로 스크롤 - window.scrollTo(0, document.body.scrollHeight); - - setTimeout(() => { - if (clickZzimButtons()) { - setTimeout(scrollAndZzim, 3000); // 3초 후 다시 시도 - } - }, 1000); - } - - // 시작 - scrollAndZzim(); - - return zzimCount; - })(); - `; - - if (inBackground) { - // 백그라운드에서 실행 (새 탭에서 백그라운드로) - const tab = await chrome.tabs.create({ - url: market.market_url, - active: false - }); + // 각 사용자의 노출된 마켓들을 수집 + data.forEach(userMarket => { + const markets = userMarket.my_markets || []; + const userMileage = userMarket.available_zzim_mile || 0; - // 페이지 로드 완료 후 스크립트 실행 - chrome.tabs.onUpdated.addListener(function listener(tabId, info) { - if (tabId === tab.id && info.status === 'complete') { - chrome.tabs.onUpdated.removeListener(listener); - - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: eval(`(${zzimScript})`) - }).then(() => { - // 일정 시간 후 탭 닫기 - setTimeout(() => { - chrome.tabs.remove(tab.id); - }, 30000); // 30초 후 + // 마일리지가 있는 사용자의 노출된 마켓만 수집 + if (userMileage > 0) { + const visibleMarkets = markets.filter(market => + market.is_visible !== false && + market.for_mutual_zzim !== false // 품앗이 대상으로 설정된 마켓만 + ); + + // 사용자 정보를 마켓에 추가 + visibleMarkets.forEach(market => { + mutualMarkets.push({ + ...market, + owner_user_id: userMarket.user_id, + owner_available_mileage: userMileage }); - } - }); - } else { - // 포그라운드에서 실행 (현재 창에서) - window.open(market.market_url, '_blank'); + }); + } + }); + + console.log('[품앗이] 수집된 품앗이 마켓 수:', mutualMarkets.length); + + if (mutualMarkets.length === 0) { + throw new Error('현재 품앗이 가능한 마켓이 없습니다.'); } - // 찜 기록 저장 - await this.recordZzim(market, zzimType); - - this.showSuccess(`찜하기가 시작되었습니다. (${market.market_nickname})`); - - } catch (error) { - console.error('찜하기 실행 오류:', error); - this.showError('찜하기 실행 중 오류가 발생했습니다.'); - } finally { - this.zzimInProgress = false; - } - } - - async recordZzim(market, zzimType) { - try { - // 찜 기록 저장 - await fetch(`${this.SUPABASE_URL}/rest/v1/jjim`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.access_token}`, - 'apikey': this.SUPABASE_ANON_KEY, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - user_id: this.user_id, - market_url: market.market_url, - market_name: market.market_name, - market_nickname: market.market_nickname, - zzim_type: zzimType, - product_count: 50 // 예상 찜 개수 (실제로는 스크립트 결과를 받아야 함) - }) + // 우선순위 기반 정렬 (마일리지가 많은 사용자 우선) + mutualMarkets.sort((a, b) => { + // 1차: 마일리지 많은 순 + if (b.owner_available_mileage !== a.owner_available_mileage) { + return b.owner_available_mileage - a.owner_available_mileage; + } + // 2차: 최신 등록 순 + return new Date(b.created_at || 0) - new Date(a.created_at || 0); }); - // 통계 업데이트 - await this.updateZzimStats(50, zzimType); + // 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개) + const maxMarkets = Math.min(10, Math.floor(myAvailableMileage / 50)); // 마켓당 50개 찜 가정 + const selectedMarkets = mutualMarkets.slice(0, maxMarkets); - // UI 새로고침 - await this.loadZzimStats(); + console.log('[품앗이] 선택된 품앗이 마켓:', selectedMarkets.length); + + return selectedMarkets; } catch (error) { - console.error('찜 기록 저장 오류:', error); - } - } - - async updateZzimStats(count, zzimType) { - try { - const mileageIncrease = zzimType === 'mutual' ? count * 2 : count; // 품앗이는 마일리지 2배 - - await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${this.access_token}`, - 'apikey': this.SUPABASE_ANON_KEY, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - current_daily_count: `current_daily_count + ${count}`, - current_mileage: `current_mileage + ${mileageIncrease}` - }) - }); - } catch (error) { - console.error('찜 통계 업데이트 오류:', error); + console.error('품앗이 마켓 목록 조회 오류:', error); + throw error; } } showStatus(message) { const statusEl = document.getElementById('zzim-status'); - const errorEl = document.getElementById('zzim-error'); - const successEl = document.getElementById('zzim-success'); + + // 진행률 표시 숨기기 + this.hideProgress(); // 모든 상태 메시지 숨기기 - if (statusEl) statusEl.style.display = 'none'; - if (errorEl) errorEl.style.display = 'none'; - if (successEl) successEl.style.display = 'none'; + this.hideAllMessages(); // 상태 메시지 표시 if (statusEl) { @@ -572,14 +2048,13 @@ class ZzimManager { } showError(message) { - const statusEl = document.getElementById('zzim-status'); const errorEl = document.getElementById('zzim-error'); - const successEl = document.getElementById('zzim-success'); + + // 진행률 표시 숨기기 + this.hideProgress(); // 모든 상태 메시지 숨기기 - if (statusEl) statusEl.style.display = 'none'; - if (errorEl) errorEl.style.display = 'none'; - if (successEl) successEl.style.display = 'none'; + this.hideAllMessages(); // 오류 메시지 표시 if (errorEl) { @@ -589,14 +2064,13 @@ class ZzimManager { } showSuccess(message) { - const statusEl = document.getElementById('zzim-status'); - const errorEl = document.getElementById('zzim-error'); const successEl = document.getElementById('zzim-success'); + // 진행률 표시 숨기기 + this.hideProgress(); + // 모든 상태 메시지 숨기기 - if (statusEl) statusEl.style.display = 'none'; - if (errorEl) errorEl.style.display = 'none'; - if (successEl) successEl.style.display = 'none'; + this.hideAllMessages(); // 성공 메시지 표시 if (successEl) { @@ -604,6 +2078,143 @@ class ZzimManager { successEl.style.display = 'block'; } } + + showProgress(current, total, message) { + const progressDiv = document.getElementById('zzim-progress'); + const progressBar = document.getElementById('progress-bar'); + const progressText = document.getElementById('progress-text'); + const progressPercent = document.getElementById('progress-percent'); + + // 진행률 표시 영역 보이기 + if (progressDiv) { + progressDiv.style.display = 'block'; + } + + // 다른 상태 메시지들 숨기기 + this.hideAllMessages(); + + const percentage = Math.round((current / total) * 100); + + if (progressBar) { + progressBar.style.width = `${percentage}%`; + } + + if (progressText) { + progressText.textContent = message; + } + + if (progressPercent) { + progressPercent.textContent = `${percentage}%`; + } + } + + hideProgress() { + const progressDiv = document.getElementById('zzim-progress'); + + if (progressDiv) { + progressDiv.style.display = 'none'; + } + } + + hideAllMessages() { + const statusEl = document.getElementById('zzim-status'); + const errorEl = document.getElementById('zzim-error'); + const successEl = document.getElementById('zzim-success'); + + if (statusEl) statusEl.style.display = 'none'; + if (errorEl) errorEl.style.display = 'none'; + if (successEl) successEl.style.display = 'none'; + } + + stopZzim() { + this.zzimInProgress = false; + this.updateButtonStates(); + this.hideProgress(); + this.showStatus('찜하기를 중단했습니다.'); + } + + updateButtonStates() { + const myMarketBtn = document.getElementById('my-market-zzim-btn'); + const mutualBtn = document.getElementById('mutual-zzim-btn'); + const stopBtn = document.getElementById('stop-zzim-btn'); + + if (this.zzimInProgress) { + // 찜하기 진행 중 + if (myMarketBtn) { + myMarketBtn.disabled = true; + myMarketBtn.textContent = '💝 찜하기 진행 중...'; + } + if (mutualBtn) { + mutualBtn.disabled = true; + mutualBtn.textContent = '🤝 찜하기 진행 중...'; + } + if (stopBtn) { + stopBtn.style.display = 'inline-block'; + stopBtn.disabled = false; + } + } else { + // 찜하기 중지 상태 + if (myMarketBtn) { + myMarketBtn.disabled = false; + myMarketBtn.textContent = '💝 내 마켓 찜하기'; + } + if (mutualBtn) { + mutualBtn.disabled = false; + mutualBtn.textContent = '🤝 품앗이 찜하기'; + } + if (stopBtn) { + stopBtn.style.display = 'none'; + } + } + } + + // 디버깅용 테스트 함수 + async testDebug() { + console.log('=== 디버깅 테스트 시작 ==='); + + try { + // 1. 현재 설정 확인 + console.log('현재 설정:', { + user_id: this.user_id, + has_token: !!this.access_token, + supabase_url: this.SUPABASE_URL + }); + + // 2. user_markets 테이블 상태 확인 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + console.log('user_markets 테이블 상태:', { + status: response.status, + ok: response.ok + }); + + if (response.ok) { + const data = await response.json(); + console.log('user_markets 데이터:', data); + } else { + const errorText = await response.text(); + console.error('user_markets 조회 오류:', errorText); + } + + // 3. 강제로 마켓 목록 다시 로드 + console.log('마켓 목록 강제 새로고침...'); + await this.loadMyMarkets(); + + this.showSuccess('디버깅 테스트 완료 - 콘솔을 확인하세요'); + + } catch (error) { + console.error('디버깅 테스트 오류:', error); + this.showError('디버깅 테스트 실패: ' + error.message); + } + + console.log('=== 디버깅 테스트 종료 ==='); + } } // 전역 인스턴스 생성 @@ -612,5 +2223,9 @@ let zzimManager; // DOM 로드 완료 후 초기화 document.addEventListener('DOMContentLoaded', async () => { zzimManager = new ZzimManager(); + + // 전역 접근을 위해 window 객체에도 등록 + window.zzimManager = zzimManager; + await zzimManager.init(); }); \ No newline at end of file