diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js index 9c53e72..b516628 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,8 @@ chrome.runtime.onInstalled.addListener(() => { chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); - // 새 어록 감지 알람 생성 (1분마다) - chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); + // 새 어록 감지 알람 생성 (5분마다) + chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 }); // 초기 마지막 확인 시간 설정 chrome.storage.local.set({ lastSayingsCheck: Date.now() }); @@ -35,21 +35,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 +305,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) { @@ -139,10 +346,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; @@ -160,8 +367,8 @@ async function checkForNewSayings() { } // 올바른 Supabase URL과 헤더 사용 - const SUPABASE_URL = "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; // 새 어록 API 호출 const apiUrl = `${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${new Date(lastCheckTime).toISOString()}&admin_approval=eq.true&order=created_at.desc`; @@ -375,8 +582,8 @@ async function handleTrademarkSearch(keyword, tab) { }); // 3. 사용자 정보 및 회원등급 확인 - const SUPABASE_URL = "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; // 사용자 기본 정보 가져오기 (토큰 검증) const authUrl = `${SUPABASE_URL}/auth/v1/user`; @@ -1144,8 +1351,8 @@ async function getApiKey(service) { try { console.log(`[background.js] ${service} API 키 조회 시작`); - const SUPABASE_URL = "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; // 먼저 간단한 테스트 호출 (인증 없이) const testUrl = `${SUPABASE_URL}/rest/v1/api_keys?select=*&limit=1`; @@ -1762,6 +1969,7 @@ async function handleKoreanToChinese(selectedText, tab) { if (!selectedText) { chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '텍스트 선택 필요', message: '번역할 텍스트를 먼저 선택해주세요.' }); @@ -1804,6 +2012,7 @@ async function handleKoreanToChinese(selectedText, tab) { // 한국어도 중국어도 아닌 경우 chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '지원하지 않는 언어', message: '한국어 또는 중국어 텍스트를 선택해주세요.' }); @@ -1811,14 +2020,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 +2051,7 @@ async function handleKoreanToChinese(selectedText, tab) { console.error('[background.js] 한국어↔중국어 번역 중 오류:', error); chrome.notifications.create({ type: 'basic', + iconUrl: 'icon.png', title: '번역 오류', message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' }); @@ -1835,31 +2060,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 +2425,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] 팝업 표시 완료'); } // ==================== 시간 알람 기능 ==================== @@ -2105,8 +2908,8 @@ async function handleAddToZzim(message, sendResponse) { throw new Error('로그인이 필요합니다'); } - const SUPABASE_URL = config.SUPABASE_URL || "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = config.SUPABASE_URL || "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; // 사용자 정보 가져오기 const userInfo = await fetchUserInfo(config.ACCESS_TOKEN); @@ -2216,6 +3019,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { handleAddToZzim(message, sendResponse); return true; // 비동기 응답을 위해 true 반환 } + + if (message.action === 'executeBackgroundZzim') { + handleExecuteBackgroundZzim(message, sendResponse); + return true; // 비동기 응답을 위해 true 반환 + } }); // 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거) @@ -2415,8 +3223,8 @@ async function incrementUserApiCalls(userId, token) { // 백엔드 설정 중앙 관리 const BACKEND_CONFIG = { - SUPABASE_URL: "http://146.56.101.199:8000", - SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + SUPABASE_URL: "https://ko.wrmc.cc", + SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU" }; // 백엔드 설정 가져오기 함수 @@ -2425,3 +3233,360 @@ 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: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' + }); + } +} diff --git a/wrmc_ext/bannedWords.js b/wrmc_ext/bannedWords.js index 929ba4a..0c88078 100644 --- a/wrmc_ext/bannedWords.js +++ b/wrmc_ext/bannedWords.js @@ -95,8 +95,8 @@ class BannedWordsManager { }); // 기본값 설정 - this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000'; - this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'; + this.SUPABASE_URL = storageData.SUPABASE_URL || 'https://ko.wrmc.cc'; + this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU'; this.ACCESS_TOKEN = storageData.access_token; } diff --git a/wrmc_ext/content.js b/wrmc_ext/content.js index 9699c14..18ca614 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 }; }); +// iframe 환경 감지 +function detectEnvironment() { + try { + // 1. iframe 내부인지 확인 + isIframeEnvironment = window.self !== window.top; + + // 2. 특별한 컨테이너 확인 + const specialContainers = [ + '.ice-container', + '[class*="aliwangwang"]', + '[class*="chat-container"]', + '[class*="message-container"]', + '[data-testid*="chat"]', + '[role="application"]', + 'webview', + 'embed' + ]; + + const hasSpecialContainer = specialContainers.some(selector => { + return document.querySelector(selector) !== null; + }); + + console.log('[content.js] 환경 감지:', { + isIframe: isIframeEnvironment, + hasSpecialContainer: hasSpecialContainer, + userAgent: navigator.userAgent.includes('AliWangWang') ? 'AliWangWang' : 'Other' + }); + + return isIframeEnvironment || hasSpecialContainer; + } catch (e) { + console.log('[content.js] 환경 감지 오류:', e); + return false; + } +} + +// 향상된 텍스트 선택 감지 +function getSelectedTextAdvanced() { + let selectedText = ''; + + try { + // 1. 기본 window.getSelection() 확인 + const selection = window.getSelection(); + if (selection && selection.toString().trim()) { + selectedText = selection.toString().trim(); + console.log('[content.js] 기본 선택에서 감지:', selectedText); + return selectedText; + } + + // 2. document.getSelection() 확인 + if (document.getSelection) { + const docSelection = document.getSelection(); + if (docSelection && docSelection.toString().trim()) { + selectedText = docSelection.toString().trim(); + console.log('[content.js] document 선택에서 감지:', selectedText); + return selectedText; + } + } + + // 3. 활성 요소에서 선택된 텍스트 확인 + const activeElement = document.activeElement; + if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { + const start = activeElement.selectionStart; + const end = activeElement.selectionEnd; + if (start !== end && start !== null && end !== null) { + selectedText = activeElement.value.substring(start, end).trim(); + console.log('[content.js] 입력 요소에서 감지:', selectedText); + return selectedText; + } + } + + // 4. contentEditable 요소 확인 + const editableElements = document.querySelectorAll('[contenteditable="true"]'); + for (const element of editableElements) { + try { + const elementSelection = element.ownerDocument.getSelection(); + if (elementSelection && elementSelection.toString().trim()) { + selectedText = elementSelection.toString().trim(); + console.log('[content.js] contentEditable에서 감지:', selectedText); + return selectedText; + } + } catch (e) { + continue; + } + } + + // 5. iframe 내부 확인 + const iframes = document.querySelectorAll('iframe'); + for (const iframe of iframes) { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + const iframeSelection = iframeDoc.getSelection(); + if (iframeSelection && iframeSelection.toString().trim()) { + selectedText = iframeSelection.toString().trim(); + console.log('[content.js] iframe에서 감지:', selectedText); + return selectedText; + } + } catch (e) { + // 크로스 오리진 iframe은 접근 불가 + console.log('[content.js] iframe 접근 제한:', e.message); + } + } + + // 6. 특별한 컨테이너 내부 확인 (알리왕왕, ice-container 등) + const containers = document.querySelectorAll( + '.ice-container, [class*="container"], [class*="chat"], [class*="message"], ' + + '[class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], ' + + '[data-testid*="message"], [role="textbox"], [contenteditable], [class*="aliwangwang"], ' + + '[class*="input"], [class*="text"], webview, embed' + ); + + for (const container of containers) { + try { + const containerSelection = container.ownerDocument.getSelection(); + if (containerSelection && containerSelection.toString().trim()) { + // 선택 범위가 해당 컨테이너 내부인지 확인 + const range = containerSelection.getRangeAt(0); + if (range && (container.contains(range.commonAncestorContainer) || + container.contains(range.startContainer) || + container.contains(range.endContainer))) { + selectedText = containerSelection.toString().trim(); + console.log('[content.js] 특별 컨테이너에서 감지:', selectedText); + return selectedText; + } + } + } catch (e) { + continue; + } + } + + // 7. Shadow DOM 확인 + function checkShadowDom(element) { + if (element.shadowRoot) { + try { + const shadowSelection = element.shadowRoot.getSelection(); + if (shadowSelection && shadowSelection.toString().trim()) { + return shadowSelection.toString().trim(); + } + } catch (e) { + // 무시 + } + } + + for (const child of element.children) { + const result = checkShadowDom(child); + if (result) return result; + } + return null; + } + + const shadowResult = checkShadowDom(document.body); + if (shadowResult) { + console.log('[content.js] Shadow DOM에서 감지:', shadowResult); + return shadowResult; + } + + console.log('[content.js] 선택된 텍스트를 찾을 수 없음'); + return ''; + + } catch (e) { + console.error('[content.js] 텍스트 선택 감지 오류:', e); + return ''; + } +} + +// 커스텀 컨텍스트 메뉴 생성 +function createCustomContextMenu(e) { + // 기존 커스텀 메뉴 제거 + removeCustomContextMenu(); + + const selectedText = getSelectedTextAdvanced(); + if (!selectedText) return; + + customContextMenu = document.createElement('div'); + customContextMenu.id = 'custom-context-menu'; + customContextMenu.style.cssText = ` + position: fixed; + z-index: 999999; + background: white; + border: 1px solid #ccc; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + min-width: 200px; + padding: 4px 0; + `; + + // 메뉴 항목들 + const menuItems = [ + { + text: '🔍 지재권검색(내차확장기)', + action: () => { + console.log('[CustomMenu] 지재권 검색 클릭:', selectedText); + chrome.runtime.sendMessage({ + action: "searchTrademark", + keyword: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[CustomMenu] 지재권 검색 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[CustomMenu] 지재권 검색 메시지 전송 성공:', response); + } + }); + removeCustomContextMenu(); + } + }, + { + text: '🌐 멀티번역하기(내차확장기)', + action: () => { + console.log('[CustomMenu] 멀티번역 클릭:', selectedText); + chrome.runtime.sendMessage({ + action: "translateText", + text: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[CustomMenu] 멀티번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[CustomMenu] 멀티번역 메시지 전송 성공:', response); + } + }); + removeCustomContextMenu(); + } + }, + { + text: '⚡ 직번역(내차확장기)', + action: () => { + console.log('[CustomMenu] 직번역 클릭:', selectedText); + chrome.runtime.sendMessage({ + action: "handleDirectTranslation", + selectedText: selectedText + }, (response) => { + if (chrome.runtime.lastError) { + console.error('[CustomMenu] 직번역 메시지 전송 실패:', chrome.runtime.lastError); + } else { + console.log('[CustomMenu] 직번역 메시지 전송 성공:', response); + } + }); + removeCustomContextMenu(); + } + }, + { + text: '📋 복사', + action: () => { + console.log('[CustomMenu] 복사 클릭:', selectedText); + navigator.clipboard.writeText(selectedText).then(() => { + console.log('[CustomMenu] 복사 성공'); + // 복사 성공 알림 (선택사항) + showTemporaryMessage('복사되었습니다!'); + }).catch(err => { + console.error('[CustomMenu] 복사 실패:', err); + // 폴백: execCommand 사용 + try { + const textArea = document.createElement('textarea'); + textArea.value = selectedText; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + console.log('[CustomMenu] 폴백 복사 성공'); + showTemporaryMessage('복사되었습니다!'); + } catch (fallbackErr) { + console.error('[CustomMenu] 폴백 복사도 실패:', fallbackErr); + } + }); + removeCustomContextMenu(); + } + } + ]; + + menuItems.forEach(item => { + const menuItem = document.createElement('div'); + menuItem.textContent = item.text; + menuItem.style.cssText = ` + padding: 8px 16px; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; + `; + + menuItem.addEventListener('mouseenter', () => { + menuItem.style.backgroundColor = '#f0f0f0'; + }); + + menuItem.addEventListener('mouseleave', () => { + menuItem.style.backgroundColor = 'transparent'; + }); + + menuItem.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + console.log('[CustomMenu] 메뉴 항목 클릭:', item.text); + item.action(); + }); + + customContextMenu.appendChild(menuItem); + }); + + document.body.appendChild(customContextMenu); + + // 위치 설정 + const rect = customContextMenu.getBoundingClientRect(); + let x = e.clientX; + let y = e.clientY; + + // 화면 경계 체크 + if (x + rect.width > window.innerWidth) { + x = window.innerWidth - rect.width - 10; + } + if (y + rect.height > window.innerHeight) { + y = window.innerHeight - rect.height - 10; + } + + customContextMenu.style.left = x + 'px'; + customContextMenu.style.top = y + 'px'; + + // 외부 클릭시 메뉴 제거 + setTimeout(() => { + const handleClickOutside = (event) => { + if (!customContextMenu.contains(event.target)) { + removeCustomContextMenu(); + document.removeEventListener('click', handleClickOutside); + } + }; + document.addEventListener('click', handleClickOutside); + }, 100); + + console.log('[CustomMenu] 커스텀 컨텍스트 메뉴 생성 완료:', selectedText); +} + +// 임시 메시지 표시 함수 +function showTemporaryMessage(message) { + const messageDiv = document.createElement('div'); + messageDiv.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 9999999; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px 20px; + border-radius: 6px; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + pointer-events: none; + `; + messageDiv.textContent = message; + + document.body.appendChild(messageDiv); + + setTimeout(() => { + if (messageDiv.parentNode) { + messageDiv.parentNode.removeChild(messageDiv); + } + }, 1500); +} + +// 커스텀 컨텍스트 메뉴 제거 +function removeCustomContextMenu() { + if (customContextMenu) { + customContextMenu.remove(); + customContextMenu = null; + } +} + +// 향상된 컨텍스트 메뉴 이벤트 document.addEventListener("contextmenu", (e) => { lastContextMenuPos = { x: e.pageX, y: e.pageY }; + + // 특별한 환경에서는 커스텀 컨텍스트 메뉴도 표시 + if (detectEnvironment()) { + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + // 기본 컨텍스트 메뉴를 막지 않고 추가로 커스텀 메뉴 표시 + setTimeout(() => createCustomContextMenu(e), 50); + } + } }); -// ESC 키로 모달 닫기 +// 향상된 단축키 처리 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); @@ -858,9 +1381,569 @@ 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(); } } + +// 자동 찜하기 기능 +function startAutoZzim(maxZzim = 50, delay = 1000) { + console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay }); + + let zzimCount = 0; + let actualZzimCount = 0; // 실제로 찜한 개수 + let currentPage = 1; + let isRunning = true; + let noMoreProducts = false; + + // 상태 표시 UI 생성 + const statusDiv = createZzimStatusUI(); + updateZzimStatus(statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`); + + function findZzimButtons() { + // 네이버 스마트스토어의 찜 버튼 선택자들 (이미 찜한 상품 제외) + const selectors = [ + // 찜하지 않은 상품만 선택 (aria-pressed="false" 또는 active 클래스가 없는 것) + 'button[data-testid="wishlist-button"]:not([aria-pressed="true"])', + 'button[data-testid="wishlist-button"][aria-pressed="false"]', + '.zzim_button:not(.active):not(.on)', + '.wish_button:not(.active):not(.on)', + 'button[class*="wish"]:not([class*="active"]):not([class*="on"])', + 'button[aria-label*="찜"]:not([aria-pressed="true"])', + 'button[class*="zzim"]:not([class*="active"]):not([class*="on"])', + 'a[class*="wish"]:not([class*="active"]):not([class*="on"])', + '.product_like:not(.active):not(.on)', + '.btn_wish:not(.active):not(.on)', + // 하트 아이콘이 비어있는 것들 + 'button:has(svg[fill="none"]) svg[class*="heart"]', + 'button:has(.ico_heart):not(.active)', + // 추가 선택자들 + '[class*="ProductWishButton"]:not([class*="active"])', + '[data-shp-contents-id] button[class*="wish"]:not([class*="active"])' + ]; + + let buttons = []; + for (const selector of selectors) { + try { + const foundButtons = document.querySelectorAll(selector); + if (foundButtons.length > 0) { + // 실제로 보이는 버튼만 필터링 + const visibleButtons = Array.from(foundButtons).filter(btn => { + const rect = btn.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0 && + window.getComputedStyle(btn).display !== 'none' && + window.getComputedStyle(btn).visibility !== 'hidden'; + }); + + if (visibleButtons.length > 0) { + console.log(`[AutoZzim] 찜 버튼 발견 (${selector}):`, visibleButtons.length); + buttons = visibleButtons; + break; + } + } + } catch (e) { + console.log(`[AutoZzim] 선택자 오류 (${selector}):`, e.message); + } + } + + // 중복 제거 및 이미 찜한 상품 재확인 + const uniqueButtons = Array.from(new Set(buttons)).filter(btn => { + return !isAlreadyZzimed(btn); + }); + + return uniqueButtons; + } + + // 이미 찜한 상품인지 확인 + function isAlreadyZzimed(button) { + try { + // 버튼의 상태 확인 + const isPressed = button.getAttribute('aria-pressed') === 'true'; + const hasActiveClass = button.classList.contains('active') || + button.classList.contains('on') || + button.classList.contains('selected'); + + // 부모 요소에서 찜 상태 확인 + const parent = button.closest('[class*="product"], [class*="item"], [data-shp-contents-id]'); + if (parent) { + const hasZzimedClass = parent.querySelector('.active, .on, [aria-pressed="true"]'); + if (hasZzimedClass) return true; + } + + // 하트 아이콘 색상 확인 + const heartIcon = button.querySelector('svg, .ico_heart, [class*="heart"]'); + if (heartIcon) { + const fill = heartIcon.getAttribute('fill'); + const color = window.getComputedStyle(heartIcon).color; + const backgroundColor = window.getComputedStyle(heartIcon).backgroundColor; + + // 빨간색이나 채워진 하트는 이미 찜한 상품 + if (fill && fill !== 'none' && fill !== 'transparent') return true; + if (color && (color.includes('rgb(255') || color.includes('red'))) return true; + if (backgroundColor && backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)') return true; + } + + return isPressed || hasActiveClass; + } catch (e) { + console.log('[AutoZzim] 찜 상태 확인 오류:', e); + return false; + } + } + + // 찜하기 버튼 클릭 및 검증 + async function clickZzimButton(button) { + return new Promise((resolve) => { + try { + // 클릭 전 상태 저장 + const beforeState = isAlreadyZzimed(button); + + // 버튼이 화면에 보이도록 스크롤 + button.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + setTimeout(() => { + if (!isRunning) { + resolve(false); + return; + } + + // 버튼 클릭 + button.click(); + console.log(`[AutoZzim] 찜 버튼 클릭 시도`); + + // 클릭 후 상태 변화 확인 + setTimeout(() => { + const afterState = isAlreadyZzimed(button); + const success = !beforeState && afterState; // 찜하지 않은 상태에서 찜한 상태로 변경 + + if (success) { + actualZzimCount++; + console.log(`[AutoZzim] 찜하기 성공! 총 ${actualZzimCount}개`); + } else { + console.log(`[AutoZzim] 찜하기 실패 또는 이미 찜한 상품`); + } + + // 확인 버튼이 나타나면 클릭 + setTimeout(() => { + const confirmSelectors = [ + 'button[data-testid="confirm"]', + '.confirm_btn', + 'button:contains("확인")', + 'button:contains("OK")', + '.modal button[class*="confirm"]', + '.popup button[class*="confirm"]', + '[class*="LayerAlert"] button', + '[class*="alert"] button[class*="confirm"]' + ]; + + for (const selector of confirmSelectors) { + try { + const confirmBtn = document.querySelector(selector); + if (confirmBtn && confirmBtn.offsetParent !== null) { + confirmBtn.click(); + console.log('[AutoZzim] 확인 버튼 클릭'); + break; + } + } catch (e) { + // 무시 + } + } + }, 300); + + resolve(success); + }, 800); // 상태 변화 확인을 위한 대기 시간 + + }, 500); // 스크롤 후 클릭까지 대기 + + } catch (e) { + console.error('[AutoZzim] 찜 버튼 클릭 오류:', e); + resolve(false); + } + }); + } + + async function processCurrentPage() { + if (!isRunning || actualZzimCount >= maxZzim) { + return false; + } + + const zzimButtons = findZzimButtons(); + console.log(`[AutoZzim] 페이지 ${currentPage}에서 찾은 찜 가능한 버튼:`, zzimButtons.length); + + if (zzimButtons.length === 0) { + console.log('[AutoZzim] 현재 페이지에 찜할 상품이 없음'); + return false; + } + + // 한 번에 최대 5개씩 처리 + const batchSize = 5; + let processedInPage = 0; + + for (let i = 0; i < zzimButtons.length && isRunning && actualZzimCount < maxZzim; i += batchSize) { + const batch = zzimButtons.slice(i, Math.min(i + batchSize, zzimButtons.length)); + + for (const button of batch) { + if (!isRunning || actualZzimCount >= maxZzim) break; + + zzimCount++; // 시도 횟수 + updateZzimStatus(statusDiv, `찜하기 진행 중... (${actualZzimCount}/${maxZzim}) - 페이지 ${currentPage}`); + + const success = await clickZzimButton(button); + if (success) { + processedInPage++; + } + + // 설정된 간격으로 대기 + if (i < zzimButtons.length - 1) { + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + // 배치 간 추가 대기 + if (i + batchSize < zzimButtons.length) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + console.log(`[AutoZzim] 페이지 ${currentPage} 처리 완료: ${processedInPage}개 찜함`); + return processedInPage > 0; + } + + async function goToNextPage() { + try { + // 다음 페이지 버튼 찾기 + const nextPageSelectors = [ + 'a[class*="next"]:not([class*="disabled"])', + 'button[class*="next"]:not([disabled])', + '.paginate_next:not(.disabled)', + '[class*="pagination"] a[class*="next"]', + '[class*="paging"] a[class*="next"]', + 'a[aria-label*="다음"]', + 'button[aria-label*="다음"]', + // 페이지 번호로 다음 페이지 찾기 + `a[href*="cp=${currentPage + 1}"]`, + `button[data-page="${currentPage + 1}"]` + ]; + + let nextButton = null; + for (const selector of nextPageSelectors) { + try { + nextButton = document.querySelector(selector); + if (nextButton && nextButton.offsetParent !== null) { + console.log(`[AutoZzim] 다음 페이지 버튼 발견: ${selector}`); + break; + } + } catch (e) { + continue; + } + } + + if (!nextButton) { + // URL 직접 변경으로 다음 페이지 이동 + const currentUrl = new URL(window.location.href); + const currentPageNum = parseInt(currentUrl.searchParams.get('cp') || '1'); + currentUrl.searchParams.set('cp', (currentPageNum + 1).toString()); + + console.log(`[AutoZzim] URL 직접 변경으로 다음 페이지 이동: ${currentUrl.href}`); + window.location.href = currentUrl.href; + return true; + } + + // 다음 페이지 버튼 클릭 + nextButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); + await new Promise(resolve => setTimeout(resolve, 500)); + + nextButton.click(); + console.log(`[AutoZzim] 다음 페이지 버튼 클릭`); + + // 페이지 로드 대기 + await new Promise(resolve => setTimeout(resolve, 3000)); + + currentPage++; + return true; + + } catch (e) { + console.error('[AutoZzim] 다음 페이지 이동 오류:', e); + return false; + } + } + + async function mainLoop() { + while (isRunning && actualZzimCount < maxZzim && currentPage <= 10) { // 최대 10페이지까지 + updateZzimStatus(statusDiv, `페이지 ${currentPage} 로딩 중... (${actualZzimCount}/${maxZzim})`); + + // 페이지 로드 대기 + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 현재 페이지 처리 + const hasProducts = await processCurrentPage(); + + if (!hasProducts) { + console.log(`[AutoZzim] 페이지 ${currentPage}에 더 이상 찜할 상품이 없음`); + + // 다음 페이지로 이동 시도 + const moved = await goToNextPage(); + if (!moved) { + console.log('[AutoZzim] 더 이상 다음 페이지가 없음'); + break; + } + } else { + // 현재 페이지에서 더 찜할 수 있는지 확인 + const remainingButtons = findZzimButtons(); + if (remainingButtons.length === 0) { + // 다음 페이지로 이동 + const moved = await goToNextPage(); + if (!moved) { + console.log('[AutoZzim] 더 이상 다음 페이지가 없음'); + break; + } + } + } + + // 페이지 간 대기 + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + // 완료 처리 + console.log(`[AutoZzim] 찜하기 완료: 실제 ${actualZzimCount}개 찜함 (시도: ${zzimCount}회)`); + updateZzimStatus(statusDiv, `찜하기 완료! (실제 ${actualZzimCount}개 찜함)`); + + // 5초 후 상태 UI 제거 + setTimeout(() => { + if (statusDiv && statusDiv.parentNode) { + statusDiv.parentNode.removeChild(statusDiv); + } + }, 5000); + } + + // 60초 후 자동 종료 + setTimeout(() => { + isRunning = false; + console.log('[AutoZzim] 시간 초과로 찜하기 종료'); + updateZzimStatus(statusDiv, `시간 초과로 종료 (실제 ${actualZzimCount}개 찜함)`); + + // 5초 후 상태 UI 제거 + setTimeout(() => { + if (statusDiv && statusDiv.parentNode) { + statusDiv.parentNode.removeChild(statusDiv); + } + }, 5000); + }, 60000); + + // 찜하기 시작 + setTimeout(mainLoop, 2000); // 페이지 로드 후 2초 대기 +} + +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; + } +} + +// 페이지 로드 시 URL 파라미터 확인 +function checkAutoZzimParam() { + const urlParams = new URLSearchParams(window.location.search); + const autoZzim = urlParams.get('auto_zzim'); + const maxZzim = parseInt(urlParams.get('max_zzim')) || 50; + + if (autoZzim === 'true') { + console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작'); + setTimeout(() => { + startAutoZzim(maxZzim); + }, 3000); // 페이지 완전 로드 후 3초 대기 + } +} + +// 페이지 로드 완료 시 자동 찜하기 파라미터 확인 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', checkAutoZzimParam); +} else { + checkAutoZzimParam(); +} + +// 키보드 이벤트 리스너 추가 +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+F3: 한중번역 + 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+F4: 직번역 + 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(); + +// 키보드 이벤트 리스너 추가 +addKeyboardListeners(); + +// iframe 환경에서도 이벤트 리스너 설정 +if (isIframeEnvironment) { + addKeyboardListeners(window.parent.document); +} + +// 컨텍스트 메뉴 이벤트 리스너 +document.addEventListener("contextmenu", (e) => { + lastContextMenuPos = { x: e.pageX, y: e.pageY }; + + const selectedText = getSelectedTextAdvanced(); + if (selectedText) { + console.log('[Context Menu] 선택된 텍스트:', selectedText); + setTimeout(() => createCustomContextMenu(e), 50); + } +}); + +// 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 +}); + +// 자동 찜하기 파라미터 확인 +checkAutoZzimParam(); diff --git a/wrmc_ext/manifest.json b/wrmc_ext/manifest.json index a7e63c0..ea2b09f 100644 --- a/wrmc_ext/manifest.json +++ b/wrmc_ext/manifest.json @@ -11,11 +11,12 @@ "notifications", "alarms", "tabs", - "scripting" + "scripting", + "clipboardRead" ], "host_permissions": [ "*://markinfo.kr/*", - "http://146.56.101.199:8000/*", + "https://ko.wrmc.cc/*", "https://oci1ckh08045.duckdns.org:8000/*", "*://smartstore.naver.com/*", "*://translate.googleapis.com/*", @@ -59,6 +60,12 @@ "default": "Ctrl+Shift+Z" }, "description": "한국어↔중국어 양방향 번역" + }, + "direct-translate": { + "suggested_key": { + "default": "Ctrl+Shift+K" + }, + "description": "직번역 (텍스트 바로 대체)" } }, "web_accessible_resources": [ 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/popup.js b/wrmc_ext/popup.js index 36dc0d2..5df65d9 100644 --- a/wrmc_ext/popup.js +++ b/wrmc_ext/popup.js @@ -24,8 +24,8 @@ async function getBackendConfig() { // 폴백 설정 const fallbackConfig = { - SUPABASE_URL: "http://146.56.101.199:8000", - SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + SUPABASE_URL: "https://ko.wrmc.cc", + SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU" }; console.log('[popup.js] 폴백 설정:', fallbackConfig); @@ -34,8 +34,8 @@ async function getBackendConfig() { } // 디버그 모드 플래그 (개발 시에만 true로 설정) -// const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김 -const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김 +const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김 +// const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김 // 로그 레벨 정의 const LOG_LEVELS = { diff --git a/wrmc_ext/rest-modal.js b/wrmc_ext/rest-modal.js index 85a5638..2a4b59d 100644 --- a/wrmc_ext/rest-modal.js +++ b/wrmc_ext/rest-modal.js @@ -93,8 +93,8 @@ class RestModal { throw new Error('Access token not found'); } - const SUPABASE_URL = this.config.SUPABASE_URL || "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; // public.events 테이블에서 event_type이 'rest_time'인 데이터 가져오기 const apiUrl = `${SUPABASE_URL}/rest/v1/events?select=message&event_type=eq.rest_time&order=created_at.desc&limit=50`; @@ -170,8 +170,8 @@ class RestModal { throw new Error('Access token not found'); } - const SUPABASE_URL = this.config.SUPABASE_URL || "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; // 최근 1달 이내의 승인된 어록 가져오기 const oneMonthAgo = new Date(); diff --git a/wrmc_ext/sayings.js b/wrmc_ext/sayings.js index 63f3ae5..0a35876 100644 --- a/wrmc_ext/sayings.js +++ b/wrmc_ext/sayings.js @@ -67,8 +67,8 @@ class SayingsManager { }); // 기본값 설정 - this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000'; - this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'; + this.SUPABASE_URL = storageData.SUPABASE_URL || 'https://ko.wrmc.cc'; + this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU'; this.ACCESS_TOKEN = storageData.access_token; } diff --git a/wrmc_ext/settings.js b/wrmc_ext/settings.js index 8a7288a..6f1ff34 100644 --- a/wrmc_ext/settings.js +++ b/wrmc_ext/settings.js @@ -116,8 +116,8 @@ class SettingsManager { return; } - const SUPABASE_URL = this.config.SUPABASE_URL || "http://146.56.101.199:8000"; - const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc"; + const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; console.log('Supabase URL:', SUPABASE_URL); console.log('토큰 존재 여부:', !!this.config.ACCESS_TOKEN); diff --git a/wrmc_ext/zzim.html b/wrmc_ext/zzim.html index e9d39ae..2c78337 100644 --- a/wrmc_ext/zzim.html +++ b/wrmc_ext/zzim.html @@ -140,45 +140,162 @@ } .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-checkbox-input { + width: 18px; + height: 18px; + accent-color: #667eea; + cursor: pointer; + } + + .market-checkbox-label { + font-size: 14px; + 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 +339,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 +636,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..aeed48e 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() { @@ -20,15 +21,15 @@ class ZzimManager { if (config.zzim_config) { 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_URL = config.zzim_config.SUPABASE_URL || "https://ko.wrmc.cc"; + this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; } 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_URL = fallbackConfig.SUPABASE_URL || "https://ko.wrmc.cc"; + this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; } 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,64 @@ 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) { + // 기본 마켓 URL에서 마지막 슬래시 제거 + const baseUrl = marketUrl.replace(/\/$/, ''); + + if (latestFirst) { + // 최신상품 우선: /best?cp=1 + return `${baseUrl}/best?cp=1`; + } else { + // 전체상품: /category/ALL?cp=1 + return `${baseUrl}/category/ALL?cp=1`; + } } async loadZzimStats() { try { - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { + // users 테이블에서 찜 통계 가져오기 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=my_zzim,zzim_mile`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -134,23 +314,28 @@ class ZzimManager { } 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 userStats = data[0] || { + my_zzim: 0, + zzim_mile: 0 }; - // UI 업데이트 - HTML의 실제 ID와 일치하도록 수정 + // 오늘 찜한 개수는 별도로 계산 (임시로 0으로 설정, 실제로는 오늘 날짜 기준으로 계산) + const todayZzimCount = 0; // TODO: 실제 구현 시 오늘 찜한 개수 계산 + const dailyLimit = 100; // 일일 찜 제한 + const mileageLimit = 10000; // 마일리지 제한 + + // 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 = dailyLimit; + if (mileageCountEl) mileageCountEl.textContent = userStats.zzim_mile || 0; + if (mileageLimitEl) mileageLimitEl.textContent = mileageLimit; + if (totalZzimEl) totalZzimEl.textContent = userStats.my_zzim || 0; } catch (error) { console.error('찜 통계 로드 오류:', error); @@ -160,7 +345,183 @@ class ZzimManager { 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); + 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 marketIndex = parseInt(e.target.dataset.marketIndex); + this.toggleMarketForZzim(marketIndex, e.target.checked); + }); + }); + + // 수정 버튼 이벤트 + const editBtns = document.querySelectorAll('.market-edit-btn'); + editBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + const marketIndex = parseInt(e.target.dataset.marketIndex); + this.editMarket(marketIndex); + }); + }); + + // 노출/숨김 버튼 이벤트 + const visibilityBtns = document.querySelectorAll('.market-visibility-btn'); + visibilityBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + const marketIndex = parseInt(e.target.dataset.marketIndex); + this.toggleMarketVisibility(marketIndex); + }); + }); + + // 삭제 버튼 이벤트 + const deleteBtns = document.querySelectorAll('.market-delete-btn'); + deleteBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + const marketIndex = parseInt(e.target.dataset.marketIndex); + this.deleteMarket(marketIndex); + }); + }); + } + + // 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 toggleMarketForZzim(marketIndex, 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 +530,66 @@ 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 || []; + + if (marketIndex < 0 || marketIndex >= markets.length) { + this.showError('잘못된 마켓 인덱스입니다.'); + return; + } + + // 찜하기 대상 상태 업데이트 + markets[marketIndex] = { + ...markets[marketIndex], + for_zzim: isChecked, + 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('마켓 찜하기 설정 변경 실패'); + } + + this.showSuccess(`마켓이 ${isChecked ? '찜하기 대상에 포함' : '찜하기 대상에서 제외'}되었습니다.`); } catch (error) { - console.error('마켓 목록 로드 오류:', error); - this.showError('마켓 목록을 불러올 수 없습니다.'); + console.error('마켓 찜하기 설정 변경 오류:', error); + this.showError('마켓 찜하기 설정 변경 중 오류가 발생했습니다.'); + + // 오류 발생 시 체크박스 상태 원복 + const checkbox = document.getElementById(`market-check-${marketIndex}`); + 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 +601,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 +642,108 @@ 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, // 내가 찜하기 할 때 포함 (기본값: 포함) + 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); @@ -258,48 +751,188 @@ class ZzimManager { } } - async editMarket(marketId) { - // 편집 모달을 표시하거나 인라인 편집 구현 - const newName = prompt('새로운 마켓 이름을 입력하세요:'); - if (!newName) return; - - const newNickname = prompt('새로운 마켓 별명을 입력하세요:'); - if (!newNickname) return; - + async editMarket(marketIndex) { 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 || []; + + if (marketIndex < 0 || marketIndex >= markets.length) { + this.showError('잘못된 마켓 인덱스입니다.'); + return; + } + + const market = markets[marketIndex]; + + // 모달에 기존 데이터 채우기 + this.currentEditIndex = marketIndex; + 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 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 (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 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 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, + 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 +940,134 @@ 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(marketIndex) { + 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 || []; + + if (marketIndex < 0 || marketIndex >= markets.length) { + this.showError('잘못된 마켓 인덱스입니다.'); + return; + } + + // 노출 상태 토글 + markets[marketIndex] = { + ...markets[marketIndex], + is_visible: !markets[marketIndex].is_visible, + 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(`마켓이 ${markets[marketIndex].is_visible ? '노출' : '숨김'} 상태로 변경되었습니다.`); + + } catch (error) { + console.error('마켓 노출 설정 변경 오류:', error); + this.showError('마켓 노출 설정 변경 중 오류가 발생했습니다.'); + } + } + + async deleteMarket(marketIndex) { + 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 || []; + + if (marketIndex < 0 || marketIndex >= markets.length) { + this.showError('잘못된 마켓 인덱스입니다.'); + return; + } + + const market = markets[marketIndex]; + + if (!confirm(`정말로 "${market.market_nickname}" 마켓을 삭제하시겠습니까?`)) { + return; + } + + // 마켓 삭제 (배열에서 제거) + markets.splice(marketIndex, 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 +1082,505 @@ 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 생성 + const loginRedirectUrl = `https://nid.naver.com/nidlogin.login?url=${encodeURIComponent(targetUrl + (targetUrl.includes('?') ? '&' : '?') + 'auto_zzim=true&max_zzim=50')}`; + + 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 { + // 1. 찜을 해준 사용자의 마일리지 증가 + const mileageIncrease = zzimType === 'mutual' ? count * 2 : count; // 품앗이는 마일리지 2배 + + 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({ + zzim_mile: `zzim_mile + ${mileageIncrease}` + }) + }); + + // 2. 찜을 받은 마켓의 찜받은 개수 증가 (품앗이인 경우) + if (zzimType === 'mutual' && targetMarket && targetMarket.owner_user_id) { + // 찜을 받은 사용자의 my_zzim 증가 + 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({ + my_zzim: `my_zzim + ${count}` + }) + }); + + // 해당 마켓의 찜받은 개수 증가 + const ownerMarketsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetMarket.owner_user_id}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (ownerMarketsResponse.ok) { + const ownerData = await ownerMarketsResponse.json(); + const ownerMarkets = ownerData[0]?.my_markets || []; + + // 해당 마켓 찾아서 찜받은 개수 증가 + const updatedOwnerMarkets = ownerMarkets.map(market => { + if (market.market_url === targetMarket.market_url) { + 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.${targetMarket.owner_user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + my_markets: updatedOwnerMarkets + }) + }); + } + } + + // 3. 내 마켓 찜하기인 경우 내 찜받은 개수 증가 + if (zzimType === 'my_market') { + 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({ + my_zzim: `my_zzim + ${count}` + }) + }); + + // 내 마켓의 찜받은 개수도 증가 + const myMarketsResponse = 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 (myMarketsResponse.ok) { + const myData = await myMarketsResponse.json(); + const myMarkets = myData[0]?.my_markets || []; + + // 해당 마켓의 찜받은 개수 증가 + const updatedMyMarkets = myMarkets.map(market => { + if (market.market_url === targetMarket?.market_url) { + 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.${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: updatedMyMarkets + }) + }); + } + } + + } catch (error) { + console.error('찜 통계 업데이트 오류:', error); + } + } + + 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 +1592,22 @@ 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`, { + // 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=neq.${this.user_id}&select=my_markets,user_id&limit=50`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -382,187 +1619,41 @@ class ZzimManager { throw new Error('품앗이 마켓 목록을 가져올 수 없습니다.'); } - const markets = await response.json(); - if (markets.length === 0) { - this.showError('품앗이할 마켓이 없습니다.'); - return; - } + const data = await response.json(); + const mutualMarkets = []; - // 랜덤 마켓 선택 - const randomMarket = markets[Math.floor(Math.random() * markets.length)]; - this.showStatus(`품앗이 마켓 "${randomMarket.market_nickname}"에서 찜하기를 시작합니다...`); - - 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 visibleMarkets = markets.filter(market => market.is_visible !== false); - // 페이지 로드 완료 후 스크립트 실행 - 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초 후 - }); - } + // 사용자 정보를 마켓에 추가 + visibleMarkets.forEach(market => { + mutualMarkets.push({ + ...market, + owner_user_id: userMarket.user_id + }); }); - } else { - // 포그라운드에서 실행 (현재 창에서) - window.open(market.market_url, '_blank'); - } - - // 찜 기록 저장 - 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 // 예상 찜 개수 (실제로는 스크립트 결과를 받아야 함) - }) }); - // 통계 업데이트 - await this.updateZzimStats(50, zzimType); - - // UI 새로고침 - await this.loadZzimStats(); + // 랜덤하게 섞고 최대 10개만 반환 + const shuffled = mutualMarkets.sort(() => 0.5 - Math.random()); + return shuffled.slice(0, 10); } 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 +1663,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 +1679,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 +1693,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 +1838,9 @@ let zzimManager; // DOM 로드 완료 후 초기화 document.addEventListener('DOMContentLoaded', async () => { zzimManager = new ZzimManager(); + + // 전역 접근을 위해 window 객체에도 등록 + window.zzimManager = zzimManager; + await zzimManager.init(); }); \ No newline at end of file diff --git a/패킹.md b/패킹.md new file mode 100644 index 0000000..5c51c27 --- /dev/null +++ b/패킹.md @@ -0,0 +1 @@ +pyinstaller --onefile --windowed --add-data "wrmc_ext;wrmc_ext" --name "ExtensionInstaller" main.py \ No newline at end of file