Enhance background.js and content.js with new auto mutual zzim functionality, improve text selection detection, and update context menu options. Adjust alarm settings for new sayings detection and implement advanced error handling in zzim operations.

This commit is contained in:
9700X_PC 2025-11-27 15:45:31 +09:00
parent 220e7e42da
commit debe58a575
3 changed files with 1939 additions and 12 deletions

View File

@ -16,8 +16,11 @@ chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
// 새 어록 감지 알람 생성 (1분마다) // 새 어록 감지 알람 생성 (5분마다)
chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 });
// 1시간마다 자동 품앗이 찜 알람
chrome.alarms.create("autoMutualZzim", { periodInMinutes: 60 });
// 초기 마지막 확인 시간 설정 // 초기 마지막 확인 시간 설정
chrome.storage.local.set({ lastSayingsCheck: Date.now() }); chrome.storage.local.set({ lastSayingsCheck: Date.now() });
@ -3245,3 +3248,467 @@ function getBackendConfig() {
} }
// 검색 결과 개선을 위한 키워드 확장 함수 // 검색 결과 개선을 위한 키워드 확장 함수
// 백그라운드 찜하기 실행 함수
async function handleExecuteBackgroundZzim(message, sendResponse) {
try {
console.log('[Background] 백그라운드 찜하기 시작:', message);
const { market, zzimType, userId, accessToken } = message;
if (!market || !market.market_url) {
throw new Error('마켓 정보가 없습니다.');
}
// 백그라운드 탭 생성
const tab = await chrome.tabs.create({
url: market.market_url,
active: false
});
console.log('[Background] 백그라운드 탭 생성됨:', tab.id);
// 찜하기 스크립트 정의
const zzimScript = function() {
return new Promise((resolve) => {
let zzimCount = 0;
const maxZzim = 50; // 최대 찜할 개수
let isRunning = true;
console.log('찜하기 스크립트 시작');
function findZzimButtons() {
// 네이버 스마트스토어의 찜 버튼 선택자들
const selectors = [
'button[data-testid="wishlist-button"]:not([aria-pressed="true"])',
'.zzim_button:not(.active)',
'.wish_button:not(.active)',
'button[class*="wish"]:not([class*="active"])',
'button[aria-label*="찜"]:not([aria-pressed="true"])'
];
let buttons = [];
for (const selector of selectors) {
buttons = document.querySelectorAll(selector);
if (buttons.length > 0) {
console.log(`찜 버튼 발견 (${selector}):`, buttons.length);
break;
}
}
return Array.from(buttons);
}
function clickZzimButtons() {
if (!isRunning || zzimCount >= maxZzim) {
console.log('찜하기 중단:', { isRunning, zzimCount, maxZzim });
return false;
}
const zzimButtons = findZzimButtons();
console.log('찾은 찜 버튼 개수:', zzimButtons.length);
if (zzimButtons.length === 0) {
console.log('더 이상 찜할 상품이 없습니다.');
return false;
}
// 최대 10개씩 찜하기
const buttonsToClick = zzimButtons.slice(0, Math.min(10, maxZzim - zzimCount));
buttonsToClick.forEach((btn, index) => {
setTimeout(() => {
if (!isRunning) return;
try {
// 버튼이 화면에 보이도록 스크롤
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
if (!isRunning) return;
btn.click();
zzimCount++;
console.log(`찜 버튼 클릭: ${zzimCount}`);
// 클릭 후 잠시 대기
setTimeout(() => {
// 추가 확인 버튼이 있다면 클릭
const confirmBtn = document.querySelector('button[data-testid="confirm"], .confirm_btn, button:contains("확인")');
if (confirmBtn) {
confirmBtn.click();
}
}, 200);
}, 300); // 스크롤 후 클릭까지 대기
} catch (e) {
console.error('찜 버튼 클릭 오류:', e);
}
}, index * 800); // 0.8초 간격으로 클릭
});
return buttonsToClick.length > 0;
}
// 페이지 스크롤 및 찜하기 반복
function scrollAndZzim() {
if (!isRunning || zzimCount >= maxZzim) {
console.log('찜하기 완료:', zzimCount);
resolve(zzimCount);
return;
}
// 페이지 하단으로 스크롤
window.scrollTo(0, document.body.scrollHeight);
// 스크롤 후 잠시 대기하여 새 상품 로드
setTimeout(() => {
if (clickZzimButtons()) {
setTimeout(scrollAndZzim, 5000); // 5초 후 다시 시도
} else {
console.log('더 이상 찜할 상품이 없어 종료');
resolve(zzimCount);
}
}, 2000);
}
// 30초 후 자동 종료
setTimeout(() => {
isRunning = false;
console.log('시간 초과로 찜하기 종료');
resolve(zzimCount);
}, 30000);
// 시작
console.log('찜하기 시작');
setTimeout(scrollAndZzim, 1000); // 페이지 로드 후 1초 대기
});
};
// 페이지 로드 완료 후 스크립트 실행
const executeZzim = (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.status === 'complete') {
chrome.tabs.onUpdated.removeListener(executeZzim);
console.log('[Background] 페이지 로드 완료, 찜하기 스크립트 실행');
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: zzimScript
}).then((results) => {
const zzimCount = results[0]?.result || 0;
console.log('[Background] 찜하기 완료:', zzimCount);
// 탭 닫기
setTimeout(() => {
chrome.tabs.remove(tab.id).catch(console.error);
}, 2000);
sendResponse({
success: true,
zzimCount: zzimCount,
message: `${zzimCount}개 상품을 찜했습니다.`
});
}).catch((error) => {
console.error('[Background] 찜하기 스크립트 실행 오류:', error);
chrome.tabs.remove(tab.id).catch(console.error);
sendResponse({
success: false,
error: '찜하기 스크립트 실행 실패: ' + error.message
});
});
}
};
chrome.tabs.onUpdated.addListener(executeZzim);
// 타임아웃 설정 (1분)
setTimeout(() => {
chrome.tabs.onUpdated.removeListener(executeZzim);
chrome.tabs.remove(tab.id).catch(console.error);
sendResponse({
success: false,
error: '찜하기 실행 시간 초과'
});
}, 60000);
} catch (error) {
console.error('[Background] 백그라운드 찜하기 오류:', error);
sendResponse({
success: false,
error: error.message
});
}
}
// 메시지 리스너 추가 - content.js에서 보내는 메시지 처리
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[Background] 메시지 수신:', message);
// 지재권 검색 요청
if (message.action === "searchTrademark") {
console.log('[Background] 지재권 검색 요청 처리:', message.keyword);
handleTrademarkSearch(message.keyword, sender.tab)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 지재권 검색 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 멀티번역 요청
if (message.action === "translateText") {
console.log('[Background] 멀티번역 요청 처리:', message.text);
handleMultiTranslate({ selectionText: message.text })
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 멀티번역 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 직번역 요청
if (message.action === "handleDirectTranslation") {
const selectedText = message.selectedText || message.text;
console.log('[Background] 직번역 요청 처리:', selectedText);
handleDirectTranslation(selectedText, sender.tab)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 직번역 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 한중번역 요청
if (message.action === "handleKoreanToChinese") {
const selectedText = message.selectedText || message.text;
console.log('[Background] 한중번역 요청 처리:', selectedText);
handleKoreanToChinese(selectedText, sender.tab)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 한중번역 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 금지어 추가 요청
if (message.action === "addBannedWord") {
handleAddBannedWord(message, sendResponse);
return true; // 비동기 응답
}
});
// 직번역 처리 함수 (선택된 텍스트를 바로 번역된 텍스트로 대체)
async function handleDirectTranslation(selectedText, tab) {
if (!selectedText) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '텍스트 선택 필요',
message: '번역할 텍스트를 먼저 선택해주세요.'
});
return;
}
console.log('[background.js] 직번역 요청:', selectedText);
try {
// 언어 감지 및 번역 방향 결정
const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(selectedText);
const isChinese = /[\u4e00-\u9fff]/.test(selectedText);
const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(selectedText.trim());
let translatedText;
let direction;
if (isKorean && !isChinese) {
// 한국어 → 중국어
translatedText = await translateText(selectedText, 'ko', 'zh');
direction = '한국어 → 중국어';
} else if (isChinese && !isKorean) {
// 중국어 → 한국어
translatedText = await translateText(selectedText, 'zh', 'ko');
direction = '중국어 → 한국어';
} else if (isEnglish && !isKorean && !isChinese) {
// 영어 → 한국어
translatedText = await translateText(selectedText, 'en', 'ko');
direction = '영어 → 한국어';
} else if (isKorean && isChinese) {
// 한국어와 중국어가 섞여있는 경우 - 한국어 비율로 판단
const koreanChars = (selectedText.match(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g) || []).length;
const chineseChars = (selectedText.match(/[\u4e00-\u9fff]/g) || []).length;
if (koreanChars >= chineseChars) {
// 한국어가 더 많으면 중국어로 번역
translatedText = await translateText(selectedText, 'ko', 'zh');
direction = '한국어 → 중국어';
} else {
// 중국어가 더 많으면 한국어로 번역
translatedText = await translateText(selectedText, 'zh', 'ko');
direction = '중국어 → 한국어';
}
} else {
// 기타 언어는 한국어로 번역
translatedText = await translateText(selectedText, 'auto', 'ko');
direction = '자동감지 → 한국어';
}
// 선택된 텍스트를 번역된 텍스트로 대체
console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText);
try {
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
function: replaceSelectedText,
args: [translatedText]
});
console.log('[background.js] replaceSelectedText 실행 결과:', result);
if (result && result[0] && result[0].result !== undefined) {
console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result);
} else {
console.log('[background.js] 스크립트 실행 완료, 반환값 없음');
}
} catch (scriptError) {
console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError);
}
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '직번역 완료',
message: `${direction}로 번역되어 텍스트가 대체되었습니다.`
});
} catch (error) {
console.error('[background.js] 직번역 중 오류:', error);
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '직번역 오류',
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
});
}
}
// ===== 메시지 리스너: 백그라운드 자동 품앗이 =====
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.action === 'autoMutualZzim') {
autoMutualZzim()
.then(() => sendResponse({ success: true }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // async 응답
}
});
// ===== 자동 품앗이 찜 메인 함수 =====
async function autoMutualZzim() {
try {
console.log('[autoMutualZzim] 시작');
// 1. 기본 정보 및 설정
const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
if (!access_token || !user_id) {
console.log('[autoMutualZzim] 토큰/사용자 정보 없음');
return;
}
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 2. 내 마일리지 및 회원등급 조회
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=available_zzim_mile,membership_level`, {
headers: {
Authorization: `Bearer ${access_token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!userRes.ok) {
console.warn('[autoMutualZzim] 사용자 조회 실패', userRes.status);
return;
}
const me = (await userRes.json())[0];
const myAvail = me?.available_zzim_mile || 0;
const myLevel = me?.membership_level || 'basic';
// 2-1. 등급별 한도 확인
const levelRes = await fetch(`${SUPABASE_URL}/rest/v1/membership_levels?level=eq.${myLevel}&select=max_zzim_mileage,mileage_per_zzim`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
});
const levelConf = levelRes.ok ? (await levelRes.json())[0] : { max_zzim_mileage: 500, mileage_per_zzim: 1 };
if (myAvail >= levelConf.max_zzim_mileage) {
console.log('[autoMutualZzim] 마일리지 최대치 도달 실행 안 함');
return;
}
// 3. 후보 마켓 조회 (v_user_market_stats 뷰)
const candRes = await fetch(`${SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json' }
});
if (!candRes.ok) {
console.warn('[autoMutualZzim] 후보 조회 실패', candRes.status);
return;
}
const users = await candRes.json();
const pool = [];
users.forEach(u => {
const markets = u.my_markets || [];
markets.forEach(m => {
if (m.is_visible !== false && m.for_mutual_zzim !== false) {
pool.push({ ...m, owner_user_id: u.user_id, owner_available_mileage: u.available_zzim_mile });
}
});
});
if (pool.length === 0) {
console.log('[autoMutualZzim] 후보 마켓 없음');
return;
}
// 4. 랜덤 1개 선택
const target = pool[Math.floor(Math.random() * pool.length)];
const targetUrl = `${target.market_url.replace(/\/$/, '')}/category/ALL?cp=1&auto_zzim=true&max_zzim=50`;
// 5. 백그라운드 찜 실행
const msg = {
action: 'executeBackgroundZzim',
market: { ...target, target_url: targetUrl },
zzimType: 'mutual',
userId: user_id,
accessToken: access_token,
settings: { totalDelay: 1000, latestFirst: false, backgroundMode: true }
};
chrome.runtime.sendMessage(msg, (res) => {
if (chrome.runtime.lastError) {
console.error('[autoMutualZzim] runtime error', chrome.runtime.lastError.message);
} else if (res && res.success) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '🎉 자동 품앗이 완료',
message: `${target.market_nickname || '마켓'}에 찜 50개 완료!`
});
} else {
console.warn('[autoMutualZzim] 실행 실패', res?.error);
}
});
} catch (e) {
console.error('[autoMutualZzim] 오류', e);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1947,7 +1947,7 @@ class ZzimManager {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
let myAvailableMileage = 0; let myAvailableMileage = 0;
if (myStatsResponse.ok) { if (myStatsResponse.ok) {
const myData = await myStatsResponse.json(); const myData = await myStatsResponse.json();