diff --git a/main.py b/main.py index ff5fb5f..b3292eb 100644 --- a/main.py +++ b/main.py @@ -131,9 +131,9 @@ class ExtensionInstaller(QWidget): guide = QLabel( "설치 안내:
" - "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
" - "2. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
" - "3. 설치 후 브라우저별 단계별 안내를 참고하세요.
") + # "1. main.py와 같은 경로의 wrmc_ext 폴더가 자동 복사됩니다.
" + "1. 설치할 브라우저를 모두 체크 후 설치 시작을 누르세요.
" + "2. 설치 후 브라우저별 단계별 안내를 참고하세요.
") guide.setWordWrap(True) guide.setStyleSheet("margin-bottom:12px;") layout.addWidget(guide) @@ -256,7 +256,7 @@ class ExtensionInstaller(QWidget): row2.setTextFormat(Qt.RichText) row2.setWordWrap(True) # 3번 압축해제된 확장프로그램 로드 - row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 버튼 클릭") + row3 = QLabel("③ [압축해제된 확장 프로그램 로드] 또는 [압축해제된 확장앱 설치] 버튼 클릭") row3.setTextFormat(Qt.RichText) row3.setWordWrap(True) # 4번 폴더 선택 및 복사 diff --git a/wrmc_ext/background.js b/wrmc_ext/background.js index b516628..304a58e 100644 --- a/wrmc_ext/background.js +++ b/wrmc_ext/background.js @@ -16,8 +16,8 @@ chrome.runtime.onInstalled.addListener(() => { chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); - // 새 어록 감지 알람 생성 (5분마다) - chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 }); + // 새 어록 감지 알람 생성 (1분마다) + chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); // 초기 마지막 확인 시간 설정 chrome.storage.local.set({ lastSayingsCheck: Date.now() }); @@ -325,6 +325,9 @@ chrome.alarms.onAlarm.addListener((alarm) => { console.log("[background.js] 서비스 워커 유지 알람 실행됨"); } else if (alarm.name === "checkNewSayings") { checkForNewSayings(); + } else if (alarm.name === "autoMutualZzim") { + console.log("[background.js] autoMutualZzim 주기 실행"); + chrome.runtime.sendMessage({ action: "autoMutualZzim" }); } }); @@ -2699,7 +2702,9 @@ class TimeAlarmManager { enabled: true, workTime: 60, // 분 restTime: 5, // 분 - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 // 포모도로 현재 사이클(0~3) }; this.startTime = null; } @@ -2754,9 +2759,10 @@ class TimeAlarmManager { } this.startTime = Date.now(); - const workTimeMs = this.settings.workTime * 60 * 1000; + const workMin = this.settings.pomodoro ? 35 : this.settings.workTime; + const workTimeMs = workMin * 60 * 1000; - console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}분`); + console.log(`[TimeAlarm] 작업 타이머 시작: ${workMin}분`); this.workTimer = setTimeout(() => { this.showRestModal(); @@ -2787,7 +2793,8 @@ class TimeAlarmManager { } startBreakTimer(popupId) { - const breakTimeMs = this.settings.restTime * 60 * 1000; + const breakTimeMin = this.settings.restTime; + const breakTimeMs = breakTimeMin * 60 * 1000; console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}분`); @@ -2802,6 +2809,11 @@ class TimeAlarmManager { // 작업 완료 알림 this.showWorkCompleteNotification(); + // 포모도로 모드: 사이클 증가 및 리셋 + if (this.settings.pomodoro) { + this.settings.cycle = (this.settings.cycle + 1) % 4; + } + // 다음 작업 타이머 시작 this.startWorkTimer(); @@ -2813,7 +2825,7 @@ class TimeAlarmManager { type: 'basic', iconUrl: 'icon.png', title: '휴식 시간입니다! 🧘‍♀️', - message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.` + message: `${this.settings.pomodoro ? 35 : this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.` }); } @@ -2876,7 +2888,7 @@ class TimeAlarmManager { // 남은 시간 계산 const now = Date.now(); const elapsed = now - this.startTime; - const totalWorkTime = this.settings.workTime * 60 * 1000; + const totalWorkTime = (this.settings.pomodoro ? 35 : this.settings.workTime) * 60 * 1000; const remainingTime = totalWorkTime - elapsed; if (remainingTime <= 0) { @@ -2889,7 +2901,7 @@ class TimeAlarmManager { return { isRunning: true, remainingTime: remainingTime, - workTime: this.settings.workTime, + workTime: this.settings.pomodoro ? 35 : this.settings.workTime, restTime: this.settings.restTime, startTime: this.startTime, elapsed: elapsed @@ -3233,360 +3245,3 @@ 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/content.js b/wrmc_ext/content.js index 18ca614..a7551b0 100644 --- a/wrmc_ext/content.js +++ b/wrmc_ext/content.js @@ -13,386 +13,11 @@ 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") { @@ -693,6 +318,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; } + // 선택 영역 교체 요청 + if (message.action === 'replaceSelectedText' && message.text) { + // 선택 영역이 있으면 해당 부분만 교체, 아니면 포커스된 엘리먼트 전체 교체 + if (!replaceSelectedText(message.text)) { + // selection이 없다면, 포커스된 .edit 등 target element 전체 교체 시도 + const activeElem = document.activeElement; + if (activeElem && activeElem.classList.contains('edit')) { + activeElem.innerText = message.text; + } else { + // 특정 요소를 찾고 싶으면 아래처럼 직접 지정 + const target = document.querySelector('.edit[contenteditable="false"]'); + if (target) { + target.innerText = message.text; + } + } + } + sendResponse({success: true}); + return true; + } + // 로딩 인디케이터 제거 요청 if (message.action === "hideLoading") { removeLoadingIndicator(); @@ -878,6 +523,18 @@ function positionTooltip(tooltip, pos) { } } +// selection 영역 교체 함수 (간단 버전) +function replaceSelectedText(newText) { + const selection = window.getSelection(); + if (selection && selection.rangeCount) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(document.createTextNode(newText)); + return true; + } + return false; +} + function removeTooltip() { if (tooltipEl && tooltipEl.parentNode) { tooltipEl.parentNode.removeChild(tooltipEl); @@ -1381,569 +1038,9 @@ function getUserLevelGuide(userLevel) { // 번역 툴팁 제거 function removeTranslationTooltip() { - const existingTooltip = document.getElementById('translation-tooltip'); - if (existingTooltip) { - existingTooltip.remove(); + const translationTooltip = document.getElementById("translation-tooltip"); + if (translationTooltip) { + translationTooltip.remove(); + console.log('[content.js] 번역 툴팁 제거됨'); } } - -// 자동 찜하기 기능 -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 ea2b09f..425196b 100644 --- a/wrmc_ext/manifest.json +++ b/wrmc_ext/manifest.json @@ -21,6 +21,7 @@ "*://smartstore.naver.com/*", "*://translate.googleapis.com/*", "*://api.mymemory.translated.net/*", + "https://market.m.taobao.com/*", "*://openapi.naver.com/*", "*://api-free.deepl.com/*", "*://api.deepl.com/*", @@ -34,6 +35,7 @@ { "matches": [""], "js": ["content.js"], + "all_frames": true, "run_at": "document_end" } ], diff --git a/wrmc_ext/manual.html b/wrmc_ext/manual.html index 607b0b7..334bf28 100644 --- a/wrmc_ext/manual.html +++ b/wrmc_ext/manual.html @@ -330,7 +330,7 @@ @@ -343,6 +343,7 @@
  • 휴식시간 추천 활동
  • 실시간 타이머 알림
  • 개인화된 시간 설정
  • +
  • [포모도로 타이머가 활성화 되면 35분/5분으로 기본설정되며 4사이클 총180분(3시간) 휴식시간 추천 활동 제공] diff --git a/wrmc_ext/settings.html b/wrmc_ext/settings.html index f801582..32eb3e0 100644 --- a/wrmc_ext/settings.html +++ b/wrmc_ext/settings.html @@ -542,21 +542,29 @@
    알람 활성화/비활성화 + + +
    + +
    + 35분 작업 / 5분 휴식 (4회) 고정 +
    - + 분 (작업 후 휴식 알림)
    - + 분 (휴식 시간 길이)
    - + +
    diff --git a/wrmc_ext/settings.js b/wrmc_ext/settings.js index 6f1ff34..3230aea 100644 --- a/wrmc_ext/settings.js +++ b/wrmc_ext/settings.js @@ -236,7 +236,9 @@ class SettingsManager { enabled: true, workTime: 60, // 분 restTime: 5, // 분 - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 }; console.log('시간 알람 설정 로드 완료:', this.timeAlarmSettings); } catch (error) { @@ -245,7 +247,9 @@ class SettingsManager { enabled: true, workTime: 60, restTime: 5, - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 }; } } @@ -321,6 +325,7 @@ class SettingsManager { const workTimeInput = document.getElementById('workTimeInput'); const restTimeInput = document.getElementById('restTimeInput'); const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); + const pomodoroToggle = document.getElementById('pomodoroToggle'); if (workTimeInput) { workTimeInput.addEventListener('change', (e) => { @@ -339,6 +344,18 @@ class SettingsManager { this.timeAlarmSettings.autoZzim = e.target.checked; }); } + + if (pomodoroToggle) { + pomodoroToggle.addEventListener('click', () => { + this.timeAlarmSettings.pomodoro = !this.timeAlarmSettings.pomodoro; + // 포모도로가 활성화되면 고정 값 적용 + if (this.timeAlarmSettings.pomodoro) { + this.timeAlarmSettings.workTime = 35; + this.timeAlarmSettings.restTime = 5; + } + this.updateTimeAlarmUI(); + }); + } } updateUI() { @@ -388,12 +405,23 @@ class SettingsManager { } } + const pomodoroToggle = document.getElementById('pomodoroToggle'); + if (pomodoroToggle) { + if (this.timeAlarmSettings.pomodoro) { + pomodoroToggle.classList.add('active'); + } else { + pomodoroToggle.classList.remove('active'); + } + } + if (workTimeInput) { workTimeInput.value = this.timeAlarmSettings.workTime; + workTimeInput.disabled = this.timeAlarmSettings.pomodoro; } if (restTimeInput) { restTimeInput.value = this.timeAlarmSettings.restTime; + restTimeInput.disabled = this.timeAlarmSettings.pomodoro; } if (autoZzimCheckbox) { @@ -412,6 +440,12 @@ class SettingsManager { this.showLoading(true); // 시간 알람 설정 업데이트 + if (this.timeAlarmSettings.pomodoro) { + // 강제 고정 값 + this.timeAlarmSettings.workTime = 35; + this.timeAlarmSettings.restTime = 5; + } + const workTimeInput = document.getElementById('workTimeInput'); const restTimeInput = document.getElementById('restTimeInput'); const autoZzimCheckbox = document.getElementById('autoZzimCheckbox'); @@ -484,10 +518,12 @@ class SettingsManager { // 시간 알람 설정 초기화 this.timeAlarmSettings = { - enabled: false, + enabled: true, workTime: 60, restTime: 5, - autoZzim: false + autoZzim: false, + pomodoro: false, + cycle: 0 }; // 저장소에서 시간 알람 설정 제거 diff --git a/wrmc_ext/zzim.html b/wrmc_ext/zzim.html index 2c78337..46b8805 100644 --- a/wrmc_ext/zzim.html +++ b/wrmc_ext/zzim.html @@ -169,6 +169,37 @@ transition: opacity 0.3s ease; } + .market-item:hover::before { + opacity: 1; + 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; } @@ -180,10 +211,17 @@ margin-bottom: 15px; } + .market-checkbox { + display: flex; + align-items: flex-start; + margin-bottom: 15px; + } + .market-checkbox { display: flex; align-items: center; gap: 8px; + gap: 8px; margin-bottom: 10px; } @@ -205,6 +243,7 @@ .market-info { flex: 1; margin: 0 15px; + margin: 0 15px; } .market-name { @@ -212,6 +251,7 @@ color: #2c3e50; margin-bottom: 5px; font-size: 16px; + font-size: 16px; } .market-nickname { @@ -219,6 +259,9 @@ font-size: 14px; margin-bottom: 8px; font-weight: 500; + font-size: 14px; + margin-bottom: 8px; + font-weight: 500; } .market-url { @@ -270,6 +313,57 @@ border: 1px solid #f5c6cb; } + .created-date { + font-size: 12px; + color: #888; + background: #f8f9fa; + padding: 4px 8px; + border-radius: 12px; + 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; @@ -293,6 +387,22 @@ transition: all 0.2s ease; } + .market-actions .btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + 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); @@ -590,6 +700,257 @@ color: #666; margin-top: 5px; } + + /* 모달 스타일 */ + .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; + } @@ -693,8 +1054,66 @@ 체크하면 전체상품(/category/ALL) 대신 최신상품(/best)부터 찜합니다. + 💝 찜하기 작업 + +
    + +
    + 체크하면 새 탭에서 백그라운드로 찜하기를 실행합니다. +
    +
    + + +
    +

    ⚙️ 찜하기 옵션

    + + +
    + +
    +
    + 기본 간격: + + ms +
    +
    + + 추가 간격: + + ms +
    +
    + = 총 간격: + 1000ms +
    +
    +
    + 💡 찜과 찜 사이의 대기 시간을 설정합니다. (1초~10초) +
    +
    + + +
    + +
    + 체크하면 전체상품(/category/ALL) 대신 최신상품(/best)부터 찜합니다. +
    +
    +
    + +
    + + + + diff --git a/wrmc_ext/zzim.js b/wrmc_ext/zzim.js index aeed48e..455ddef 100644 --- a/wrmc_ext/zzim.js +++ b/wrmc_ext/zzim.js @@ -286,22 +286,45 @@ class ZzimManager { // 마켓 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`; + try { + // 기본 마켓 URL에서 마지막 슬래시 제거 + const baseUrl = marketUrl.replace(/\/$/, ''); + + console.log('[ZzimManager] 마켓 URL 생성:', { + baseUrl: baseUrl, + latestFirst: latestFirst + }); + + // 네이버 스마트스토어 URL 구조 분석 + const urlParts = baseUrl.split('/'); + const storeName = urlParts[urlParts.length - 1]; + + let targetUrl; + + if (latestFirst) { + // 최신상품 우선: /best?cp=1 + targetUrl = `${baseUrl}/best?cp=1`; + } else { + // 전체상품: /category/ALL?cp=1 + targetUrl = `${baseUrl}/category/ALL?cp=1`; + } + + console.log('[ZzimManager] 생성된 마켓 URL:', targetUrl); + return targetUrl; + + } catch (error) { + console.error('[ZzimManager] 마켓 URL 생성 오류:', error); + // 오류 발생 시 기본 URL 반환 + return marketUrl + (latestFirst ? '/best?cp=1' : '/category/ALL?cp=1'); } } async loadZzimStats() { try { - // users 테이블에서 찜 통계 가져오기 - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=my_zzim,zzim_mile`, { + console.log('찜 통계 로드 시작...'); + + // 1. 사용자 기본 정보 및 회원등급 조회 (users 테이블) + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=my_zzim,zzim_mile,available_zzim_mile,membership_level,today_zzim_count,today_zzim_date`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -309,22 +332,78 @@ class ZzimManager { } }); - if (!response.ok) { - throw new Error(`찜 통계 로드 실패: ${response.status}`); + if (!userRes.ok) { + throw new Error('사용자 통계 정보를 불러올 수 없습니다.'); } - const data = await response.json(); - const userStats = data[0] || { - my_zzim: 0, - zzim_mile: 0 + const userData = await userRes.json(); + const userStats = userData[0] || {}; + console.log('users 테이블에서 로드된 통계:', userStats); + + // 2. 회원등급별 제한 조회 + const membershipLevel = userStats.membership_level || 'basic'; + + const limitsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/membership_levels?level=eq.${membershipLevel}`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + let limits = { + daily_zzim_limit: 50, + max_zzim_mileage: 500, + mileage_per_zzim: 1, + max_markets: 5, + mutual_zzim_enabled: true, + background_zzim_enabled: false }; - // 오늘 찜한 개수는 별도로 계산 (임시로 0으로 설정, 실제로는 오늘 날짜 기준으로 계산) - const todayZzimCount = 0; // TODO: 실제 구현 시 오늘 찜한 개수 계산 - const dailyLimit = 100; // 일일 찜 제한 - const mileageLimit = 10000; // 마일리지 제한 + if (limitsResponse.ok) { + const limitsData = await limitsResponse.json(); + if (limitsData.length > 0) { + // membership_levels 테이블에는 제한 필드만 있으므로 기본 limits 객체와 병합 + limits = { ...limits, ...limitsData[0] }; + } + } else { + console.warn('회원등급별 제한 조회 실패, 기본값 사용'); + } - // UI 업데이트 + // 3. 오늘 찜한 개수 확인 및 리셋 + const today = new Date().toISOString().split('T')[0]; + const lastZzimDate = userStats.today_zzim_date; + let todayZzimCount = userStats.today_zzim_count || 0; + + // 날짜가 바뀌었으면 리셋 + if (lastZzimDate !== today) { + console.log('날짜 변경 감지, 일일 찜 카운트 리셋'); + + // users 테이블에 오늘 카운트를 0으로 초기화 + const resetUsersResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + today_zzim_count: 0, + today_zzim_date: today + }) + }); + + if (resetUsersResponse.ok) { + todayZzimCount = 0; + console.log('일일 찜 카운트 리셋 완료'); + } + } + + // 4. 사용 가능한 찜 마일리지 계산 + const totalZzimMile = userStats.zzim_mile || 0; + const availableZzimMile = userStats.available_zzim_mile || totalZzimMile; + + // 5. UI 업데이트 const todayCountEl = document.getElementById('today-zzim-count'); const todayLimitEl = document.getElementById('today-zzim-limit'); const mileageCountEl = document.getElementById('zzim-mileage'); @@ -332,14 +411,97 @@ class ZzimManager { const totalZzimEl = document.getElementById('total-zzim-received'); if (todayCountEl) todayCountEl.textContent = todayZzimCount; - if (todayLimitEl) todayLimitEl.textContent = dailyLimit; - if (mileageCountEl) mileageCountEl.textContent = userStats.zzim_mile || 0; - if (mileageLimitEl) mileageLimitEl.textContent = mileageLimit; + if (todayLimitEl) todayLimitEl.textContent = limits.daily_zzim_limit; + if (mileageCountEl) mileageCountEl.textContent = availableZzimMile; + if (mileageLimitEl) mileageLimitEl.textContent = limits.max_zzim_mileage; if (totalZzimEl) totalZzimEl.textContent = userStats.my_zzim || 0; + // 6. 진행률 표시 + const todayProgress = (todayZzimCount / limits.daily_zzim_limit) * 100; + const mileageProgress = (availableZzimMile / limits.max_zzim_mileage) * 100; + + // 진행률 바 업데이트 (있는 경우) + const todayProgressBar = document.getElementById('today-progress-bar'); + const mileageProgressBar = document.getElementById('mileage-progress-bar'); + + if (todayProgressBar) { + todayProgressBar.style.width = `${Math.min(todayProgress, 100)}%`; + todayProgressBar.className = `progress-bar ${todayProgress >= 100 ? 'full' : todayProgress >= 80 ? 'warning' : 'normal'}`; + } + + if (mileageProgressBar) { + mileageProgressBar.style.width = `${Math.min(mileageProgress, 100)}%`; + mileageProgressBar.className = `progress-bar ${mileageProgress >= 100 ? 'full' : mileageProgress >= 80 ? 'warning' : 'normal'}`; + } + + // 7. 제한 상태 표시 + const todayLimitReached = todayZzimCount >= limits.daily_zzim_limit; + const mileageLimitReached = availableZzimMile >= limits.max_zzim_mileage; + + // 버튼 상태 업데이트 + const myMarketBtn = document.getElementById('my-market-zzim-btn'); + const mutualZzimBtn = document.getElementById('mutual-zzim-btn'); + const backgroundModeCheckbox = document.getElementById('background-mode'); + + if (myMarketBtn) { + myMarketBtn.disabled = todayLimitReached; + myMarketBtn.title = todayLimitReached ? `일일 찜 제한 도달 (${todayZzimCount}/${limits.daily_zzim_limit})` : ''; + } + + if (mutualZzimBtn) { + mutualZzimBtn.disabled = !limits.mutual_zzim_enabled || mileageLimitReached; + mutualZzimBtn.title = !limits.mutual_zzim_enabled + ? '품앗이 기능이 비활성화되어 있습니다.' + : mileageLimitReached + ? `마일리지 한도 도달 (${availableZzimMile}/${limits.max_zzim_mileage})` + : ''; + } + + if (backgroundModeCheckbox) { + backgroundModeCheckbox.disabled = !limits.background_zzim_enabled; + if (!limits.background_zzim_enabled) { + backgroundModeCheckbox.checked = false; + backgroundModeCheckbox.title = '백그라운드 모드는 프리미엄/VIP 회원만 사용 가능합니다.'; + } + } + + // 8. 상태 메시지 표시 + const statusMessage = document.getElementById('zzim-status-message'); + if (statusMessage) { + if (todayLimitReached) { + statusMessage.innerHTML = `⚠️ 오늘 찜 제한에 도달했습니다. (${todayZzimCount}/${limits.daily_zzim_limit})`; + statusMessage.className = 'status-warning'; + } else if (mileageLimitReached) { + statusMessage.innerHTML = `⚠️ 찜 마일리지 한도에 도달했습니다. (${availableZzimMile}/${limits.max_zzim_mileage})`; + statusMessage.className = 'status-warning'; + } else { + statusMessage.innerHTML = `✅ 찜하기 가능 (오늘: ${todayZzimCount}/${limits.daily_zzim_limit}, 마일리지: ${availableZzimMile}/${limits.max_zzim_mileage})`; + statusMessage.className = 'status-success'; + } + } + + console.log('찜 통계 로드 완료:', { + membershipLevel, + todayZzimCount, + dailyLimit: limits.daily_zzim_limit, + availableZzimMile, + maxZzimMileage: limits.max_zzim_mileage, + totalReceived: userStats.my_zzim, + mutualEnabled: limits.mutual_zzim_enabled, + backgroundEnabled: limits.background_zzim_enabled + }); + + // 현재 제한 정보를 인스턴스 변수에 저장 + this.currentLimits = limits; + this.currentStats = { + todayZzimCount, + availableZzimMile, + totalReceived: userStats.my_zzim || 0 + }; + } catch (error) { console.error('찜 통계 로드 오류:', error); - this.showError('찜 통계를 불러올 수 없습니다.'); + this.showError('찜 통계를 불러올 수 없습니다: ' + error.message); } } @@ -415,14 +577,26 @@ class ZzimManager { const marketHTML = markets.map((market, index) => { console.log(`마켓 ${index} 렌더링:`, market); + + // 고유 식별자 생성 (마켓 URL + 생성일시) + const marketId = `${market.market_url}_${market.created_at}`; + return ` -
    +
    -
    - - +
    +
    + + +
    +
    + + +
    ${market.market_name || '마켓명 없음'}
    @@ -437,11 +611,11 @@ class ZzimManager {
    - - + - +
    @@ -462,8 +636,9 @@ class ZzimManager { 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 marketId = e.target.dataset.marketId; + const checkboxType = e.target.dataset.checkboxType; + this.toggleMarketCheckbox(marketId, checkboxType, e.target.checked); }); }); @@ -471,8 +646,8 @@ class ZzimManager { 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 marketId = e.target.dataset.marketId; + this.editMarket(marketId); }); }); @@ -480,8 +655,8 @@ class ZzimManager { 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 marketId = e.target.dataset.marketId; + this.toggleMarketVisibility(marketId); }); }); @@ -489,8 +664,8 @@ class ZzimManager { const deleteBtns = document.querySelectorAll('.market-delete-btn'); deleteBtns.forEach(btn => { btn.addEventListener('click', (e) => { - const marketIndex = parseInt(e.target.dataset.marketIndex); - this.deleteMarket(marketIndex); + const marketId = e.target.dataset.marketId; + this.deleteMarket(marketId); }); }); } @@ -517,8 +692,8 @@ class ZzimManager { } } - // 마켓 찜하기 체크박스 토글 - async toggleMarketForZzim(marketIndex, isChecked) { + // 마켓 체크박스 토글 (통합) + async toggleMarketCheckbox(marketId, checkboxType, isChecked) { try { // 기존 마켓 목록 가져오기 const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { @@ -536,17 +711,22 @@ class ZzimManager { const data = await response.json(); const markets = data[0]?.my_markets || []; - if (marketIndex < 0 || marketIndex >= markets.length) { - this.showError('잘못된 마켓 인덱스입니다.'); + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); return; } - // 찜하기 대상 상태 업데이트 - markets[marketIndex] = { - ...markets[marketIndex], - for_zzim: isChecked, - updated_at: new Date().toISOString() - }; + // 체크박스 타입에 따라 해당 필드 업데이트 + if (checkboxType === 'for_zzim') { + market.for_zzim = isChecked; + } else if (checkboxType === 'for_mutual_zzim') { + market.for_mutual_zzim = isChecked; + } + + market.updated_at = new Date().toISOString(); // user_markets 테이블 업데이트 const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { @@ -562,17 +742,21 @@ class ZzimManager { }); if (!updateResponse.ok) { - throw new Error('마켓 찜하기 설정 변경 실패'); + throw new Error('마켓 설정 변경 실패'); } - this.showSuccess(`마켓이 ${isChecked ? '찜하기 대상에 포함' : '찜하기 대상에서 제외'}되었습니다.`); + const statusMessage = checkboxType === 'for_zzim' + ? (isChecked ? '내 찜하기 대상에 포함' : '내 찜하기 대상에서 제외') + : (isChecked ? '품앗이 대상에 포함' : '품앗이 대상에서 제외'); + + this.showSuccess(`마켓이 ${statusMessage}되었습니다.`); } catch (error) { - console.error('마켓 찜하기 설정 변경 오류:', error); - this.showError('마켓 찜하기 설정 변경 중 오류가 발생했습니다.'); + console.error('마켓 설정 변경 오류:', error); + this.showError('마켓 설정 변경 중 오류가 발생했습니다.'); // 오류 발생 시 체크박스 상태 원복 - const checkbox = document.getElementById(`market-check-${marketIndex}`); + const checkbox = document.querySelector(`[data-market-id="${marketId}"][data-checkbox-type="${checkboxType}"]`); if (checkbox) { checkbox.checked = !isChecked; } @@ -671,6 +855,7 @@ class ZzimManager { market_nickname: marketNickname, is_visible: true, // 다른 사람에게 노출 (기본값: 노출) for_zzim: true, // 내가 찜하기 할 때 포함 (기본값: 포함) + for_mutual_zzim: true, // 품앗이 대상 여부 (기본값: 포함) zzim_received_count: 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString() @@ -751,7 +936,7 @@ class ZzimManager { } } - async editMarket(marketIndex) { + async editMarket(marketId) { try { // 기존 마켓 목록 가져오기 const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { @@ -769,15 +954,16 @@ class ZzimManager { const data = await response.json(); const markets = data[0]?.my_markets || []; - if (marketIndex < 0 || marketIndex >= markets.length) { - this.showError('잘못된 마켓 인덱스입니다.'); + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); return; } - const market = markets[marketIndex]; - // 모달에 기존 데이터 채우기 - this.currentEditIndex = marketIndex; + this.currentEditIndex = markets.indexOf(market); this.openEditModal(market); } catch (error) { @@ -793,6 +979,7 @@ class ZzimManager { const nameInput = document.getElementById('edit-market-name'); const nicknameInput = document.getElementById('edit-market-nickname'); const forZzimCheckbox = document.getElementById('edit-market-for-zzim'); + const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim'); const visibleCheckbox = document.getElementById('edit-market-visible'); // 기존 데이터로 폼 채우기 @@ -800,6 +987,7 @@ class ZzimManager { if (nameInput) nameInput.value = market.market_name || ''; if (nicknameInput) nicknameInput.value = market.market_nickname || ''; if (forZzimCheckbox) forZzimCheckbox.checked = market.for_zzim !== false; + if (forMutualZzimCheckbox) forMutualZzimCheckbox.checked = market.for_mutual_zzim !== false; if (visibleCheckbox) visibleCheckbox.checked = market.is_visible !== false; // 모달 표시 @@ -858,6 +1046,7 @@ class ZzimManager { const nameInput = document.getElementById('edit-market-name'); const nicknameInput = document.getElementById('edit-market-nickname'); const forZzimCheckbox = document.getElementById('edit-market-for-zzim'); + const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim'); const visibleCheckbox = document.getElementById('edit-market-visible'); const saveBtn = document.querySelector('.btn-modal-save'); @@ -865,6 +1054,7 @@ class ZzimManager { const marketName = nameInput?.value.trim() || ''; const marketNickname = nicknameInput?.value.trim() || ''; const forZzim = forZzimCheckbox?.checked || false; + const forMutualZzim = forMutualZzimCheckbox?.checked || false; const isVisible = visibleCheckbox?.checked || false; // 유효성 검사 @@ -927,6 +1117,7 @@ class ZzimManager { market_name: marketName, market_nickname: marketNickname, for_zzim: forZzim, + for_mutual_zzim: forMutualZzim, is_visible: isVisible, updated_at: new Date().toISOString() }; @@ -966,7 +1157,7 @@ class ZzimManager { } } - async toggleMarketVisibility(marketIndex) { + async toggleMarketVisibility(marketId) { try { // 기존 마켓 목록 가져오기 const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { @@ -984,17 +1175,17 @@ class ZzimManager { const data = await response.json(); const markets = data[0]?.my_markets || []; - if (marketIndex < 0 || marketIndex >= markets.length) { - this.showError('잘못된 마켓 인덱스입니다.'); - return; - } - + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); + return; + } + // 노출 상태 토글 - markets[marketIndex] = { - ...markets[marketIndex], - is_visible: !markets[marketIndex].is_visible, - updated_at: new Date().toISOString() - }; + market.is_visible = !market.is_visible; + market.updated_at = new Date().toISOString(); // user_markets 테이블 업데이트 const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { @@ -1014,7 +1205,7 @@ class ZzimManager { } await this.loadMyMarkets(); - this.showSuccess(`마켓이 ${markets[marketIndex].is_visible ? '노출' : '숨김'} 상태로 변경되었습니다.`); + this.showSuccess(`마켓이 ${market.is_visible ? '노출' : '숨김'} 상태로 변경되었습니다.`); } catch (error) { console.error('마켓 노출 설정 변경 오류:', error); @@ -1022,7 +1213,7 @@ class ZzimManager { } } - async deleteMarket(marketIndex) { + async deleteMarket(marketId) { try { // 기존 마켓 목록 가져오기 const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { @@ -1040,19 +1231,20 @@ class ZzimManager { const data = await response.json(); const markets = data[0]?.my_markets || []; - if (marketIndex < 0 || marketIndex >= markets.length) { - this.showError('잘못된 마켓 인덱스입니다.'); + // 마켓 ID로 해당 마켓 찾기 + const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId); + + if (!market) { + this.showError('잘못된 마켓 ID입니다.'); return; } - const market = markets[marketIndex]; - if (!confirm(`정말로 "${market.market_nickname}" 마켓을 삭제하시겠습니까?`)) { return; } // 마켓 삭제 (배열에서 제거) - markets.splice(marketIndex, 1); + markets.splice(markets.indexOf(market), 1); // user_markets 테이블 업데이트 const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, { @@ -1409,8 +1601,25 @@ class ZzimManager { // 마켓 URL에서 스토어 ID 추출 const storeId = market.market_url.split('/').pop(); + // 올바른 네이버 스마트스토어 URL 생성 + let finalTargetUrl = targetUrl; + + // URL에 찜하기 파라미터 추가 + const urlObj = new URL(finalTargetUrl); + urlObj.searchParams.set('auto_zzim', 'true'); + urlObj.searchParams.set('max_zzim', '50'); + + // 네이버 스마트스토어는 cp 파라미터를 사용 (page가 아님) + if (!urlObj.searchParams.has('cp')) { + urlObj.searchParams.set('cp', '1'); // 첫 번째 페이지부터 시작 + } + + finalTargetUrl = urlObj.toString(); + + console.log('[ZzimManager] 최종 찜하기 URL:', finalTargetUrl); + // 로그인 리다이렉트 URL 생성 - const loginRedirectUrl = `https://nid.naver.com/nidlogin.login?url=${encodeURIComponent(targetUrl + (targetUrl.includes('?') ? '&' : '?') + 'auto_zzim=true&max_zzim=50')}`; + const loginRedirectUrl = `https://nid.naver.com/nidlogin.login?url=${encodeURIComponent(finalTargetUrl)}`; this.showSuccess(`찜하기 페이지로 이동합니다. (${market.market_nickname})`); @@ -1448,25 +1657,23 @@ class ZzimManager { async updateZzimStats(count, zzimType, targetMarket = null) { try { - // 1. 찜을 해준 사용자의 마일리지 증가 - const mileageIncrease = zzimType === 'mutual' ? count * 2 : count; // 품앗이는 마일리지 2배 + console.log('찜 통계 업데이트 시작:', { count, zzimType, targetMarket }); - 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}` - }) - }); + // 현재 제한 정보 확인 + if (!this.currentLimits) { + console.warn('제한 정보가 없음, 통계 다시 로드'); + await this.loadZzimStats(); + } - // 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}`, { + const limits = this.currentLimits || { mileage_per_zzim: 1 }; + const today = new Date().toISOString().split('T')[0]; + + if (zzimType === 'my_market') { + // 내 마켓 찜하기: 오늘 찜한 개수 증가 + 받은 찜 개수 증가 + console.log('내 마켓 찜하기 통계 업데이트'); + + // users 테이블 업데이트 + const myMarketUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, @@ -1474,37 +1681,53 @@ class ZzimManager { 'Content-Type': 'application/json' }, body: JSON.stringify({ + today_zzim_count: `today_zzim_count + ${count}`, + today_zzim_date: today, 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`, { + if (!myMarketUpdateResponse.ok) { + throw new Error('내 마켓 찜하기 통계 업데이트 실패'); + } + + // user_markets 테이블의 집계 필드는 제거되었으므로 별도 업데이트 필요 없음 + + // 내 마켓의 찜받은 개수도 증가 + if (targetMarket) { + await this.updateMarketZzimCount(targetMarket.market_url, count); + } + + // 찜 기록 저장 + await this.saveZzimRecord(zzimType, targetMarket, count, 0); + + } else if (zzimType === 'mutual') { + // 품앗이 찜하기: 내 마일리지 차감 + 상대방 마일리지 증가 + 상대방 받은 찜 개수 증가 + console.log('품앗이 찜하기 통계 업데이트'); + + const mileageUsed = count * limits.mileage_per_zzim; + + // 내 마일리지 차감 (users 테이블) + const myMileageUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' - } + }, + body: JSON.stringify({ + available_zzim_mile: `available_zzim_mile - ${mileageUsed}` + }) }); - if (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}`, { + if (!myMileageUpdateResponse.ok) { + throw new Error('내 마일리지 차감 실패'); + } + + // 상대방 마일리지 증가 및 받은 찜 개수 증가 + if (targetMarket && targetMarket.owner_user_id) { + // 상대방 users 테이블 업데이트 + const targetUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${targetMarket.owner_user_id}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, @@ -1512,15 +1735,88 @@ class ZzimManager { 'Content-Type': 'application/json' }, body: JSON.stringify({ - my_markets: updatedOwnerMarkets + zzim_mile: `zzim_mile + ${mileageUsed}`, + available_zzim_mile: `available_zzim_mile + ${mileageUsed}`, + my_zzim: `my_zzim + ${count}` }) }); + + if (!targetUpdateResponse.ok) { + console.warn('상대방 마일리지 및 받은 찜 개수 업데이트 실패'); + } + + // (중복 패치 제거: targetUpdateResponse 에서 이미 users 테이블을 갱신했습니다.) + // 상대방 users 테이블도 업데이트 (호환성 유지) + const targetUsersUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${targetMarket.owner_user_id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + zzim_mile: `zzim_mile + ${mileageUsed}`, + available_zzim_mile: `available_zzim_mile + ${mileageUsed}`, + my_zzim: `my_zzim + ${count}` + }) + }); + + if (!targetUsersUpdateResponse.ok) { + console.warn('상대방 users 테이블 업데이트 실패'); + } + + // 상대방 마켓의 찜받은 개수도 증가 + await this.updateMarketZzimCount(targetMarket.market_url, count, targetMarket.owner_user_id); } + + // 찜 기록 저장 + await this.saveZzimRecord(zzimType, targetMarket, count, -mileageUsed); + + console.log(`품앗이 완료: 마일리지 ${mileageUsed} 사용, 찜 ${count}개 제공`); } - // 3. 내 마켓 찜하기인 경우 내 찜받은 개수 증가 - if (zzimType === 'my_market') { - await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, { + // 통계 다시 로드하여 UI 업데이트 + await this.loadZzimStats(); + + console.log('찜 통계 업데이트 완료'); + + } catch (error) { + console.error('찜 통계 업데이트 오류:', error); + throw error; + } + } + + // 마켓별 찜받은 개수 업데이트 + async updateMarketZzimCount(marketUrl, count, userId = null) { + try { + const targetUserId = userId || this.user_id; + + const marketsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetUserId}&select=my_markets`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (marketsResponse.ok) { + const marketsData = await marketsResponse.json(); + const markets = marketsData[0]?.my_markets || []; + + // 해당 마켓 찾아서 찜받은 개수 증가 + const updatedMarkets = markets.map(market => { + if (market.market_url === marketUrl) { + return { + ...market, + zzim_received_count: (market.zzim_received_count || 0) + count, + updated_at: new Date().toISOString() + }; + } + return market; + }); + + // 업데이트된 마켓 목록 저장 + await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetUserId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, @@ -1528,53 +1824,88 @@ class ZzimManager { 'Content-Type': 'application/json' }, body: JSON.stringify({ - my_zzim: `my_zzim + ${count}` + my_markets: updatedMarkets }) }); - // 내 마켓의 찜받은 개수도 증가 - 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', + console.log(`마켓 ${marketUrl} 찜받은 개수 ${count} 증가`); + } + } catch (error) { + console.error('마켓 찜받은 개수 업데이트 오류:', error); + } + } + + // 찜 기록 저장 + async saveZzimRecord(zzimType, targetMarket, zzimCount, mileageEarned) { + try { + const record = { + user_id: this.user_id, + market_url: targetMarket?.market_url || '', + market_name: targetMarket?.market_name || '', + market_nickname: targetMarket?.market_nickname || '', + zzim_type: zzimType, + zzim_count: zzimCount, + mileage_earned: mileageEarned, + target_user_id: targetMarket?.owner_user_id || null, + created_at: new Date().toISOString() + }; + + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/jjim`, { + method: 'POST', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - my_markets: updatedMyMarkets - }) + body: JSON.stringify(record) }); - } + + if (!response.ok) { + console.warn('찜 기록 저장 실패:', response.status); + } else { + console.log('찜 기록 저장 완료'); + } + } catch (error) { + console.error('찜 기록 저장 오류:', error); + } + } + + // 찜하기 가능 여부 확인 + canZzim(zzimType) { + if (!this.currentStats || !this.currentLimits) { + return { canZzim: false, reason: '통계 정보를 불러오는 중입니다.' }; + } + + if (zzimType === 'my_market') { + // 내 마켓 찜하기: 오늘 찜한 개수 확인 + const todayCount = this.currentStats.todayZzimCount; + const dailyLimit = this.currentLimits.daily_zzim_limit; + + if (todayCount >= dailyLimit) { + return { + canZzim: false, + reason: `오늘 찜 제한에 도달했습니다. (${todayCount}/${dailyLimit})` + }; } - } catch (error) { - console.error('찜 통계 업데이트 오류:', error); + return { canZzim: true, remaining: dailyLimit - todayCount }; + + } else if (zzimType === 'mutual') { + // 품앗이 찜하기: 사용 가능한 마일리지 확인 + const availableMileage = this.currentStats.availableZzimMile; + const requiredMileage = 50; // 마켓당 50개 찜 가정 + + if (availableMileage < requiredMileage) { + return { + canZzim: false, + reason: `품앗이에 필요한 마일리지가 부족합니다. (보유: ${availableMileage}, 필요: ${requiredMileage})` + }; + } + + return { canZzim: true, remaining: Math.floor(availableMileage / requiredMileage) }; } + + return { canZzim: false, reason: '알 수 없는 찜 타입입니다.' }; } async getActiveMarkets() { @@ -1606,8 +1937,31 @@ class ZzimManager { async getMutualMarkets() { try { - // 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=neq.${this.user_id}&select=my_markets,user_id&limit=50`, { + console.log('[품앗이] 품앗이 마켓 목록 조회 시작'); + + // 내 현재 마일리지 확인 (users 테이블) + const myStatsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=available_zzim_mile`, { + headers: { + 'Authorization': `Bearer ${this.access_token}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + let myAvailableMileage = 0; + if (myStatsResponse.ok) { + const myData = await myStatsResponse.json(); + myAvailableMileage = myData[0]?.available_zzim_mile || 0; + } + + console.log('[품앗이] 내 사용 가능한 마일리지:', myAvailableMileage); + + if (myAvailableMileage <= 0) { + throw new Error('사용 가능한 마일리지가 없습니다. 먼저 다른 사람의 마켓에 찜을 받아 마일리지를 적립하세요.'); + } + + // 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - v_user_market_stats 뷰 사용 + const response = await fetch(`${this.SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${this.user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, @@ -1620,25 +1974,56 @@ class ZzimManager { } const data = await response.json(); + console.log('[품앗이] 조회된 사용자 데이터:', data.length); + const mutualMarkets = []; // 각 사용자의 노출된 마켓들을 수집 data.forEach(userMarket => { const markets = userMarket.my_markets || []; - const visibleMarkets = markets.filter(market => market.is_visible !== false); + const userMileage = userMarket.available_zzim_mile || 0; - // 사용자 정보를 마켓에 추가 - visibleMarkets.forEach(market => { - mutualMarkets.push({ - ...market, - owner_user_id: userMarket.user_id + // 마일리지가 있는 사용자의 노출된 마켓만 수집 + if (userMileage > 0) { + const visibleMarkets = markets.filter(market => + market.is_visible !== false && + market.for_mutual_zzim !== false // 품앗이 대상으로 설정된 마켓만 + ); + + // 사용자 정보를 마켓에 추가 + visibleMarkets.forEach(market => { + mutualMarkets.push({ + ...market, + owner_user_id: userMarket.user_id, + owner_available_mileage: userMileage + }); }); - }); + } }); - // 랜덤하게 섞고 최대 10개만 반환 - const shuffled = mutualMarkets.sort(() => 0.5 - Math.random()); - return shuffled.slice(0, 10); + console.log('[품앗이] 수집된 품앗이 마켓 수:', mutualMarkets.length); + + if (mutualMarkets.length === 0) { + throw new Error('현재 품앗이 가능한 마켓이 없습니다.'); + } + + // 우선순위 기반 정렬 (마일리지가 많은 사용자 우선) + mutualMarkets.sort((a, b) => { + // 1차: 마일리지 많은 순 + if (b.owner_available_mileage !== a.owner_available_mileage) { + return b.owner_available_mileage - a.owner_available_mileage; + } + // 2차: 최신 등록 순 + return new Date(b.created_at || 0) - new Date(a.created_at || 0); + }); + + // 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개) + const maxMarkets = Math.min(10, Math.floor(myAvailableMileage / 50)); // 마켓당 50개 찜 가정 + const selectedMarkets = mutualMarkets.slice(0, maxMarkets); + + console.log('[품앗이] 선택된 품앗이 마켓:', selectedMarkets.length); + + return selectedMarkets; } catch (error) { console.error('품앗이 마켓 목록 조회 오류:', error);