3593 lines
135 KiB
JavaScript
3593 lines
135 KiB
JavaScript
// background.js (Service Worker)
|
||
|
||
chrome.runtime.onInstalled.addListener(() => {
|
||
// 컨텍스트 메뉴를 개별적으로 생성 (단축키 포함)
|
||
chrome.contextMenus.create({
|
||
id: "searchTrademark",
|
||
title: "지재권검색 (Ctrl+Shift+S)",
|
||
contexts: ["selection"]
|
||
});
|
||
|
||
chrome.contextMenus.create({
|
||
id: "multiTranslate",
|
||
title: "멀티번역 (Ctrl+Shift+E)",
|
||
contexts: ["selection"]
|
||
});
|
||
|
||
chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
|
||
|
||
// 새 어록 감지 알람 생성 (5분마다)
|
||
chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 });
|
||
|
||
// 초기 마지막 확인 시간 설정
|
||
chrome.storage.local.set({ lastSayingsCheck: Date.now() });
|
||
});
|
||
|
||
// 단축키 명령어 처리
|
||
chrome.commands.onCommand.addListener(async (command) => {
|
||
console.log(`[background.js] 단축키 명령어 실행: ${command}`);
|
||
|
||
try {
|
||
// 현재 활성 탭 가져오기
|
||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||
if (!tab) {
|
||
console.error('[background.js] 활성 탭을 찾을 수 없습니다');
|
||
return;
|
||
}
|
||
|
||
// 선택된 텍스트 가져오기 (개선된 방식)
|
||
const results = await chrome.scripting.executeScript({
|
||
target: { tabId: tab.id },
|
||
function: () => {
|
||
// 다양한 방식으로 선택된 텍스트 감지
|
||
function getSelectedTextAdvanced() {
|
||
let selectedText = '';
|
||
console.log('[텍스트선택] 선택된 텍스트 감지 시작');
|
||
|
||
// 1. 기본 window.getSelection() 시도
|
||
const selection = window.getSelection();
|
||
if (selection && selection.toString().trim()) {
|
||
selectedText = selection.toString().trim();
|
||
console.log('[텍스트선택] window.getSelection()으로 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
|
||
// 2. document.getSelection() 시도
|
||
if (document.getSelection) {
|
||
const docSelection = document.getSelection();
|
||
if (docSelection && docSelection.toString().trim()) {
|
||
selectedText = docSelection.toString().trim();
|
||
console.log('[텍스트선택] document.getSelection()으로 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
}
|
||
|
||
// 3. activeElement에서 선택된 텍스트 확인 (input, textarea)
|
||
const activeElement = document.activeElement;
|
||
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
|
||
const start = activeElement.selectionStart;
|
||
const end = activeElement.selectionEnd;
|
||
if (start !== end && start !== null && end !== null) {
|
||
selectedText = activeElement.value.substring(start, end).trim();
|
||
console.log('[텍스트선택] activeElement에서 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
}
|
||
|
||
// 4. contentEditable 요소에서 선택된 텍스트 확인
|
||
const editableElements = document.querySelectorAll('[contenteditable="true"]');
|
||
for (const element of editableElements) {
|
||
const elementSelection = element.ownerDocument.getSelection();
|
||
if (elementSelection && elementSelection.toString().trim()) {
|
||
selectedText = elementSelection.toString().trim();
|
||
console.log('[텍스트선택] contentEditable에서 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
}
|
||
|
||
// 5. iframe 내부 확인
|
||
const iframes = document.querySelectorAll('iframe');
|
||
for (const iframe of iframes) {
|
||
try {
|
||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||
const iframeSelection = iframeDoc.getSelection();
|
||
if (iframeSelection && iframeSelection.toString().trim()) {
|
||
selectedText = iframeSelection.toString().trim();
|
||
console.log('[텍스트선택] iframe에서 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
} catch (e) {
|
||
// 크로스 오리진 iframe은 접근 불가
|
||
console.log('[텍스트선택] iframe 접근 제한:', e.message);
|
||
}
|
||
}
|
||
|
||
// 6. Shadow DOM 확인
|
||
function checkShadowDom(element) {
|
||
if (element.shadowRoot) {
|
||
const shadowSelection = element.shadowRoot.getSelection();
|
||
if (shadowSelection && shadowSelection.toString().trim()) {
|
||
return shadowSelection.toString().trim();
|
||
}
|
||
}
|
||
|
||
for (const child of element.children) {
|
||
const result = checkShadowDom(child);
|
||
if (result) return result;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const shadowResult = checkShadowDom(document.body);
|
||
if (shadowResult) {
|
||
console.log('[텍스트선택] Shadow DOM에서 감지:', shadowResult);
|
||
return shadowResult;
|
||
}
|
||
|
||
// 7. 특정 컨테이너 내부 확인 (ice-container 등)
|
||
const containers = document.querySelectorAll('.ice-container, [class*="container"], [class*="chat"], [class*="message"], [class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], [data-testid*="message"], [role="textbox"], [contenteditable]');
|
||
for (const container of containers) {
|
||
try {
|
||
const containerSelection = container.ownerDocument.getSelection();
|
||
if (containerSelection && containerSelection.toString().trim()) {
|
||
// 선택 범위가 해당 컨테이너 내부인지 확인
|
||
const range = containerSelection.getRangeAt(0);
|
||
if (range && (container.contains(range.commonAncestorContainer) ||
|
||
container.contains(range.startContainer) ||
|
||
container.contains(range.endContainer))) {
|
||
selectedText = containerSelection.toString().trim();
|
||
console.log('[텍스트선택] 특정 컨테이너에서 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('[텍스트선택] 컨테이너 확인 오류:', e.message);
|
||
}
|
||
}
|
||
|
||
// 8. 모든 요소에서 선택 상태 확인 (최후의 수단)
|
||
const allElements = document.querySelectorAll('*');
|
||
for (const element of allElements) {
|
||
try {
|
||
// 요소가 포커스되어 있고 선택된 텍스트가 있는지 확인
|
||
if (element === document.activeElement || element.contains(document.activeElement)) {
|
||
const computedStyle = window.getComputedStyle(element);
|
||
if (computedStyle.userSelect !== 'none') {
|
||
const elementText = element.textContent || element.innerText;
|
||
if (elementText && elementText.trim()) {
|
||
// 마우스로 선택된 영역이 이 요소 내부에 있는지 확인
|
||
const selection = window.getSelection();
|
||
if (selection && selection.rangeCount > 0) {
|
||
const range = selection.getRangeAt(0);
|
||
if (element.contains(range.commonAncestorContainer)) {
|
||
selectedText = selection.toString().trim();
|
||
if (selectedText) {
|
||
console.log('[텍스트선택] 활성 요소에서 감지:', selectedText);
|
||
return selectedText;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 무시
|
||
}
|
||
}
|
||
|
||
// 9. 클립보드에서 최근 복사된 텍스트 확인 (사용자가 Ctrl+C를 눌렀을 가능성)
|
||
try {
|
||
// 참고: 보안상의 이유로 클립보드 읽기는 제한적임
|
||
console.log('[텍스트선택] 클립보드 확인은 보안 제약으로 제한됨');
|
||
} catch (e) {
|
||
console.log('[텍스트선택] 클립보드 접근 불가:', e.message);
|
||
}
|
||
|
||
console.log('[텍스트선택] 선택된 텍스트를 찾을 수 없음');
|
||
return '';
|
||
}
|
||
|
||
return getSelectedTextAdvanced();
|
||
}
|
||
});
|
||
|
||
const selectedText = results[0]?.result;
|
||
if (!selectedText) {
|
||
// 클립보드에서 텍스트 가져오기 시도
|
||
try {
|
||
const clipboardText = await navigator.clipboard.readText();
|
||
if (clipboardText && clipboardText.trim() && clipboardText.length < 1000) {
|
||
console.log('[단축키] 클립보드에서 텍스트 사용:', clipboardText.substring(0, 50));
|
||
|
||
// 클립보드 텍스트로 처리 계속
|
||
await ensureContentScriptReady(tab.id);
|
||
|
||
if (command === 'trademark-search') {
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, {
|
||
action: "showLoading",
|
||
message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 지재권 검색 중... (클립보드 사용)`
|
||
});
|
||
} catch (loadingError) {
|
||
console.log('[단축키-지재권검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
|
||
}
|
||
|
||
await handleTrademarkSearch(clipboardText.trim(), tab);
|
||
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
|
||
} catch (e) {
|
||
console.log('[단축키-지재권검색] 로딩 인디케이터 제거 실패 (무시):', e.message);
|
||
}
|
||
|
||
} else if (command === 'multi-translate') {
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, {
|
||
action: "showLoading",
|
||
message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 번역 중... (클립보드 사용)`
|
||
});
|
||
} catch (loadingError) {
|
||
console.log('[단축키-멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
|
||
}
|
||
|
||
await handleMultiTranslate({ selectionText: clipboardText.trim() });
|
||
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
|
||
} catch (e) {
|
||
console.log('[단축키-멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message);
|
||
}
|
||
|
||
} else if (command === 'korean-to-chinese') {
|
||
await handleKoreanToChinese(clipboardText.trim(), tab);
|
||
} else if (command === 'direct-translate') {
|
||
await handleDirectTranslation(clipboardText.trim(), tab);
|
||
}
|
||
|
||
return; // 클립보드 처리 완료
|
||
}
|
||
} catch (clipboardError) {
|
||
console.log('[단축키] 클립보드 읽기 실패:', clipboardError.message);
|
||
}
|
||
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '텍스트 선택 필요',
|
||
message: 'AliWangWang 등의 앱에서는 텍스트를 정확히 드래그 선택 후 단축키를 눌러주세요. 선택이 안 되면 Ctrl+C로 복사 후 다시 시도해보세요.'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Content script 준비 확인
|
||
await ensureContentScriptReady(tab.id);
|
||
|
||
// 명령어에 따른 처리
|
||
if (command === 'trademark-search') {
|
||
// 로딩 인디케이터 표시
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, {
|
||
action: "showLoading",
|
||
message: `🔄 "${selectedText.substring(0, 20)}${selectedText.length > 20 ? '...' : ''}" 지재권 검색 중...`
|
||
});
|
||
} catch (loadingError) {
|
||
console.log('[단축키-지재권검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
|
||
}
|
||
|
||
await handleTrademarkSearch(selectedText, tab);
|
||
|
||
// 로딩 인디케이터 제거
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
|
||
} catch (e) {
|
||
console.log('[단축키-지재권검색] 로딩 인디케이터 제거 실패 (무시):', e.message);
|
||
}
|
||
|
||
} else if (command === 'multi-translate') {
|
||
// 로딩 인디케이터 표시
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, {
|
||
action: "showLoading",
|
||
message: `🔄 "${selectedText.substring(0, 20)}${selectedText.length > 20 ? '...' : ''}" 번역 중...`
|
||
});
|
||
} catch (loadingError) {
|
||
console.log('[단축키-멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
|
||
}
|
||
|
||
await handleMultiTranslate({ selectionText: selectedText });
|
||
|
||
// 로딩 인디케이터 제거
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
|
||
} catch (e) {
|
||
console.log('[단축키-멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message);
|
||
}
|
||
|
||
} else if (command === 'korean-to-chinese') {
|
||
await handleKoreanToChinese(selectedText, tab);
|
||
} else if (command === 'direct-translate') {
|
||
await handleDirectTranslation(selectedText, tab);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`[background.js] 단축키 처리 중 오류:`, error);
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '오류 발생',
|
||
message: '단축키 처리 중 문제가 발생했습니다.'
|
||
});
|
||
}
|
||
});
|
||
|
||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||
if (alarm.name === "keepAlive") {
|
||
console.log("[background.js] 서비스 워커 유지 알람 실행됨");
|
||
} else if (alarm.name === "checkNewSayings") {
|
||
checkForNewSayings();
|
||
}
|
||
});
|
||
|
||
// 새 어록 확인 함수
|
||
async function checkForNewSayings() {
|
||
try {
|
||
console.log("[background.js] 새 어록 확인 시작");
|
||
|
||
// Chrome 확장 프로그램 컨텍스트 확인
|
||
if (!chrome || !chrome.storage || !chrome.storage.local) {
|
||
console.error("[background.js] Chrome 확장 프로그램 API에 접근할 수 없습니다.");
|
||
return;
|
||
}
|
||
|
||
// 마지막 확인 시간 가져오기 (안전한 방식)
|
||
let lastSayingsCheck;
|
||
try {
|
||
const result = await chrome.storage.local.get("lastSayingsCheck");
|
||
lastSayingsCheck = result.lastSayingsCheck;
|
||
} catch (storageError) {
|
||
console.error("[background.js] 스토리지 접근 오류:", storageError);
|
||
lastSayingsCheck = Date.now() - 300000; // 기본값: 1분 전
|
||
}
|
||
|
||
const lastCheckTime = lastSayingsCheck || Date.now() - 300000;
|
||
|
||
// 액세스 토큰 가져오기 (안전한 방식)
|
||
let access_token;
|
||
try {
|
||
const result = await chrome.storage.local.get("access_token");
|
||
access_token = result.access_token;
|
||
} catch (storageError) {
|
||
console.error("[background.js] 토큰 스토리지 접근 오류:", storageError);
|
||
return;
|
||
}
|
||
|
||
if (!access_token) {
|
||
console.log("[background.js] 액세스 토큰이 없어 새 어록 확인을 건너뜁니다.");
|
||
return;
|
||
}
|
||
|
||
// 올바른 Supabase URL과 헤더 사용
|
||
const SUPABASE_URL = "https://ko.wrmc.cc";
|
||
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
|
||
|
||
// 새 어록 API 호출
|
||
const apiUrl = `${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${new Date(lastCheckTime).toISOString()}&admin_approval=eq.true&order=created_at.desc`;
|
||
|
||
console.log("[background.js] API 호출:", apiUrl);
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'apikey': SUPABASE_ANON_KEY,
|
||
'Authorization': `Bearer ${access_token}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const newSayings = await response.json();
|
||
|
||
if (newSayings && newSayings.length > 0) {
|
||
console.log(`[background.js] ${newSayings.length}개의 새 어록을 발견했습니다.`);
|
||
|
||
// 브라우저 알림 표시 (안전한 방식)
|
||
try {
|
||
if (chrome.notifications) {
|
||
chrome.notifications.create('newSayings', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '새 어록 알림',
|
||
message: `${newSayings.length}개의 새로운 어록이 등록되었습니다.`,
|
||
buttons: [
|
||
{ title: '확인하기' },
|
||
{ title: '나중에' }
|
||
]
|
||
});
|
||
}
|
||
} catch (notificationError) {
|
||
console.error("[background.js] 알림 생성 오류:", notificationError);
|
||
}
|
||
|
||
// 새 어록 데이터를 스토리지에 저장 (안전한 방식)
|
||
try {
|
||
await chrome.storage.local.set({
|
||
pendingNewSayings: newSayings,
|
||
hasNewSayings: true
|
||
});
|
||
console.log("[background.js] 새 어록 데이터 스토리지 저장 완료");
|
||
} catch (storageError) {
|
||
console.error("[background.js] 새 어록 데이터 저장 오류:", storageError);
|
||
}
|
||
} else {
|
||
console.log("[background.js] 새 어록이 없습니다.");
|
||
}
|
||
|
||
// 마지막 확인 시간 업데이트 (안전한 방식)
|
||
try {
|
||
await chrome.storage.local.set({ lastSayingsCheck: Date.now() });
|
||
console.log("[background.js] 마지막 확인 시간 업데이트 완료");
|
||
} catch (storageError) {
|
||
console.error("[background.js] 마지막 확인 시간 업데이트 오류:", storageError);
|
||
}
|
||
|
||
} else {
|
||
const errorText = await response.text();
|
||
console.error("[background.js] 새 어록 확인 실패:", response.status, response.statusText, errorText);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error("[background.js] 새 어록 확인 중 전체 오류:", error);
|
||
|
||
// 오류 유형별 처리
|
||
if (error.message.includes('Could not establish connection')) {
|
||
console.error("[background.js] Chrome 확장 프로그램 연결 오류 - 확장 프로그램을 다시 로드하세요.");
|
||
} else if (error.message.includes('Receiving end does not exist')) {
|
||
console.error("[background.js] 메시지 수신자가 존재하지 않음 - 페이지를 새로고침하세요.");
|
||
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||
console.error("[background.js] 네트워크 연결 오류 - 서버 연결을 확인하세요.");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 알림 클릭 처리
|
||
chrome.notifications.onClicked.addListener((notificationId) => {
|
||
if (notificationId === 'newSayings') {
|
||
// 어록 관리 페이지 열기
|
||
chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') });
|
||
chrome.notifications.clear(notificationId);
|
||
} else if (notificationId === 'multiTranslateLogin') {
|
||
// 로그인 필요 알림 클릭 시 팝업 열기
|
||
chrome.action.openPopup();
|
||
chrome.notifications.clear(notificationId);
|
||
} else if (notificationId === 'multiTranslateLimit') {
|
||
// API 한도 초과 알림 클릭 시 팝업 열기
|
||
chrome.action.openPopup();
|
||
chrome.notifications.clear(notificationId);
|
||
} else if (notificationId === 'multiTranslateUserInfo') {
|
||
// 사용자 정보 오류 알림 클릭 시 팝업 열기
|
||
chrome.action.openPopup();
|
||
chrome.notifications.clear(notificationId);
|
||
} else if (notificationId === 'multiTranslateError') {
|
||
// 번역 오류 알림 클릭 시 알림만 제거
|
||
chrome.notifications.clear(notificationId);
|
||
} else if (notificationId === 'multiTranslateBasic') {
|
||
// 기본 회원 안내 알림 클릭 시 알림만 제거
|
||
chrome.notifications.clear(notificationId);
|
||
} else if (notificationId === 'multiTranslateException') {
|
||
// 예외 오류 알림 클릭 시 알림만 제거
|
||
chrome.notifications.clear(notificationId);
|
||
}
|
||
});
|
||
|
||
// 알림 버튼 클릭 처리
|
||
chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
|
||
if (notificationId === 'newSayings') {
|
||
if (buttonIndex === 0) { // 확인하기
|
||
chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') });
|
||
}
|
||
chrome.notifications.clear(notificationId);
|
||
}
|
||
});
|
||
|
||
// 컨텍스트 메뉴 클릭 시 처리
|
||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||
const keyword = info.selectionText.trim();
|
||
if (!keyword) return;
|
||
|
||
// 지재권 검색 처리
|
||
if (info.menuItemId === "searchTrademark") {
|
||
try {
|
||
// Content script 준비 확인
|
||
await ensureContentScriptReady(tab.id);
|
||
|
||
// 로딩 인디케이터 표시
|
||
await chrome.tabs.sendMessage(tab.id, {
|
||
action: "showLoading",
|
||
message: `🔄 "${keyword.substring(0, 20)}${keyword.length > 20 ? '...' : ''}" 지재권 검색 중...`
|
||
});
|
||
} catch (loadingError) {
|
||
console.log('[지재권 검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
|
||
}
|
||
|
||
await handleTrademarkSearch(keyword, tab);
|
||
|
||
// 로딩 인디케이터 제거
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
|
||
} catch (e) {
|
||
console.log('[지재권 검색] 로딩 인디케이터 제거 실패 (무시):', e.message);
|
||
}
|
||
}
|
||
|
||
// 멀티번역 처리
|
||
if (info.menuItemId === "multiTranslate") {
|
||
try {
|
||
// Content script 준비 확인
|
||
await ensureContentScriptReady(tab.id);
|
||
|
||
// 로딩 인디케이터 표시
|
||
await chrome.tabs.sendMessage(tab.id, {
|
||
action: "showLoading",
|
||
message: `🔄 "${keyword.substring(0, 20)}${keyword.length > 20 ? '...' : ''}" 번역 중...`
|
||
});
|
||
} catch (loadingError) {
|
||
console.log('[멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
|
||
}
|
||
|
||
await handleMultiTranslate(info);
|
||
|
||
// 로딩 인디케이터 제거
|
||
try {
|
||
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
|
||
} catch (e) {
|
||
console.log('[멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 기존 지재권 검색 함수로 분리
|
||
async function handleTrademarkSearch(keyword, tab) {
|
||
try {
|
||
// 1. 토큰 확인
|
||
const { access_token } = await chrome.storage.local.get("access_token");
|
||
if (!access_token) {
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "로그인 필요",
|
||
message: "지재권 검색을 사용하려면 먼저 로그인하세요."
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 2. API 호출량 증가 및 한도 확인
|
||
console.log('[지재권 검색] API 호출량 확인 시작');
|
||
const apiCallResult = await incrementApiCallsAndCheckLimit();
|
||
|
||
if (!apiCallResult.success) {
|
||
console.error('[지재권 검색] API 호출량 한도 초과:', apiCallResult.error);
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "API 호출 한도 초과",
|
||
message: apiCallResult.error
|
||
});
|
||
return;
|
||
}
|
||
|
||
console.log('[지재권 검색] API 호출량 확인 완료:', {
|
||
current: apiCallResult.current,
|
||
limit: apiCallResult.limit,
|
||
remaining: apiCallResult.remaining
|
||
});
|
||
|
||
// 3. 사용자 정보 및 회원등급 확인
|
||
const SUPABASE_URL = "https://ko.wrmc.cc";
|
||
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
|
||
|
||
// 사용자 기본 정보 가져오기 (토큰 검증)
|
||
const authUrl = `${SUPABASE_URL}/auth/v1/user`;
|
||
const authRes = await fetch(authUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${access_token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!authRes.ok) {
|
||
const errorText = await authRes.text();
|
||
console.error("[지재권 검색] 토큰 검증 실패:", authRes.status, errorText);
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "인증 오류",
|
||
message: "세션이 만료되었습니다. 다시 로그인해주세요."
|
||
});
|
||
return;
|
||
}
|
||
|
||
const authUser = await authRes.json();
|
||
console.log("[지재권 검색] 사용자 인증 성공:", authUser.email);
|
||
|
||
// 사용자 상세 정보 및 회원등급 확인 (users 테이블에서 직접 조회)
|
||
const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
|
||
const detailsRes = await fetch(detailsUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${access_token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!detailsRes.ok) {
|
||
const errorText = await detailsRes.text();
|
||
console.error("[지재권 검색] 사용자 정보 조회 실패:", detailsRes.status, errorText);
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "사용자 정보 오류",
|
||
message: "사용자 정보를 가져올 수 없습니다."
|
||
});
|
||
return;
|
||
}
|
||
|
||
const detailsData = await detailsRes.json();
|
||
const userDetails = detailsData[0];
|
||
|
||
if (!userDetails) {
|
||
console.error("[지재권 검색] 사용자 정보 없음");
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "사용자 정보 없음",
|
||
message: "사용자 정보를 찾을 수 없습니다."
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 4. 회원등급 확인 (premium, vip만 허용)
|
||
const membershipLevel = userDetails.membership_level;
|
||
console.log("[지재권 검색] 사용자 회원등급:", membershipLevel);
|
||
|
||
// premium 또는 vip가 아닌 경우 접근 거부
|
||
if (!membershipLevel || (membershipLevel !== 'premium' && membershipLevel !== 'vip')) {
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "권한 부족",
|
||
message: `지재권 검색은 프리미엄/VIP 회원만 사용할 수 있습니다.\n현재 등급: ${membershipLevel || '기본'}`
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 5. 권한이 있는 경우 지재권 검색 실행
|
||
console.log(`[지재권 검색] 사용자: ${authUser.email}, 등급: ${membershipLevel}, 키워드: ${keyword}, API 호출: ${apiCallResult.current}/${apiCallResult.limit}`);
|
||
|
||
const url = buildMarkInfoUrl(keyword);
|
||
|
||
// 키워드 검색 실행
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error(`네트워크 오류: ${response.status}`);
|
||
}
|
||
|
||
const html = await response.text();
|
||
const match = /<script[^>]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/i.exec(html);
|
||
if (!match) {
|
||
throw new Error("__NUXT_DATA__ 태그를 찾을 수 없습니다.");
|
||
}
|
||
|
||
let jsonString = match[1];
|
||
let globalData;
|
||
try {
|
||
globalData = JSON.parse(jsonString);
|
||
} catch (e) {
|
||
throw new Error("JSON 파싱 실패: " + e.toString());
|
||
}
|
||
|
||
// 키워드 검색 결과 파싱
|
||
const keywordResults = parseSearchResults(globalData);
|
||
if (!Array.isArray(keywordResults) || keywordResults.length === 0) {
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "검색 결과 없음",
|
||
message: `'${keyword}'에 대한 지재권 검색 결과가 없습니다.`
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 최대 10건까지만 처리
|
||
const limitedResults = keywordResults.slice(0, 10);
|
||
|
||
// 각 결과의 출원번호로 상세 조회
|
||
const detailPromises = limitedResults.map(result => {
|
||
const appNum = result.registration_info.applicationNum;
|
||
return fetchDetailInfo(appNum)
|
||
.then(detail => {
|
||
result.detail = detail;
|
||
return result;
|
||
})
|
||
.catch(err => {
|
||
result.detailError = err.toString();
|
||
return result;
|
||
});
|
||
});
|
||
|
||
const allResults = await Promise.all(detailPromises);
|
||
|
||
// 결과를 컨텐츠 스크립트로 전송
|
||
try {
|
||
console.log(`[지재권 검색] 탭 ${tab.id}로 결과 전송 시도, 결과 개수: ${allResults.length}`);
|
||
|
||
// 탭이 여전히 유효한지 확인
|
||
const tabInfo = await chrome.tabs.get(tab.id);
|
||
if (!tabInfo || tabInfo.status !== 'complete') {
|
||
throw new Error('탭이 준비되지 않았습니다');
|
||
}
|
||
|
||
// 콘텐츠 스크립트 상태 확인을 위한 핑 테스트
|
||
let contentScriptReady = false;
|
||
try {
|
||
console.log('[지재권 검색] 콘텐츠 스크립트 상태 확인 중...');
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error('핑 테스트 타임아웃'));
|
||
}, 2000);
|
||
|
||
chrome.tabs.sendMessage(tab.id, { action: "ping" }, (response) => {
|
||
clearTimeout(timeout);
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
} else {
|
||
console.log('[지재권 검색] 콘텐츠 스크립트 준비 완료');
|
||
contentScriptReady = true;
|
||
resolve(response);
|
||
}
|
||
});
|
||
});
|
||
} catch (pingError) {
|
||
console.log('[지재권 검색] 콘텐츠 스크립트 미준비, 동적 주입 필요:', pingError.message);
|
||
contentScriptReady = false;
|
||
}
|
||
|
||
// 콘텐츠 스크립트가 준비되지 않은 경우 미리 주입
|
||
if (!contentScriptReady) {
|
||
try {
|
||
console.log('[지재권 검색] 콘텐츠 스크립트 사전 주입 시작');
|
||
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId: tab.id },
|
||
files: ['content.js']
|
||
});
|
||
|
||
console.log('[지재권 검색] 콘텐츠 스크립트 사전 주입 완료');
|
||
|
||
// 주입 후 대기
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
} catch (preInjectionError) {
|
||
console.error('[지재권 검색] 사전 주입 실패:', preInjectionError);
|
||
}
|
||
}
|
||
|
||
// 메시지 전송 시도
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error('메시지 전송 타임아웃'));
|
||
}, 5000);
|
||
|
||
chrome.tabs.sendMessage(tab.id, {
|
||
action: "showTooltip",
|
||
detailInfo: allResults,
|
||
keyword: keyword
|
||
}, (response) => {
|
||
clearTimeout(timeout);
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
} else {
|
||
console.log('[지재권 검색] 메시지 전송 성공, 응답:', response);
|
||
resolve(response);
|
||
}
|
||
});
|
||
});
|
||
|
||
console.log('[지재권 검색] 결과 전송 완료');
|
||
|
||
} catch (contentError) {
|
||
console.error('[지재권 검색] 컨텐츠 스크립트 오류:', contentError);
|
||
|
||
// 컨텐츠 스크립트 오류 시 알림으로 대체
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "지재권 검색 결과",
|
||
message: `'${keyword}' 검색 완료 (결과: ${allResults.length}건)\n상세 정보는 브라우저 콘솔을 확인하세요.`
|
||
});
|
||
|
||
console.log('[지재권 검색] 상세 결과:', allResults);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[지재권 검색] 오류:', error);
|
||
chrome.notifications.create({
|
||
type: "basic",
|
||
iconUrl: "icon.png",
|
||
title: "지재권 검색 오류",
|
||
message: error.message || "검색 중 오류가 발생했습니다."
|
||
});
|
||
}
|
||
}
|
||
|
||
// 멀티번역 처리 함수
|
||
async function handleMultiTranslate(info) {
|
||
const selectedText = info.selectionText?.trim();
|
||
if (!selectedText) return;
|
||
|
||
console.log('[background.js] 멀티번역 요청:', selectedText);
|
||
|
||
try {
|
||
// 토큰 검증
|
||
const token = await getStoredToken();
|
||
if (!token) {
|
||
console.log('[background.js] 토큰 없음 - 로그인 필요 알림 표시');
|
||
chrome.notifications.create('multiTranslateLogin', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '멀티번역',
|
||
message: '로그인이 필요합니다. 확장 프로그램을 클릭하여 로그인해 주세요.'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// API 호출량 증가 및 한도 확인
|
||
console.log('[멀티번역] API 호출량 확인 시작');
|
||
const apiCallResult = await incrementApiCallsAndCheckLimit();
|
||
|
||
if (!apiCallResult.success) {
|
||
console.error('[멀티번역] API 호출량 한도 초과:', apiCallResult.error);
|
||
chrome.notifications.create('multiTranslateLimit', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: 'API 호출 한도 초과',
|
||
message: apiCallResult.error
|
||
});
|
||
return;
|
||
}
|
||
|
||
console.log('[멀티번역] API 호출량 확인 완료:', {
|
||
current: apiCallResult.current,
|
||
limit: apiCallResult.limit,
|
||
remaining: apiCallResult.remaining
|
||
});
|
||
|
||
// 사용자 정보 가져오기
|
||
const userInfo = await fetchUserInfo(token);
|
||
if (!userInfo) {
|
||
console.log('[background.js] 사용자 정보 없음 - 재로그인 필요 알림 표시');
|
||
chrome.notifications.create('multiTranslateUserInfo', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '멀티번역',
|
||
message: '사용자 정보를 가져올 수 없습니다. 다시 로그인해 주세요.'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const userLevel = userInfo.membership_level || 'basic';
|
||
console.log(`[background.js] 사용자 레벨: ${userLevel}, API 호출: ${apiCallResult.current}/${apiCallResult.limit}`);
|
||
|
||
// 회원등급별 접근 권한 및 안내 메시지
|
||
let accessMessage = '';
|
||
switch (userLevel.toLowerCase()) {
|
||
case 'basic':
|
||
accessMessage = '기본 회원: Google, MyMemory 번역 엔진 사용 가능';
|
||
break;
|
||
case 'premium':
|
||
accessMessage = '프리미엄 회원: Google, MyMemory, DeepL 번역 엔진 사용 가능';
|
||
break;
|
||
case 'vip':
|
||
accessMessage = 'VIP 회원: 모든 번역 엔진 사용 가능 (ChatGPT, Gemini 포함)';
|
||
break;
|
||
default:
|
||
accessMessage = '기본 회원: 제한된 번역 엔진 사용 가능';
|
||
}
|
||
|
||
// 회원등급별 사용 가능한 번역 엔진 결정
|
||
const availableEngines = await getAvailableEngines(userLevel);
|
||
console.log(`[background.js] 사용 가능한 번역 엔진:`, availableEngines);
|
||
|
||
// 번역 실행
|
||
const translationResults = await performTranslations(selectedText, availableEngines);
|
||
|
||
// 결과를 content script로 전송
|
||
const tabs = await chrome.tabs.query({active: true, currentWindow: true});
|
||
if (tabs[0]) {
|
||
await ensureContentScriptReady(tabs[0].id);
|
||
|
||
chrome.tabs.sendMessage(tabs[0].id, {
|
||
action: "showTranslationTooltip",
|
||
originalText: selectedText,
|
||
results: translationResults,
|
||
userLevel: userLevel,
|
||
accessMessage: accessMessage
|
||
}, (response) => {
|
||
if (chrome.runtime.lastError) {
|
||
console.error('[background.js] 번역 결과 전송 실패:', chrome.runtime.lastError);
|
||
chrome.notifications.create('multiTranslateError', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '멀티번역',
|
||
message: '번역 결과를 표시할 수 없습니다.'
|
||
});
|
||
} else {
|
||
console.log('[background.js] 번역 결과 전송 성공');
|
||
|
||
// 기본 회원인 경우 추가 안내 알림
|
||
if (userLevel.toLowerCase() === 'basic') {
|
||
chrome.notifications.create('multiTranslateBasic', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '멀티번역 완료',
|
||
message: `${accessMessage}\n프리미엄 업그레이드 시 더 많은 번역 엔진을 이용하실 수 있습니다.`
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] 멀티번역 처리 중 오류:', error);
|
||
chrome.notifications.create('multiTranslateException', {
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '멀티번역 오류',
|
||
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
|
||
});
|
||
}
|
||
}
|
||
|
||
// Content Script가 준비되었는지 확인하고 필요시 주입
|
||
async function ensureContentScriptReady(tabId) {
|
||
try {
|
||
// 핑 테스트로 content script 상태 확인
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error('핑 테스트 타임아웃'));
|
||
}, 1000);
|
||
|
||
chrome.tabs.sendMessage(tabId, { action: "ping" }, (response) => {
|
||
clearTimeout(timeout);
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
} else {
|
||
console.log('[background.js] Content script 준비 완료');
|
||
resolve(response);
|
||
}
|
||
});
|
||
});
|
||
} catch (pingError) {
|
||
console.log('[background.js] Content script 미준비, 주입 시도:', pingError.message);
|
||
|
||
try {
|
||
// Content script 주입
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId: tabId },
|
||
files: ['content.js']
|
||
});
|
||
|
||
console.log('[background.js] Content script 주입 완료');
|
||
|
||
// 주입 후 잠시 대기
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
} catch (injectionError) {
|
||
console.error('[background.js] Content script 주입 실패:', injectionError);
|
||
throw injectionError;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 회원등급별 사용 가능한 번역 엔진 반환 (설정 고려)
|
||
async function getAvailableEngines(userLevel) {
|
||
const enginesByLevel = {
|
||
'basic': ['google', 'mymemory'],
|
||
'premium': ['google', 'mymemory', 'deepl'],
|
||
'vip': ['google', 'mymemory', 'deepl', 'openai'] // gemini 제거
|
||
};
|
||
|
||
// 소문자로 변환하여 비교
|
||
const normalizedLevel = (userLevel || 'basic').toLowerCase();
|
||
const levelEngines = enginesByLevel[normalizedLevel] || enginesByLevel.basic;
|
||
|
||
console.log(`[background.js] 회원등급 정규화: ${userLevel} -> ${normalizedLevel}`);
|
||
console.log(`[background.js] 등급별 사용 가능한 엔진:`, levelEngines);
|
||
|
||
// 사용자 설정 확인
|
||
try {
|
||
const result = await chrome.storage.local.get('translation_engine_settings');
|
||
const userSettings = result.translation_engine_settings || {};
|
||
|
||
console.log('[background.js] 사용자 번역 엔진 설정:', userSettings);
|
||
|
||
// 등급별 사용 가능한 엔진 중에서 사용자가 활성화한 엔진만 필터링
|
||
const enabledEngines = levelEngines.filter(engine => {
|
||
const isEnabled = userSettings[engine] !== false; // 기본값은 true
|
||
console.log(`[background.js] ${engine} 엔진 활성화 상태:`, isEnabled);
|
||
return isEnabled;
|
||
});
|
||
|
||
console.log(`[background.js] 최종 사용 가능한 엔진:`, enabledEngines);
|
||
|
||
// 최소 1개 엔진은 활성화되어야 함
|
||
if (enabledEngines.length === 0) {
|
||
console.warn('[background.js] 활성화된 번역 엔진이 없어 기본 엔진(Google) 사용');
|
||
return ['google'];
|
||
}
|
||
|
||
return enabledEngines;
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] 번역 엔진 설정 로드 실패:', error);
|
||
return levelEngines; // 오류 시 등급별 기본 엔진 사용
|
||
}
|
||
}
|
||
|
||
// 여러 번역 엔진으로 번역 실행
|
||
async function performTranslations(text, engines) {
|
||
const results = [];
|
||
|
||
// 각 엔진별로 병렬 번역 실행
|
||
const translatePromises = engines.map(async (engine) => {
|
||
try {
|
||
const result = await translateWithEngine(text, engine);
|
||
return {
|
||
engine: engine,
|
||
success: true,
|
||
translatedText: result
|
||
};
|
||
} catch (error) {
|
||
console.error(`[background.js] ${engine} 번역 실패:`, error);
|
||
return {
|
||
engine: engine,
|
||
success: false,
|
||
error: error.message || '번역 실패'
|
||
};
|
||
}
|
||
});
|
||
|
||
const translationResults = await Promise.all(translatePromises);
|
||
return translationResults;
|
||
}
|
||
|
||
// 개별 번역 엔진별 번역 함수
|
||
async function translateWithEngine(text, engine) {
|
||
switch (engine) {
|
||
case 'google':
|
||
return await translateWithGoogle(text);
|
||
case 'mymemory':
|
||
return await translateWithMyMemory(text);
|
||
case 'deepl':
|
||
return await translateWithDeepL(text);
|
||
case 'openai':
|
||
return await translateWithOpenAI(text);
|
||
case 'gemini':
|
||
return await translateWithGemini(text);
|
||
default:
|
||
throw new Error(`지원하지 않는 번역 엔진: ${engine}`);
|
||
}
|
||
}
|
||
|
||
// Google 번역
|
||
async function translateWithGoogle(text) {
|
||
const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=ko&dt=t&q=${encodeURIComponent(text)}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Google 번역 요청 실패');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data || !data[0] || !data[0][0] || !data[0][0][0]) {
|
||
throw new Error('Google 번역 응답 형식 오류');
|
||
}
|
||
|
||
return data[0][0][0];
|
||
}
|
||
|
||
// MyMemory 번역 (무료)
|
||
async function translateWithMyMemory(text) {
|
||
// 언어 감지를 위한 개선된 로직
|
||
const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(text);
|
||
const isChinese = /[\u4e00-\u9fff]/.test(text);
|
||
const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(text.trim());
|
||
|
||
let sourceLang, targetLang;
|
||
|
||
if (isKorean) {
|
||
sourceLang = 'ko';
|
||
targetLang = 'zh'; // 한국어 -> 중국어
|
||
} else if (isChinese) {
|
||
sourceLang = 'zh';
|
||
targetLang = 'ko'; // 중국어 -> 한국어
|
||
} else if (isEnglish) {
|
||
sourceLang = 'en';
|
||
targetLang = 'ko'; // 영어 -> 한국어
|
||
} else {
|
||
// 기본값: 영어 -> 한국어
|
||
sourceLang = 'en';
|
||
targetLang = 'ko';
|
||
}
|
||
|
||
const response = await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('MyMemory 번역 요청 실패');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data || !data.responseData || !data.responseData.translatedText) {
|
||
throw new Error('MyMemory 번역 응답 형식 오류');
|
||
}
|
||
|
||
return data.responseData.translatedText;
|
||
}
|
||
|
||
// DeepL 번역
|
||
async function translateWithDeepL(text) {
|
||
console.log('[background.js] DeepL 번역 시작:', { textLength: text.length });
|
||
|
||
const apiKey = await getApiKey('deepl');
|
||
if (!apiKey || !apiKey.authKey) {
|
||
console.error('[background.js] DeepL API 키가 설정되지 않았습니다:', apiKey);
|
||
throw new Error('DeepL API 키가 설정되지 않았습니다');
|
||
}
|
||
|
||
console.log('[background.js] DeepL API 키 확인 완료');
|
||
|
||
try {
|
||
// DeepL API 키 형식에 따라 엔드포인트 결정
|
||
const isFreeKey = apiKey.authKey.endsWith(':fx');
|
||
const apiUrl = isFreeKey
|
||
? 'https://api-free.deepl.com/v2/translate'
|
||
: 'https://api.deepl.com/v2/translate';
|
||
|
||
console.log(`[background.js] DeepL API 엔드포인트: ${apiUrl} (Free Key: ${isFreeKey})`);
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `DeepL-Auth-Key ${apiKey.authKey}`,
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: `text=${encodeURIComponent(text)}&target_lang=KO`
|
||
});
|
||
|
||
console.log('[background.js] DeepL API 응답 상태:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
ok: response.ok
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('[background.js] DeepL API 에러 응답:', errorText);
|
||
throw new Error(`DeepL 번역 요청 실패 (${response.status}): ${errorText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('[background.js] DeepL API 응답 데이터:', data);
|
||
|
||
if (!data || !data.translations || !data.translations[0] || !data.translations[0].text) {
|
||
console.error('[background.js] DeepL 응답 형식 오류:', data);
|
||
throw new Error('DeepL 번역 응답 형식 오류');
|
||
}
|
||
|
||
console.log('[background.js] DeepL 번역 성공');
|
||
return data.translations[0].text;
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] DeepL 번역 중 오류:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// OpenAI (ChatGPT) 번역
|
||
async function translateWithOpenAI(text) {
|
||
const apiKey = await getApiKey('openai');
|
||
if (!apiKey || !apiKey.apiKey) {
|
||
throw new Error('OpenAI API 키가 설정되지 않았습니다');
|
||
}
|
||
|
||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey.apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'gpt-4o-mini',
|
||
messages: [
|
||
{
|
||
role: 'system',
|
||
content: '당신은 전문 번역가입니다. 주어진 텍스트를 한국어로 자연스럽게 번역하고 의미를 이해할수 있도록 의역도 추가해주세요.'
|
||
},
|
||
{
|
||
role: 'user',
|
||
content: text
|
||
}
|
||
],
|
||
max_tokens: 1000,
|
||
temperature: 0.3
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('OpenAI 번역 요청 실패');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data || !data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
||
throw new Error('OpenAI 번역 응답 형식 오류');
|
||
}
|
||
|
||
return data.choices[0].message.content.trim();
|
||
}
|
||
|
||
// Google Gemini 번역
|
||
async function translateWithGemini(text) {
|
||
console.log('[background.js] Gemini 번역 시작:', { textLength: text.length });
|
||
|
||
const apiKey = await getApiKey('gemini');
|
||
if (!apiKey || !apiKey.apiKey) {
|
||
console.error('[background.js] Gemini API 키가 설정되지 않았습니다:', apiKey);
|
||
throw new Error('Gemini API 키가 설정되지 않았습니다');
|
||
}
|
||
|
||
console.log('[background.js] Gemini API 키 확인 완료');
|
||
|
||
try {
|
||
// 더 간단한 프롬프트로 토큰 사용량 줄이기
|
||
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey.apiKey}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
contents: [{
|
||
parts: [{
|
||
text: `당신은 전문 번역가입니다. 주어진 텍스트를 한국어로 자연스럽게 번역하고 의미를 이해할수 있도록 의역도 추가해주세요.:\n\n${text}`
|
||
}]
|
||
}],
|
||
generationConfig: {
|
||
temperature: 0.1,
|
||
maxOutputTokens: 2048,
|
||
topP: 0.8,
|
||
topK: 10
|
||
}
|
||
})
|
||
});
|
||
|
||
console.log('[background.js] Gemini API 응답 상태:', {
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
ok: response.ok
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('[background.js] Gemini API 에러 응답:', errorText);
|
||
throw new Error(`Gemini 번역 요청 실패 (${response.status}): ${errorText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('[background.js] Gemini API 응답 데이터:', data);
|
||
|
||
// 응답 구조 검사 및 텍스트 추출
|
||
let translatedText = null;
|
||
|
||
if (data && data.candidates && Array.isArray(data.candidates) && data.candidates.length > 0) {
|
||
const candidate = data.candidates[0];
|
||
console.log('[background.js] Gemini 첫 번째 candidate:', candidate);
|
||
|
||
// finishReason 확인
|
||
if (candidate.finishReason === 'MAX_TOKENS') {
|
||
console.warn('[background.js] Gemini 응답이 토큰 제한으로 잘렸습니다');
|
||
}
|
||
|
||
// content.parts 구조 확인
|
||
if (candidate.content && candidate.content.parts && Array.isArray(candidate.content.parts) && candidate.content.parts.length > 0) {
|
||
const part = candidate.content.parts[0];
|
||
|
||
// parts[0]이 문자열인 경우
|
||
if (typeof part === 'string') {
|
||
translatedText = part;
|
||
}
|
||
// parts[0]이 객체이고 text 속성이 있는 경우
|
||
else if (typeof part === 'object' && part.text) {
|
||
translatedText = part.text;
|
||
}
|
||
}
|
||
// content가 없고 바로 parts가 있는 경우
|
||
else if (candidate.parts && Array.isArray(candidate.parts) && candidate.parts.length > 0) {
|
||
const part = candidate.parts[0];
|
||
if (typeof part === 'string') {
|
||
translatedText = part;
|
||
} else if (typeof part === 'object' && part.text) {
|
||
translatedText = part.text;
|
||
}
|
||
}
|
||
// content가 없고 바로 text가 있는 경우
|
||
else if (candidate.text) {
|
||
translatedText = candidate.text;
|
||
}
|
||
}
|
||
|
||
if (!translatedText) {
|
||
console.error('[background.js] Gemini 응답에서 번역 텍스트를 찾을 수 없습니다:', data);
|
||
|
||
// 디버깅을 위해 전체 응답 구조 로그
|
||
if (data && data.candidates && data.candidates[0]) {
|
||
console.log('[background.js] Gemini candidate 구조:', JSON.stringify(data.candidates[0], null, 2));
|
||
}
|
||
|
||
throw new Error('Gemini 번역 응답에서 텍스트를 추출할 수 없습니다');
|
||
}
|
||
|
||
console.log('[background.js] Gemini 번역 성공:', translatedText.substring(0, 100) + '...');
|
||
return translatedText.trim();
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] Gemini 번역 중 오류:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// API 키 저장소에서 가져오기
|
||
async function getApiKey(service) {
|
||
try {
|
||
console.log(`[background.js] ${service} API 키 조회 시작`);
|
||
|
||
const SUPABASE_URL = "https://ko.wrmc.cc";
|
||
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
|
||
|
||
// 먼저 간단한 테스트 호출 (인증 없이)
|
||
const testUrl = `${SUPABASE_URL}/rest/v1/api_keys?select=*&limit=1`;
|
||
console.log(`[background.js] 테스트 API 호출:`, testUrl);
|
||
|
||
try {
|
||
const testResponse = await fetch(testUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'apikey': SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
console.log(`[background.js] 테스트 응답 상태:`, testResponse.status);
|
||
|
||
if (testResponse.ok) {
|
||
const testData = await testResponse.json();
|
||
console.log(`[background.js] 테스트 데이터:`, testData);
|
||
} else {
|
||
const testError = await testResponse.text();
|
||
console.log(`[background.js] 테스트 오류:`, testError);
|
||
}
|
||
} catch (testErr) {
|
||
console.error(`[background.js] 테스트 호출 실패:`, testErr);
|
||
}
|
||
|
||
// 이제 실제 인증된 호출 시도
|
||
const { access_token } = await chrome.storage.local.get("access_token");
|
||
if (!access_token) {
|
||
console.error(`[background.js] Access token이 없습니다`);
|
||
return null;
|
||
}
|
||
|
||
// Supabase에서 API 키 조회
|
||
const apiUrl = `${SUPABASE_URL}/rest/v1/api_keys?select=*&source=eq.${service}&limit=1`;
|
||
|
||
console.log(`[background.js] API 키 조회 URL:`, apiUrl);
|
||
console.log(`[background.js] 사용할 access_token:`, access_token ? access_token.substring(0, 20) + '...' : 'null');
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'apikey': SUPABASE_ANON_KEY,
|
||
'Authorization': `Bearer ${access_token}`,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=representation'
|
||
}
|
||
});
|
||
|
||
console.log(`[background.js] API 응답 상태:`, response.status);
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error(`[background.js] API 키 조회 실패: ${response.status}`, errorText);
|
||
return null;
|
||
}
|
||
|
||
const apiKeys = await response.json();
|
||
console.log(`[background.js] 조회된 API 키 개수:`, apiKeys?.length || 0);
|
||
|
||
if (!apiKeys || apiKeys.length === 0) {
|
||
console.warn(`[background.js] ${service} API 키가 데이터베이스에 없습니다`);
|
||
return null;
|
||
}
|
||
|
||
const apiKeyRecord = apiKeys[0];
|
||
const apiKeyValue = apiKeyRecord.apikey; // 'api'에서 'apikey'로 변경
|
||
|
||
if (!apiKeyValue) {
|
||
console.error(`[background.js] ${service} API 키 값이 비어있습니다`);
|
||
return null;
|
||
}
|
||
|
||
// 서비스별 API 키 구조 생성
|
||
let formattedApiKey;
|
||
switch (service) {
|
||
case 'deepl':
|
||
formattedApiKey = {
|
||
authKey: apiKeyValue
|
||
};
|
||
break;
|
||
case 'openai':
|
||
case 'gemini':
|
||
formattedApiKey = {
|
||
apiKey: apiKeyValue
|
||
};
|
||
break;
|
||
default:
|
||
console.error(`[background.js] 지원하지 않는 서비스: ${service}`);
|
||
return null;
|
||
}
|
||
|
||
console.log(`[background.js] ${service} API 키 로드 성공`);
|
||
return formattedApiKey;
|
||
|
||
} catch (error) {
|
||
console.error(`[background.js] ${service} API 키 가져오기 실패:`, error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/* ===== 1. 키워드 검색 결과 파싱 (재귀적 인덱스 재활용 방식) ===== */
|
||
|
||
/**
|
||
* 키워드 검색 __NUXT_DATA__에서 등록정보(상표명, 출원번호 등)만 추출하여 결과 배열로 반환
|
||
*/
|
||
function parseSearchResults(globalData) {
|
||
let results = [];
|
||
let i = 0;
|
||
while (i < globalData.length) {
|
||
let item = globalData[i];
|
||
if (isPlainObject(item) && item.hasOwnProperty("applicationNum")) {
|
||
let regRes = extractRegistrationInfo(globalData, i);
|
||
let registration_info = regRes.registration_info;
|
||
i = regRes.nextIndex;
|
||
results.push({ registration_info });
|
||
} else {
|
||
i++;
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 등록정보 추출 (키워드 검색용 – 재귀적 인덱스 재활용)
|
||
*/
|
||
function extractRegistrationInfo(globalData, startIndex) {
|
||
let regMapping = null;
|
||
let i = startIndex;
|
||
for (; i < globalData.length; i++) {
|
||
let item = globalData[i];
|
||
if (isPlainObject(item) && item.hasOwnProperty("applicationNum")) {
|
||
regMapping = item;
|
||
break;
|
||
}
|
||
}
|
||
if (!regMapping) return { registration_info: {}, nextIndex: i };
|
||
const registration_info = {};
|
||
for (let key in regMapping) {
|
||
const ref = regMapping[key];
|
||
if (typeof ref === "number" && ref < globalData.length) {
|
||
registration_info[key] = globalData[ref];
|
||
} else {
|
||
registration_info[key] = ref;
|
||
}
|
||
}
|
||
return { registration_info, nextIndex: i + 1 };
|
||
}
|
||
|
||
/* ===== 2. 상세 검색 (출원번호 기반) – 단일 결과용, 단순 치환 방식 ===== */
|
||
|
||
/**
|
||
* 상세 조회: 주어진 출원번호(appNum)로 상세 페이지 __NUXT_DATA__를 가져와서
|
||
* 등록정보, 권리정보, 출원인 정보를 추출합니다.
|
||
* (대리인 정보는 제외)
|
||
*/
|
||
function fetchDetailInfo(appNum) {
|
||
const detailUrl = `https://markinfo.kr/search/${appNum}`;
|
||
return fetch(detailUrl)
|
||
.then(resp => {
|
||
if (!resp.ok) throw new Error(`네트워크 오류: ${resp.status}`);
|
||
return resp.text();
|
||
})
|
||
.then(html => {
|
||
const match = /<script[^>]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/i.exec(html);
|
||
if (!match) throw new Error("__NUXT_DATA__ 태그를 찾을 수 없습니다.");
|
||
let jsonString = match[1];
|
||
let detailGlobalData;
|
||
try {
|
||
detailGlobalData = JSON.parse(jsonString);
|
||
} catch (e) {
|
||
throw new Error("JSON 파싱 실패: " + e.toString());
|
||
}
|
||
if (!Array.isArray(detailGlobalData)) {
|
||
throw new Error("globalData가 배열이 아님");
|
||
}
|
||
// 단일 결과이므로, mapping 객체 내 숫자값은 바로 치환
|
||
const registration_info = detailExtractRegistrationInfo(detailGlobalData);
|
||
const rights_info = detailExtractRightsInfo(detailGlobalData);
|
||
const applicant_info = detailExtractApplicantInfo(detailGlobalData);
|
||
return { registration_info, rights_info, applicant_info };
|
||
});
|
||
}
|
||
|
||
// 단일 상세 조회용 등록정보 추출 (숫자이면 한 번만 치환)
|
||
function detailExtractRegistrationInfo(globalData) {
|
||
let regMapping = null;
|
||
for (let item of globalData) {
|
||
if (isPlainObject(item) && item.hasOwnProperty("applicationNum")) {
|
||
regMapping = item;
|
||
break;
|
||
}
|
||
}
|
||
if (!regMapping) return {};
|
||
const registration_info = {};
|
||
for (let key in regMapping) {
|
||
const ref = regMapping[key];
|
||
registration_info[key] = (typeof ref === "number" && ref < globalData.length)
|
||
? globalData[ref]
|
||
: ref;
|
||
}
|
||
return registration_info;
|
||
}
|
||
|
||
// 단일 상세 조회용 권리정보 추출 (숫자 치환만 진행)
|
||
function detailExtractRightsInfo(globalData) {
|
||
const rights_info = {};
|
||
for (let item of globalData) {
|
||
if (isPlainObject(item) &&
|
||
"classificationCode" in item &&
|
||
"asignProductName" in item &&
|
||
"asignProductNameEn" in item &&
|
||
"similarCodes" in item &&
|
||
!("applicationNum" in item)) {
|
||
let classificationCode = (typeof item.classificationCode === "number" && item.classificationCode < globalData.length)
|
||
? globalData[item.classificationCode]
|
||
: item.classificationCode;
|
||
let asignProductName = (typeof item.asignProductName === "number" && item.asignProductName < globalData.length)
|
||
? globalData[item.asignProductName]
|
||
: item.asignProductName;
|
||
let asignProductNameEn = (typeof item.asignProductNameEn === "number" && item.asignProductNameEn < globalData.length)
|
||
? globalData[item.asignProductNameEn]
|
||
: item.asignProductNameEn;
|
||
let similarCodes = (typeof item.similarCodes === "number" && item.similarCodes < globalData.length)
|
||
? globalData[item.similarCodes]
|
||
: item.similarCodes;
|
||
let designation = String(classificationCode);
|
||
if (!rights_info[designation]) {
|
||
rights_info[designation] = [];
|
||
}
|
||
rights_info[designation].push({
|
||
asignProductName,
|
||
asignProductNameEn,
|
||
similarCodes
|
||
});
|
||
}
|
||
}
|
||
return rights_info;
|
||
}
|
||
|
||
// 단일 상세 조회용 출원인 정보 추출 – 오직 nationalCodeName와 applicantName만 표시, 그리고 출원날짜와 권리상태도 추가
|
||
function detailExtractApplicantInfo(globalData) {
|
||
let mapping = {};
|
||
for (let item of globalData) {
|
||
if (isPlainObject(item) && item.hasOwnProperty("applicantCode")) {
|
||
mapping["nationalCodeName"] = (typeof item["nationalCodeName"] === "number" && item["nationalCodeName"] < globalData.length)
|
||
? globalData[item["nationalCodeName"]]
|
||
: item["nationalCodeName"];
|
||
mapping["applicantName"] = (typeof item["applicantName"] === "number" && item["applicantName"] < globalData.length)
|
||
? globalData[item["applicantName"]]
|
||
: item["applicantName"];
|
||
// 추가: 출원날짜와 권리상태 (예: lastDisposalCodeName)
|
||
mapping["applicationDate"] = (typeof item["applicationDate"] === "number" && item["applicationDate"] < globalData.length)
|
||
? globalData[item["applicationDate"]]
|
||
: item["applicationDate"];
|
||
mapping["lastDisposalCodeName"] = (typeof item["lastDisposalCodeName"] === "number" && item["lastDisposalCodeName"] < globalData.length)
|
||
? globalData[item["lastDisposalCodeName"]]
|
||
: item["lastDisposalCodeName"];
|
||
break;
|
||
}
|
||
}
|
||
return { mapping };
|
||
}
|
||
|
||
/* ===== 유틸리티 함수 ===== */
|
||
function isPlainObject(obj) {
|
||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||
}
|
||
|
||
// 금지어 추가 메시지 리스너 (중복 제거됨 - 삭제 완료)
|
||
|
||
// 금지어 추가 처리 함수
|
||
async function handleAddBannedWord(message, sendResponse) {
|
||
try {
|
||
console.log('[background.js] 금지어 추가 요청 받음:', message.keyword, '등급:', message.grade);
|
||
|
||
// 1. 토큰 확인
|
||
const { access_token } = await chrome.storage.local.get("access_token");
|
||
if (!access_token) {
|
||
sendResponse({ success: false, error: "로그인이 필요합니다." });
|
||
return;
|
||
}
|
||
|
||
// 2. 저장된 사용자 ID 확인
|
||
const { user_id, user_email } = await chrome.storage.local.get(["user_id", "user_email"]);
|
||
if (!user_id) {
|
||
sendResponse({ success: false, error: "사용자 정보를 찾을 수 없습니다. 다시 로그인해주세요." });
|
||
return;
|
||
}
|
||
|
||
console.log("[금지어 추가] 저장된 사용자 정보 사용:", { user_id, user_email });
|
||
|
||
// 백엔드 설정 가져오기
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 3. 중복 체크 - 같은 사용자의 같은 banned_word가 이미 존재하는지 확인
|
||
const duplicateCheckUrl = `${SUPABASE_URL}/rest/v1/user_banned_words?select=word_id&user_id=eq.${user_id}&banned_word=eq.${encodeURIComponent(message.keyword)}&limit=1`;
|
||
const duplicateCheckRes = await fetch(duplicateCheckUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${access_token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!duplicateCheckRes.ok) {
|
||
const errorText = await duplicateCheckRes.text();
|
||
console.error("[금지어 추가] 중복 체크 실패:", duplicateCheckRes.status, errorText);
|
||
sendResponse({ success: false, error: "중복 체크 중 오류가 발생했습니다." });
|
||
return;
|
||
}
|
||
|
||
const duplicateData = await duplicateCheckRes.json();
|
||
if (duplicateData && duplicateData.length > 0) {
|
||
console.log("[금지어 추가] 중복된 금지어 발견");
|
||
sendResponse({ success: false, error: "이미 금지어 목록에 있는 단어입니다." });
|
||
return;
|
||
}
|
||
|
||
// 4. user_banned_words 테이블에 금지어 추가
|
||
const bannedWordData = {
|
||
user_id: user_id,
|
||
banned_word: message.keyword,
|
||
grade: message.grade || '비허용',
|
||
created_at: new Date().toISOString()
|
||
};
|
||
|
||
console.log("[금지어 추가] user_banned_words에 삽입할 데이터:", bannedWordData);
|
||
|
||
const bannedWordUrl = `${SUPABASE_URL}/rest/v1/user_banned_words`;
|
||
const bannedWordRes = await fetch(bannedWordUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${access_token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=representation'
|
||
},
|
||
body: JSON.stringify(bannedWordData)
|
||
});
|
||
|
||
if (!bannedWordRes.ok) {
|
||
const errorText = await bannedWordRes.text();
|
||
console.error("[금지어 추가] user_banned_words 삽입 실패:", bannedWordRes.status, errorText);
|
||
sendResponse({ success: false, error: `금지어 추가 실패: ${errorText}` });
|
||
return;
|
||
}
|
||
|
||
const insertedBannedWord = await bannedWordRes.json();
|
||
const bannedWordId = insertedBannedWord[0].word_id;
|
||
console.log("[금지어 추가] user_banned_words 삽입 성공, word_id:", bannedWordId);
|
||
|
||
// 5. user_banned_words_kipris 테이블에 지재권 검색 결과 저장
|
||
if (message.searchResults && Array.isArray(message.searchResults) && message.searchResults.length > 0) {
|
||
const kiprisPromises = message.searchResults.map(async (result, index) => {
|
||
// 상태 매핑 (파이썬 _map_status 로직 적용)
|
||
const rawStatus = result.registration_info?.status || result.detail?.registration_info?.lastDisposalCodeName || "";
|
||
const mappedStatus = mapStatus(rawStatus);
|
||
|
||
// 분류 코드 처리 (파이썬 로직 적용)
|
||
const rawClassificationCode = result.registration_info?.classificationCode || "";
|
||
const { code: finalClassificationCode, description: finalCategoryDescription } = processClassificationCode(rawClassificationCode);
|
||
|
||
// 권리정보 처리 (파이썬 category_description 로직)
|
||
let categoryDescription = finalCategoryDescription;
|
||
if (result.detail?.rights_info && Object.keys(result.detail.rights_info).length > 0) {
|
||
let processedRights = "";
|
||
for (const [catCode, entries] of Object.entries(result.detail.rights_info)) {
|
||
processedRights += `카테고리 코드: ${catCode}\n`;
|
||
if (Array.isArray(entries)) {
|
||
for (const entry of entries) {
|
||
const name = (entry.asignProductName || "").trim();
|
||
const similar = (entry.similarCodes || "").trim();
|
||
processedRights += ` 지정상품군: ${name}, 유사군코드: ${similar}\n`;
|
||
}
|
||
}
|
||
processedRights += "\n";
|
||
}
|
||
categoryDescription = processedRights;
|
||
}
|
||
|
||
const kiprisData = {
|
||
user_id: user_id,
|
||
banned_word_id: bannedWordId,
|
||
application_status: mappedStatus,
|
||
registration_date: result.registration_info?.applicationDate || "",
|
||
applicant_name: result.detail?.applicant_info?.mapping?.applicantName || "",
|
||
classification_code: finalClassificationCode,
|
||
category_description: categoryDescription,
|
||
drawing: result.registration_info?.drawing || result.registration_info?.trademarkImage || "",
|
||
bigDrawing: null,
|
||
tradeMark_name: result.registration_info?.trademarkName || "",
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
|
||
console.log(`[금지어 추가] user_banned_words_kipris에 삽입할 데이터 ${index + 1}:`, kiprisData);
|
||
|
||
const kiprisUrl = `${SUPABASE_URL}/rest/v1/user_banned_words_kipris`;
|
||
const kiprisRes = await fetch(kiprisUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${access_token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=minimal'
|
||
},
|
||
body: JSON.stringify(kiprisData)
|
||
});
|
||
|
||
if (!kiprisRes.ok) {
|
||
const errorText = await kiprisRes.text();
|
||
console.error(`[금지어 추가] user_banned_words_kipris 삽입 실패 ${index + 1}:`, kiprisRes.status, errorText);
|
||
throw new Error(`지재권 결과 저장 실패 ${index + 1}: ${errorText}`);
|
||
}
|
||
|
||
console.log(`[금지어 추가] user_banned_words_kipris 삽입 성공 ${index + 1}`);
|
||
return true;
|
||
});
|
||
|
||
try {
|
||
await Promise.all(kiprisPromises);
|
||
console.log("[금지어 추가] 모든 지재권 결과 저장 완료");
|
||
} catch (kiprisError) {
|
||
console.error("[금지어 추가] 지재권 결과 저장 중 오류:", kiprisError);
|
||
sendResponse({
|
||
success: true,
|
||
warning: `금지어는 추가되었지만 일부 검색 결과 저장에 실패했습니다: ${kiprisError.message}`
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 6. 성공 응답
|
||
sendResponse({
|
||
success: true,
|
||
message: `"${message.keyword}"이(가) 금지어 목록에 추가되었습니다. (등급: ${message.grade || '비허용'})`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('[금지어 추가] 전체 오류:', error);
|
||
sendResponse({
|
||
success: false,
|
||
error: `금지어 추가 중 오류가 발생했습니다: ${error.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
// 저장된 토큰 가져오기
|
||
async function getStoredToken() {
|
||
try {
|
||
const result = await chrome.storage.local.get("access_token");
|
||
return result.access_token || null;
|
||
} catch (error) {
|
||
console.error('[background.js] 토큰 가져오기 실패:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 사용자 정보 가져오기
|
||
async function fetchUserInfo(token) {
|
||
try {
|
||
// 백엔드 설정 가져오기
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 사용자 기본 정보 가져오기 (토큰 검증)
|
||
const authUrl = `${SUPABASE_URL}/auth/v1/user`;
|
||
const authRes = await fetch(authUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!authRes.ok) {
|
||
console.error("[background.js] 토큰 검증 실패:", authRes.status);
|
||
return null;
|
||
}
|
||
|
||
const authUser = await authRes.json();
|
||
|
||
// 사용자 상세 정보 및 회원등급 확인
|
||
const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
|
||
const detailsRes = await fetch(detailsUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!detailsRes.ok) {
|
||
console.error("[background.js] 사용자 정보 조회 실패:", detailsRes.status);
|
||
return null;
|
||
}
|
||
|
||
const detailsData = await detailsRes.json();
|
||
return detailsData[0] || null;
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] 사용자 정보 가져오기 실패:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 키워드 검색 URL (기본 코드 그대로)
|
||
function buildMarkInfoUrl(keyword) {
|
||
const encoded = encodeURIComponent(keyword);
|
||
return `https://markinfo.kr/search?page=1&size=20&sort=_score,desc&sort=applicationDate,desc&searchType=ST01&searchKeyword=${encoded}&statuses=APPLICATION&statuses=PUBLICATION&statuses=REGISTRATION`;
|
||
}
|
||
|
||
// 상태 매핑 함수 (파이썬 _map_status와 동일)
|
||
function mapStatus(statusVal) {
|
||
if (typeof statusVal === 'string') {
|
||
const upperStatus = statusVal.toUpperCase();
|
||
if (upperStatus === "REGISTRATION") {
|
||
return "등록";
|
||
} else if (upperStatus === "APPLICATION") {
|
||
return "출원";
|
||
} else if (upperStatus === "PUBLICATION") {
|
||
return "공고";
|
||
} else {
|
||
return statusVal;
|
||
}
|
||
} else if (typeof statusVal === 'number') {
|
||
if (statusVal === 223) {
|
||
return "출원";
|
||
} else if (statusVal === 192) {
|
||
return "등록";
|
||
} else {
|
||
return "공고";
|
||
}
|
||
}
|
||
return statusVal;
|
||
}
|
||
|
||
// 분류 코드 처리 함수
|
||
// 카테고리 설명 데이터 (kiprisCategories.json 내용)
|
||
const KIPRIS_CATEGORIES = {
|
||
"01": "공업/과학 및 사진용 및 농업/원예 및 임업용 화학제; 미가공 인조수지, 미가공 플라스틱; 소화 및 화재예방용 조성물; 조질제 및 땜납용 조제; 수피용 무두질제; 공업용 접착제; 퍼티 및 기타 페이스트 충전제; 퇴비, 거름, 비료; 산업용 및 과학용 생물학적 제제",
|
||
"02": "페인트, 니스, 래커; 방청제 및 목재 보존제; 착색제, 염료; 인쇄, 표시 및 판화용 잉크; 미가공 천연수지; 도장용/장식용/인쇄용/미술용 금속박(箔) 및 금속분(紛)",
|
||
"03": "비의료용 화장품 및 세면용품; 비의료용 치약; 향료, 에센셜 오일; 표백제 및 기타 세탁용 제제; 세정/광택 및 연마재",
|
||
"04": "공업용 오일 및 그리스, 왁스; 윤활제; 먼지흡수제, 먼지습윤제 및 먼지흡착제; 연료 및 발광체; 조명용 양초 및 심지",
|
||
"05": "약제, 의료용 및 수의과용 제제; 의료용 위생제; 의료용 또는 수의과용 식이요법 식품 및 제제, 유아용 식품; 인체용 및 동물용 식이보충제; 플라스터, 외상치료용 재료; 치과용 충전재료, 치과용 왁스; 소독제; 해충구제제; 살균제, 제초제",
|
||
"06": "일반금속 및 그 합금, 광석; 금속제 건축 및 구축용 재료; 금속제 이동식 건축물; 비전기용 일반금속제 케이블 및 와이어; 소형금속제품; 저장 또는 운반용 금속제 용기; 금고",
|
||
"07": "기계, 공작기계, 전동공구; 모터 및 엔진(육상차량용은 제외); 기계 커플링 및 전동장치 부품(육상차량용은 제외); 농기구(수동식 수공구는 제외); 부란기(孵卵器); 자동판매기",
|
||
"08": "수동식 수공구 및 수동기구; 커틀러리; 휴대 무기(화기는 제외); 면도기",
|
||
"09": "과학, 연구, 항법, 측량, 사진, 영화, 시청각, 광학, 계량, 측정, 신호, 탐지, 시험, 검사, 구명 및 교육용 기기; 전기 분배 또는 전기 사용의 전도, 전환, 변형, 축적, 조절 또는 통제를 위한 기기; 음향/영상 또는 데이터의 기록/전송/재생 또는 처리용 장치 및 기구; 기록 및 내려받기 가능한 미디어, 컴퓨터 소프트웨어, 빈 디지털 또는 아날로그 기록 및 저장매체; 동전작동식 기계장치; 금전등록기, 계산기; 컴퓨터 및 컴퓨터주변기기; 잠수복, 잠수마스크, 잠수용 귀마개, 다이버 및 수영용 노즈클립, 잠수용 장갑, 잠수용 호흡장치; 소화기기",
|
||
"10": "외과용, 내과용, 치과용 및 수의과용 기계기구; 의지(義肢), 의안(義眼) 및 의치(義齒); 정형외과용품; 봉합용 재료; 장애인용 치료 및 재활보조장치; 안마기; 유아수유용 기기 및 용품; 성활동용 기기 및 용품",
|
||
"11": "조명용, 가열용, 냉각용, 증기발생용, 조리용, 건조용, 환기용, 급수용, 위생용 장치 및 설비",
|
||
"12": "수송기계기구; 육상, 항공 또는 해상을 통해 이동하는 수송수단",
|
||
"13": "화기(火器); 탄약 및 발사체; 폭약; 폭죽",
|
||
"14": "귀금속 및 그 합금; 보석, 귀석 및 반귀석; 시계용구",
|
||
"15": "악기; 악보대 및 악기용 받침대; 지휘봉",
|
||
"16": "종이 및 판지; 인쇄물; 제본재료; 사진; 문방구 및 사무용품(가구는 제외); 문방구용 또는 가정용 접착제; 제도용구 및 미술용 재료; 회화용 솔; 교재; 포장용 플라스틱제 시트, 필름 및 가방; 인쇄활자, 프린팅블록",
|
||
"17": "미가공 및 반가공 고무, 구타페르카, 고무액(gum), 석면, 운모(雲母) 및 이들의 제품; 제조용 압출성형형태의 플라스틱 및 수지; 충전용, 마개용 및 절연용 재료; 비금속제 신축관, 튜브 및 호스",
|
||
"18": "가죽 및 모조가죽; 수피; 수하물가방 및 운반용 가방; 우산 및 파라솔; 걷기용 지팡이; 채찍 및 마구(馬具); 동물용 목걸이, 가죽끈 및 의류",
|
||
"19": "건축용 및 구축용 비금속제 건축재료; 건축용 비금속제 경질관(硬質管); 아스팔트, 피치, 타르 및 역청; 비금속제 이동식 건축물; 비금속제 기념물",
|
||
"20": "가구, 거울, 액자; 보관 또는 운송용 비금속제 컨테이너; 미가공 또는 반가공 뼈, 뿔, 고래수염 또는 나전(螺鈿); 패각; 해포석(海泡石); 호박(琥珀)(원석)",
|
||
"21": "가정용 또는 주방용 기구 및 용기; 조리기구 및 식기(포크, 나이프 및 스푼은 제외); 빗 및 스펀지; 솔(페인트 솔은 제외); 솔 제조용 재료; 청소용구; 비건축용 미가공 또는 반가공 유리; 유리제품, 도자기제품 및 토기제품",
|
||
"22": "로프 및 노끈; 망(網); 텐트 및 타폴린; 직물제 또는 합성재료제 차양; 돛; 하역물운반용 및 보관용 포대; 충전재료(고무/플라스틱/종이 및 판지제는 제외); 직물용 미가공 섬유 및 그 대용품",
|
||
"23": "직물용 실(絲)",
|
||
"24": "직물 및 직물대용품; 가정용 린넨; 직물 또는 플라스틱제 커튼",
|
||
"25": "의류, 신발, 모자",
|
||
"26": "레이스, 장식용 끈 및 자수포, 의류장식용 리본 및 나비매듭리본; 단추, 훅 및 아이(hooks and eyes), 핀 및 바늘; 조화(造花); 머리장식품; 가발",
|
||
"27": "카펫, 융단, 매트, 리놀륨 및 기타 바닥깔개용 재료; 비직물제 벽걸이",
|
||
"28": "오락용구, 장난감; 비디오게임장치; 체조 및 스포츠용품; 크리스마스트리용 장식품",
|
||
"29": "식육, 생선, 가금 및 엽조수; 고기진액; 보존처리/냉동/건조 및 조리된 과일 및 채소; 젤리, 잼, 콤폿; 달걀; 우유, 치즈, 버터, 요구르트 및 기타 유제품; 식용 유지(油脂)",
|
||
"30": "커피, 차(茶), 코코아 및 그 대용물; 쌀, 파스타 및 국수; 타피오카 및 사고(sago); 곡분 및 곡물 조제품; 빵, 페이스트리 및 과자; 초콜릿; 아이스크림, 셔벗 및 기타 식용 얼음; 설탕, 꿀, 당밀(糖蜜); 식품용 이스트, 베이킹 파우더; 소금, 조미료, 향신료, 보존처리된 허브; 식초, 소스 및 기타 조미료; 얼음",
|
||
"31": "미가공 농업, 수산양식, 원예 및 임업 생산물; 미가공 곡물 및 종자; 신선한 과실 및 채소, 신선한 허브; 살아 있는 식물 및 꽃; 구근(球根), 모종 및 재배용 곡물종자; 살아있는 동물; 동물용 사료 및 음료; 맥아",
|
||
"32": "맥주; 비알코올성 음료; 광천수 및 탄산수; 과실음료 및 과실주스; 시럽 및 비알코올성 음료용 제제",
|
||
"33": "알코올성 음료(맥주는 제외); 음료제조용 알코올성 제제",
|
||
"34": "담배 및 대용담배; 권연 및 여송연; 흡연자용 전자담배 및 기화기; 흡연용구; 성냥",
|
||
"35": "광고업; 사업관리/조직 및 경영업; 사무처리업",
|
||
"36": "금융, 통화 및 은행업; 보험서비스업; 부동산업",
|
||
"37": "건축서비스업; 설치 및 수리서비스업; 채광업/석유 및 가스 시추업",
|
||
"38": "통신서비스업",
|
||
"39": "운송업; 상품의 포장 및 보관업; 여행알선업",
|
||
"40": "재료처리업; 폐기물 재생업; 공기 정화 및 물 처리업; 인쇄 서비스업; 음식 및 음료수 보존업",
|
||
"41": "교육업; 훈련제공업; 연예오락업; 스포츠 및 문화활동업",
|
||
"42": "과학적, 기술적 서비스업 및 관련 연구, 디자인업; 산업분석, 산업연구 및 산업디자인 서비스업; 품질 관리 및 인증 서비스업; 컴퓨터 하드웨어 및 소프트웨어의 디자인 및 개발업",
|
||
"43": "식음료제공서비스업; 임시숙박시설업",
|
||
"44": "의료업; 수의업; 인간 또는 동물을 위한 위생 및 미용업; 농업, 수산양식, 원예 및 임업 서비스업",
|
||
"45": "법무서비스업; 유형의 재산 및 개인을 물리적으로 보호하기 위한 보안서비스업; 이성(異性) 소개업, 온라인 소셜 네트워킹 서비스업; 장례업; 베이비시팅업"
|
||
};
|
||
|
||
// 분류 코드 처리 함수
|
||
function processClassificationCode(classificationCode) {
|
||
if (!classificationCode) return { code: "", description: "" };
|
||
|
||
let codes = [];
|
||
if (typeof classificationCode === 'string') {
|
||
codes = classificationCode.split(',').map(code => code.trim());
|
||
} else if (Array.isArray(classificationCode)) {
|
||
codes = classificationCode;
|
||
} else {
|
||
codes = [String(classificationCode)];
|
||
}
|
||
|
||
const finalCode = codes.join('|');
|
||
const mappedList = codes.map(code => {
|
||
const description = KIPRIS_CATEGORIES[code] || code;
|
||
return `[${code}] ${description}`;
|
||
});
|
||
const finalDescription = mappedList.join('|') + '\n';
|
||
|
||
return { code: finalCode, description: finalDescription };
|
||
}
|
||
|
||
// 한국어를 중국어로 번역하여 텍스트 대체
|
||
async function handleKoreanToChinese(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);
|
||
|
||
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 (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 {
|
||
// 한국어도 중국어도 아닌 경우
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '지원하지 않는 언어',
|
||
message: '한국어 또는 중국어 텍스트를 선택해주세요.'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 선택된 텍스트를 번역된 텍스트로 대체
|
||
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: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 범용 번역 함수
|
||
async function translateText(text, sourceLang, targetLang) {
|
||
console.log(`[translateText] 번역 시작: ${sourceLang} -> ${targetLang}, 텍스트: ${text}`);
|
||
|
||
// 1. Google 번역 시도
|
||
try {
|
||
const googleUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
|
||
console.log('[translateText] Google 번역 시도:', googleUrl);
|
||
|
||
const response = await fetch(googleUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
if (data && data[0] && data[0][0] && data[0][0][0]) {
|
||
const translatedText = data[0][0][0];
|
||
console.log('[translateText] Google 번역 성공:', translatedText);
|
||
return translatedText;
|
||
}
|
||
}
|
||
|
||
console.log('[translateText] Google 번역 응답 형식 오류');
|
||
} catch (error) {
|
||
console.error('[translateText] Google 번역 실패:', error);
|
||
}
|
||
|
||
// 2. MyMemory 번역 시도
|
||
try {
|
||
console.log('[translateText] MyMemory 번역 시도');
|
||
const myMemoryUrl = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`;
|
||
|
||
const response = await fetch(myMemoryUrl);
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
if (data && data.responseData && data.responseData.translatedText) {
|
||
const translatedText = data.responseData.translatedText;
|
||
console.log('[translateText] MyMemory 번역 성공:', translatedText);
|
||
return translatedText;
|
||
}
|
||
}
|
||
|
||
console.log('[translateText] MyMemory 번역 응답 형식 오류');
|
||
} catch (error) {
|
||
console.error('[translateText] MyMemory 번역 실패:', error);
|
||
}
|
||
|
||
// 3. 간단한 사전 기반 번역 (한중/중한만)
|
||
if ((sourceLang === 'ko' && targetLang === 'zh') || (sourceLang === 'zh' && targetLang === 'ko')) {
|
||
try {
|
||
console.log('[translateText] 사전 기반 번역 시도');
|
||
const dictResult = await simpleDictionaryTranslate(text, sourceLang, targetLang);
|
||
if (dictResult) {
|
||
console.log('[translateText] 사전 기반 번역 성공:', dictResult);
|
||
return dictResult;
|
||
}
|
||
} catch (error) {
|
||
console.error('[translateText] 사전 기반 번역 실패:', error);
|
||
}
|
||
}
|
||
|
||
// 4. 모든 번역 실패 시 원본 텍스트 반환
|
||
console.log('[translateText] 모든 번역 방법 실패, 원본 텍스트 반환');
|
||
throw new Error('모든 번역 엔진에서 번역에 실패했습니다');
|
||
}
|
||
|
||
// 간단한 사전 기반 번역 함수
|
||
async function simpleDictionaryTranslate(text, sourceLang, targetLang) {
|
||
// 기본적인 한중/중한 단어 사전
|
||
const koreanToChinese = {
|
||
'안녕하세요': '你好',
|
||
'감사합니다': '谢谢',
|
||
'죄송합니다': '对不起',
|
||
'네': '是',
|
||
'아니요': '不是',
|
||
'좋습니다': '好的',
|
||
'플래그십': '旗舰',
|
||
'스토어': '店',
|
||
'상점': '商店',
|
||
'가게': '店铺'
|
||
};
|
||
|
||
const chineseToKorean = {
|
||
'你好': '안녕하세요',
|
||
'谢谢': '감사합니다',
|
||
'对不起': '죄송합니다',
|
||
'是': '네',
|
||
'不是': '아니요',
|
||
'好的': '좋습니다',
|
||
'旗舰': '플래그십',
|
||
'店': '스토어',
|
||
'商店': '상점',
|
||
'店铺': '가게',
|
||
'旗舰店': '플래그십 스토어'
|
||
};
|
||
|
||
if (sourceLang === 'ko' && targetLang === 'zh') {
|
||
return koreanToChinese[text.trim()] || null;
|
||
} else if (sourceLang === 'zh' && targetLang === 'ko') {
|
||
return chineseToKorean[text.trim()] || null;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 선택된 텍스트를 새로운 텍스트로 대체하는 함수 (content script에서 실행)
|
||
function replaceSelectedText(newText) {
|
||
console.log('[replaceSelectedText] 텍스트 대체 시작:', newText);
|
||
console.log('[replaceSelectedText] 현재 URL:', window.location.href);
|
||
console.log('[replaceSelectedText] activeElement:', document.activeElement);
|
||
|
||
const selection = window.getSelection();
|
||
const activeElement = document.activeElement;
|
||
let textReplaced = false;
|
||
|
||
// 선택된 텍스트의 위치 정보 저장
|
||
let selectionRect = null;
|
||
if (selection.rangeCount > 0) {
|
||
const range = selection.getRangeAt(0);
|
||
selectionRect = range.getBoundingClientRect();
|
||
}
|
||
|
||
// 알리왕왕 특화 처리 - 더 광범위한 선택자 사용
|
||
if (window.location.href.includes('aliexpress') || window.location.href.includes('1688') ||
|
||
document.querySelector('.ice-container') || document.querySelector('[class*="aliwangwang"]') ||
|
||
window.location.href.includes('alibaba') || document.querySelector('[class*="im-"]')) {
|
||
console.log('[replaceSelectedText] 알리왕왕/알리바바 환경 감지됨');
|
||
|
||
// 더 광범위한 입력창 선택자
|
||
const inputSelectors = [
|
||
// 기본 입력 요소
|
||
'textarea', 'input[type="text"]', 'input:not([type])',
|
||
// contentEditable 요소
|
||
'[contenteditable="true"]', '[contenteditable=""]', 'div[contenteditable]',
|
||
// 알리왕왕 특화 선택자
|
||
'.ice-container textarea', '.ice-container input', '.ice-container [contenteditable]',
|
||
'.chat-input textarea', '.chat-input input', '.chat-input [contenteditable]',
|
||
'[class*="input"] textarea', '[class*="input"] input', '[class*="input"] [contenteditable]',
|
||
'[class*="chat"] textarea', '[class*="chat"] input', '[class*="chat"] [contenteditable]',
|
||
'[class*="message"] textarea', '[class*="message"] input', '[class*="message"] [contenteditable]',
|
||
'[class*="editor"] textarea', '[class*="editor"] input', '[class*="editor"] [contenteditable]',
|
||
// 역할 기반 선택자
|
||
'div[role="textbox"]', '[role="textbox"]', '[role="combobox"]',
|
||
// 플레이스홀더 기반
|
||
'[placeholder*="메시지"]', '[placeholder*="message"]', '[placeholder*="输入"]', '[placeholder*="请输入"]',
|
||
// 클래스명 기반
|
||
'.editor', '.input-box', '.text-input', '.message-input', '.chat-editor'
|
||
];
|
||
|
||
const allInputs = document.querySelectorAll(inputSelectors.join(','));
|
||
console.log('[replaceSelectedText] 찾은 모든 입력 요소 수:', allInputs.length);
|
||
|
||
// 각 입력 요소 상세 검사
|
||
for (let i = 0; i < allInputs.length; i++) {
|
||
const input = allInputs[i];
|
||
const rect = input.getBoundingClientRect();
|
||
const isVisible = rect.width > 0 && rect.height > 0;
|
||
const isFocused = input.matches(':focus') || input === activeElement;
|
||
const hasText = input.value || input.textContent || input.innerText;
|
||
|
||
console.log(`[replaceSelectedText] 입력창 ${i+1}:`, {
|
||
tagName: input.tagName,
|
||
className: input.className,
|
||
id: input.id,
|
||
isVisible: isVisible,
|
||
isFocused: isFocused,
|
||
hasText: !!hasText,
|
||
placeholder: input.placeholder,
|
||
contentEditable: input.contentEditable
|
||
});
|
||
|
||
// 활성화된 또는 보이는 입력창에 텍스트 삽입 시도
|
||
if (isVisible && (isFocused || rect.width > 100)) {
|
||
console.log(`[replaceSelectedText] 입력창 ${i+1} 처리 시도`);
|
||
|
||
if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
|
||
try {
|
||
// 포커스 설정
|
||
input.focus();
|
||
input.click();
|
||
|
||
// 기존 값 백업
|
||
const originalValue = input.value || '';
|
||
console.log('[replaceSelectedText] 기존 값:', originalValue);
|
||
|
||
// 선택 영역 확인
|
||
const start = input.selectionStart || 0;
|
||
const end = input.selectionEnd || originalValue.length;
|
||
|
||
// 텍스트 대체 - 여러 방법 시도
|
||
|
||
// 방법 1: 직접 value 설정
|
||
if (start !== end && start < end) {
|
||
// 선택된 텍스트 대체
|
||
input.value = originalValue.substring(0, start) + newText + originalValue.substring(end);
|
||
input.selectionStart = start;
|
||
input.selectionEnd = start + newText.length;
|
||
} else {
|
||
// 전체 텍스트 대체 또는 추가
|
||
if (originalValue.trim()) {
|
||
input.value = newText; // 기존 텍스트를 새 텍스트로 완전 대체
|
||
} else {
|
||
input.value = newText; // 빈 입력창에 새 텍스트 입력
|
||
}
|
||
input.selectionStart = input.selectionEnd = newText.length;
|
||
}
|
||
|
||
console.log('[replaceSelectedText] 새로운 값:', input.value);
|
||
|
||
// 방법 2: execCommand 시도 (폴백)
|
||
if (input.value !== newText && !input.value.includes(newText)) {
|
||
input.select();
|
||
document.execCommand('insertText', false, newText);
|
||
}
|
||
|
||
// 다양한 이벤트 발생
|
||
const events = [
|
||
'focus', 'input', 'change', 'keydown', 'keyup', 'keypress',
|
||
'textInput', 'compositionstart', 'compositionend', 'blur'
|
||
];
|
||
|
||
events.forEach(eventType => {
|
||
try {
|
||
let event;
|
||
if (eventType.includes('key')) {
|
||
event = new KeyboardEvent(eventType, {
|
||
key: 'a',
|
||
code: 'KeyA',
|
||
bubbles: true,
|
||
cancelable: true
|
||
});
|
||
} else {
|
||
event = new Event(eventType, {
|
||
bubbles: true,
|
||
cancelable: true
|
||
});
|
||
}
|
||
input.dispatchEvent(event);
|
||
} catch (e) {
|
||
console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e);
|
||
}
|
||
});
|
||
|
||
// React/Vue 등의 프레임워크를 위한 특별 처리
|
||
if (input._valueTracker) {
|
||
input._valueTracker.setValue(originalValue);
|
||
}
|
||
|
||
// 강제로 다시 포커스
|
||
setTimeout(() => {
|
||
input.focus();
|
||
input.selectionStart = input.selectionEnd = input.value.length;
|
||
}, 100);
|
||
|
||
// 텍스트가 실제로 변경되었는지 확인
|
||
if (input.value.includes(newText)) {
|
||
textReplaced = true;
|
||
console.log('[replaceSelectedText] INPUT/TEXTAREA 처리 완료');
|
||
return true;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[replaceSelectedText] INPUT/TEXTAREA 처리 중 오류:', error);
|
||
}
|
||
|
||
} else if (input.contentEditable === 'true' || input.contentEditable === '' ||
|
||
input.getAttribute('contenteditable') === 'true') {
|
||
try {
|
||
console.log('[replaceSelectedText] contentEditable 요소 처리');
|
||
|
||
// 포커스 설정
|
||
input.focus();
|
||
input.click();
|
||
|
||
// 기존 내용 백업
|
||
const originalContent = input.textContent || input.innerHTML || '';
|
||
console.log('[replaceSelectedText] 기존 내용:', originalContent);
|
||
|
||
// 방법 1: 선택 후 대체
|
||
const sel = window.getSelection();
|
||
|
||
// 전체 내용 선택
|
||
const range = document.createRange();
|
||
range.selectNodeContents(input);
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
|
||
// execCommand로 텍스트 삽입
|
||
if (document.execCommand('insertText', false, newText)) {
|
||
console.log('[replaceSelectedText] execCommand insertText 성공');
|
||
} else {
|
||
// 방법 2: 직접 DOM 조작
|
||
input.innerHTML = '';
|
||
input.textContent = newText;
|
||
|
||
// 커서를 끝으로 이동
|
||
const newRange = document.createRange();
|
||
newRange.selectNodeContents(input);
|
||
newRange.collapse(false);
|
||
sel.removeAllRanges();
|
||
sel.addRange(newRange);
|
||
}
|
||
|
||
console.log('[replaceSelectedText] 새로운 내용:', input.textContent || input.innerHTML);
|
||
|
||
// 이벤트 발생
|
||
const events = [
|
||
'focus', 'input', 'textInput', 'keyup', 'compositionend',
|
||
'DOMCharacterDataModified', 'DOMSubtreeModified', 'blur'
|
||
];
|
||
|
||
events.forEach(eventType => {
|
||
try {
|
||
const event = new Event(eventType, { bubbles: true, cancelable: true });
|
||
input.dispatchEvent(event);
|
||
} catch (e) {
|
||
console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e);
|
||
}
|
||
});
|
||
|
||
// 강제로 다시 포커스
|
||
setTimeout(() => {
|
||
input.focus();
|
||
const finalRange = document.createRange();
|
||
finalRange.selectNodeContents(input);
|
||
finalRange.collapse(false);
|
||
const finalSel = window.getSelection();
|
||
finalSel.removeAllRanges();
|
||
finalSel.addRange(finalRange);
|
||
}, 100);
|
||
|
||
// 텍스트가 실제로 변경되었는지 확인
|
||
if (input.textContent.includes(newText) || input.innerHTML.includes(newText)) {
|
||
textReplaced = true;
|
||
console.log('[replaceSelectedText] contentEditable 처리 완료');
|
||
return true;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[replaceSelectedText] contentEditable 처리 중 오류:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 일반 사이트 처리 (기존 로직)
|
||
if (!textReplaced) {
|
||
console.log('[replaceSelectedText] 일반 사이트 처리 시작');
|
||
|
||
// 1. input/textarea 처리
|
||
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
|
||
console.log('[replaceSelectedText] INPUT/TEXTAREA 요소 처리');
|
||
const start = activeElement.selectionStart;
|
||
const end = activeElement.selectionEnd;
|
||
const value = activeElement.value;
|
||
|
||
activeElement.value = value.substring(0, start) + newText + value.substring(end);
|
||
activeElement.selectionStart = start;
|
||
activeElement.selectionEnd = start + newText.length;
|
||
|
||
// 다양한 이벤트 트리거
|
||
['input', 'change', 'keyup', 'blur'].forEach(eventType => {
|
||
activeElement.dispatchEvent(new Event(eventType, { bubbles: true }));
|
||
});
|
||
|
||
if (activeElement.value.includes(newText)) {
|
||
textReplaced = true;
|
||
console.log('[replaceSelectedText] INPUT/TEXTAREA 텍스트 대체 완료');
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 2. contentEditable 요소 처리
|
||
if (activeElement && activeElement.contentEditable === 'true') {
|
||
console.log('[replaceSelectedText] contentEditable 요소 처리');
|
||
if (selection.rangeCount > 0) {
|
||
const range = selection.getRangeAt(0);
|
||
range.deleteContents();
|
||
range.insertNode(document.createTextNode(newText));
|
||
|
||
// 커서 위치 조정
|
||
range.collapse(false);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
|
||
// contentEditable 이벤트 트리거
|
||
['input', 'textInput', 'keyup'].forEach(eventType => {
|
||
activeElement.dispatchEvent(new Event(eventType, { bubbles: true }));
|
||
});
|
||
|
||
textReplaced = true;
|
||
console.log('[replaceSelectedText] contentEditable 텍스트 대체 완료');
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 3. 일반 선택 영역 처리
|
||
if (selection.rangeCount > 0) {
|
||
console.log('[replaceSelectedText] 일반 선택 영역 처리');
|
||
const range = selection.getRangeAt(0);
|
||
|
||
// 선택된 텍스트가 있는지 확인
|
||
if (range.toString().trim()) {
|
||
range.deleteContents();
|
||
range.insertNode(document.createTextNode(newText));
|
||
|
||
// 새로운 텍스트 선택
|
||
const newRange = document.createRange();
|
||
newRange.setStart(range.startContainer, range.startOffset - newText.length);
|
||
newRange.setEnd(range.startContainer, range.startOffset);
|
||
selection.removeAllRanges();
|
||
selection.addRange(newRange);
|
||
|
||
textReplaced = true;
|
||
console.log('[replaceSelectedText] 일반 텍스트 대체 완료');
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 텍스트 대체가 실패한 경우 팝업 표시
|
||
if (!textReplaced) {
|
||
console.log('[replaceSelectedText] 텍스트 대체 실패, 팝업 표시');
|
||
showTranslationPopup(newText, selectionRect);
|
||
|
||
// 클립보드에도 복사
|
||
try {
|
||
navigator.clipboard.writeText(newText).then(() => {
|
||
console.log('[replaceSelectedText] 클립보드에 복사 완료');
|
||
}).catch(err => {
|
||
console.error('[replaceSelectedText] 클립보드 복사 실패:', err);
|
||
});
|
||
} catch (error) {
|
||
console.error('[replaceSelectedText] 클립보드 접근 실패:', error);
|
||
}
|
||
}
|
||
|
||
return textReplaced;
|
||
}
|
||
|
||
// 번역 결과 팝업을 표시하는 함수
|
||
function showTranslationPopup(translatedText, selectionRect) {
|
||
console.log('[showTranslationPopup] 팝업 표시 시작:', translatedText);
|
||
|
||
// 기존 팝업이 있으면 제거
|
||
const existingPopup = document.getElementById('translation-popup-extension');
|
||
if (existingPopup) {
|
||
existingPopup.remove();
|
||
}
|
||
|
||
// 팝업 컨테이너 생성
|
||
const popup = document.createElement('div');
|
||
popup.id = 'translation-popup-extension';
|
||
popup.style.cssText = `
|
||
position: fixed;
|
||
z-index: 999999;
|
||
background: #ffffff;
|
||
border: 2px solid #4CAF50;
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
font-family: 'Arial', 'Malgun Gothic', sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
max-width: 300px;
|
||
word-wrap: break-word;
|
||
animation: fadeInScale 0.3s ease-out;
|
||
`;
|
||
|
||
// 애니메이션 CSS 추가
|
||
if (!document.getElementById('translation-popup-styles')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'translation-popup-styles';
|
||
style.textContent = `
|
||
@keyframes fadeInScale {
|
||
0% {
|
||
opacity: 0;
|
||
transform: scale(0.8) translateY(-10px);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
}
|
||
@keyframes fadeOut {
|
||
0% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: scale(0.9);
|
||
}
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// 팝업 내용 생성
|
||
const content = document.createElement('div');
|
||
content.innerHTML = `
|
||
<div style="margin-bottom: 8px; font-weight: bold; color: #4CAF50; font-size: 12px;">
|
||
🌐 번역 결과
|
||
</div>
|
||
<div style="color: #333; margin-bottom: 10px; padding: 8px; background: #f9f9f9; border-radius: 4px;">
|
||
${translatedText}
|
||
</div>
|
||
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
||
<button id="copy-translation" style="
|
||
background: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: background 0.2s;
|
||
">복사</button>
|
||
<button id="close-popup" style="
|
||
background: #666;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: background 0.2s;
|
||
">닫기</button>
|
||
</div>
|
||
`;
|
||
|
||
popup.appendChild(content);
|
||
|
||
// 팝업 위치 계산
|
||
let left = 50;
|
||
let top = 50;
|
||
|
||
if (selectionRect && selectionRect.width > 0 && selectionRect.height > 0) {
|
||
// 선택된 텍스트 위치 기준으로 팝업 위치 설정
|
||
left = selectionRect.left + selectionRect.width / 2;
|
||
top = selectionRect.top - 10; // 선택된 텍스트 위쪽에 표시
|
||
|
||
// 화면 경계 확인 및 조정
|
||
const maxLeft = window.innerWidth - 320; // 팝업 너비 고려
|
||
const maxTop = window.innerHeight - 150; // 팝업 높이 고려
|
||
|
||
if (left > maxLeft) left = maxLeft;
|
||
if (left < 10) left = 10;
|
||
|
||
if (top < 10) {
|
||
// 위쪽 공간이 부족하면 선택된 텍스트 아래쪽에 표시
|
||
top = selectionRect.bottom + 10;
|
||
}
|
||
if (top > maxTop) top = maxTop;
|
||
} else {
|
||
// 선택 영역 정보가 없으면 화면 중앙에 표시
|
||
left = (window.innerWidth - 300) / 2;
|
||
top = (window.innerHeight - 150) / 2;
|
||
}
|
||
|
||
popup.style.left = left + 'px';
|
||
popup.style.top = top + 'px';
|
||
|
||
// 문서에 팝업 추가
|
||
document.body.appendChild(popup);
|
||
|
||
// 이벤트 리스너 추가
|
||
const copyButton = popup.querySelector('#copy-translation');
|
||
const closeButton = popup.querySelector('#close-popup');
|
||
|
||
copyButton.addEventListener('click', async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(translatedText);
|
||
copyButton.textContent = '복사됨!';
|
||
copyButton.style.background = '#2196F3';
|
||
setTimeout(() => {
|
||
copyButton.textContent = '복사';
|
||
copyButton.style.background = '#4CAF50';
|
||
}, 1500);
|
||
} catch (err) {
|
||
console.error('클립보드 복사 실패:', err);
|
||
copyButton.textContent = '실패';
|
||
copyButton.style.background = '#f44336';
|
||
}
|
||
});
|
||
|
||
closeButton.addEventListener('click', () => {
|
||
popup.style.animation = 'fadeOut 0.2s ease-in';
|
||
setTimeout(() => {
|
||
if (popup.parentNode) {
|
||
popup.remove();
|
||
}
|
||
}, 200);
|
||
});
|
||
|
||
// 5초 후 자동으로 팝업 제거
|
||
setTimeout(() => {
|
||
if (popup.parentNode) {
|
||
popup.style.animation = 'fadeOut 0.3s ease-in';
|
||
setTimeout(() => {
|
||
if (popup.parentNode) {
|
||
popup.remove();
|
||
}
|
||
}, 300);
|
||
}
|
||
}, 5000);
|
||
|
||
// 팝업 외부 클릭 시 닫기
|
||
document.addEventListener('click', function closeOnOutsideClick(e) {
|
||
if (!popup.contains(e.target)) {
|
||
popup.style.animation = 'fadeOut 0.2s ease-in';
|
||
setTimeout(() => {
|
||
if (popup.parentNode) {
|
||
popup.remove();
|
||
}
|
||
}, 200);
|
||
document.removeEventListener('click', closeOnOutsideClick);
|
||
}
|
||
});
|
||
|
||
console.log('[showTranslationPopup] 팝업 표시 완료');
|
||
}
|
||
|
||
// ==================== 시간 알람 기능 ====================
|
||
|
||
class TimeAlarmManager {
|
||
constructor() {
|
||
this.workTimer = null;
|
||
this.breakTimer = null;
|
||
this.isBreakTime = false;
|
||
this.settings = {
|
||
enabled: true,
|
||
workTime: 60, // 분
|
||
restTime: 5, // 분
|
||
autoZzim: false
|
||
};
|
||
this.startTime = null;
|
||
}
|
||
|
||
async init() {
|
||
console.log('[TimeAlarm] 타이머 초기화 시작');
|
||
|
||
// 로그인 상태 확인
|
||
try {
|
||
const { access_token } = await chrome.storage.local.get('access_token');
|
||
if (!access_token) {
|
||
console.log('[TimeAlarm] 로그인되지 않음 - 타이머 시작하지 않음');
|
||
return;
|
||
}
|
||
console.log('[TimeAlarm] 로그인 상태 확인됨');
|
||
} catch (error) {
|
||
console.error('[TimeAlarm] 로그인 상태 확인 실패:', error);
|
||
return;
|
||
}
|
||
|
||
await this.loadSettings();
|
||
if (this.settings.enabled) {
|
||
this.startWorkTimer();
|
||
console.log('[TimeAlarm] 시간 알람 시작:', this.settings);
|
||
} else {
|
||
console.log('[TimeAlarm] 시간 알람이 비활성화되어 있음');
|
||
}
|
||
}
|
||
|
||
async loadSettings() {
|
||
try {
|
||
const result = await chrome.storage.local.get('time_alarm_settings');
|
||
this.settings = { ...this.settings, ...result.time_alarm_settings };
|
||
console.log('[TimeAlarm] 설정 로드:', this.settings);
|
||
|
||
// 설정 로드 후 타이머 재시작
|
||
if (this.settings.enabled) {
|
||
console.log('[TimeAlarm] 설정 로드 후 타이머 재시작');
|
||
this.startWorkTimer();
|
||
} else {
|
||
console.log('[TimeAlarm] 시간 알람 비활성화 - 타이머 중지');
|
||
this.stopAllTimers();
|
||
}
|
||
} catch (error) {
|
||
console.error('[TimeAlarm] 설정 로드 실패:', error);
|
||
}
|
||
}
|
||
|
||
startWorkTimer() {
|
||
if (this.workTimer) {
|
||
clearTimeout(this.workTimer);
|
||
}
|
||
|
||
this.startTime = Date.now();
|
||
const workTimeMs = this.settings.workTime * 60 * 1000;
|
||
|
||
console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}분`);
|
||
|
||
this.workTimer = setTimeout(() => {
|
||
this.showRestModal();
|
||
}, workTimeMs);
|
||
}
|
||
|
||
async showRestModal() {
|
||
try {
|
||
console.log('[TimeAlarm] 휴식 모달 표시');
|
||
|
||
// 휴식 모달 창 열기
|
||
const popup = await chrome.windows.create({
|
||
url: chrome.runtime.getURL('rest-modal.html'),
|
||
type: 'popup',
|
||
width: 500,
|
||
height: 910,
|
||
focused: true
|
||
});
|
||
|
||
// 휴식 시간 타이머 시작
|
||
this.startBreakTimer(popup.id);
|
||
|
||
} catch (error) {
|
||
console.error('[TimeAlarm] 휴식 모달 표시 실패:', error);
|
||
// 폴백: 알림으로 표시
|
||
this.showRestNotification();
|
||
}
|
||
}
|
||
|
||
startBreakTimer(popupId) {
|
||
const breakTimeMs = this.settings.restTime * 60 * 1000;
|
||
|
||
console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}분`);
|
||
|
||
this.breakTimer = setTimeout(async () => {
|
||
try {
|
||
// 팝업 창 닫기
|
||
await chrome.windows.remove(popupId);
|
||
} catch (error) {
|
||
console.log('[TimeAlarm] 팝업 창이 이미 닫혔습니다:', error);
|
||
}
|
||
|
||
// 작업 완료 알림
|
||
this.showWorkCompleteNotification();
|
||
|
||
// 다음 작업 타이머 시작
|
||
this.startWorkTimer();
|
||
|
||
}, breakTimeMs);
|
||
}
|
||
|
||
showRestNotification() {
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '휴식 시간입니다! 🧘♀️',
|
||
message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.`
|
||
});
|
||
}
|
||
|
||
showWorkCompleteNotification() {
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '휴식 완료! 🚀',
|
||
message: '이제 다시 열심히 월매출 1억을 향해 달려가세요! 💪'
|
||
});
|
||
}
|
||
|
||
async updateSettings(newSettings) {
|
||
this.settings = { ...this.settings, ...newSettings };
|
||
|
||
try {
|
||
await chrome.storage.local.set({ time_alarm_settings: this.settings });
|
||
console.log('[TimeAlarm] 설정 업데이트:', this.settings);
|
||
|
||
// 타이머 재시작
|
||
if (this.settings.enabled) {
|
||
this.startWorkTimer();
|
||
} else {
|
||
this.stopAllTimers();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[TimeAlarm] 설정 저장 실패:', error);
|
||
}
|
||
}
|
||
|
||
stopAllTimers() {
|
||
if (this.workTimer) {
|
||
clearTimeout(this.workTimer);
|
||
this.workTimer = null;
|
||
}
|
||
if (this.breakTimer) {
|
||
clearTimeout(this.breakTimer);
|
||
this.breakTimer = null;
|
||
}
|
||
console.log('[TimeAlarm] 모든 타이머 중지');
|
||
}
|
||
|
||
// 현재 타이머 상태 반환
|
||
getTimerStatus() {
|
||
if (!this.settings.enabled) {
|
||
return {
|
||
isRunning: false,
|
||
reason: '시간 알림이 비활성화됨'
|
||
};
|
||
}
|
||
|
||
if (!this.workTimer || !this.startTime) {
|
||
return {
|
||
isRunning: false,
|
||
reason: '작업 타이머가 실행 중이 아님'
|
||
};
|
||
}
|
||
|
||
// 남은 시간 계산
|
||
const now = Date.now();
|
||
const elapsed = now - this.startTime;
|
||
const totalWorkTime = this.settings.workTime * 60 * 1000;
|
||
const remainingTime = totalWorkTime - elapsed;
|
||
|
||
if (remainingTime <= 0) {
|
||
return {
|
||
isRunning: false,
|
||
reason: '작업 시간이 이미 완료됨'
|
||
};
|
||
}
|
||
|
||
return {
|
||
isRunning: true,
|
||
remainingTime: remainingTime,
|
||
workTime: this.settings.workTime,
|
||
restTime: this.settings.restTime,
|
||
startTime: this.startTime,
|
||
elapsed: elapsed
|
||
};
|
||
}
|
||
}
|
||
|
||
// 시간 알람 매니저 인스턴스
|
||
const timeAlarmManager = new TimeAlarmManager();
|
||
|
||
// 찜하기 기능 처리
|
||
async function handleAddToZzim(message, sendResponse) {
|
||
try {
|
||
const config = await getStoredConfig();
|
||
if (!config.ACCESS_TOKEN) {
|
||
throw new Error('로그인이 필요합니다');
|
||
}
|
||
|
||
const SUPABASE_URL = config.SUPABASE_URL || "https://ko.wrmc.cc";
|
||
const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
|
||
|
||
// 사용자 정보 가져오기
|
||
const userInfo = await fetchUserInfo(config.ACCESS_TOKEN);
|
||
if (!userInfo || !userInfo.user_id) {
|
||
throw new Error('사용자 정보를 가져올 수 없습니다');
|
||
}
|
||
|
||
// 찜하기 데이터 준비
|
||
const zzimData = {
|
||
user_id: userInfo.user_id,
|
||
saying_id: message.sayingId,
|
||
saying_text: message.saying,
|
||
category: message.category,
|
||
target: message.target,
|
||
created_at: new Date().toISOString()
|
||
};
|
||
|
||
console.log('[AddToZzim] 찜하기 데이터:', zzimData);
|
||
|
||
// API 호출
|
||
const response = await fetch(`${SUPABASE_URL}/rest/v1/user_zzim_sayings`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'apikey': SUPABASE_ANON_KEY,
|
||
'Authorization': `Bearer ${config.ACCESS_TOKEN}`,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=minimal'
|
||
},
|
||
body: JSON.stringify(zzimData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
throw new Error(`찜하기 실패: ${response.status} - ${errorText}`);
|
||
}
|
||
|
||
console.log('[AddToZzim] 찜하기 성공');
|
||
sendResponse({ success: true });
|
||
|
||
} catch (error) {
|
||
console.error('[AddToZzim] 찜하기 실패:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
}
|
||
}
|
||
|
||
// 설정 정보 가져오기 함수
|
||
async function getStoredConfig() {
|
||
try {
|
||
const result = await chrome.storage.local.get('settings_config');
|
||
return result.settings_config || {};
|
||
} catch (error) {
|
||
console.error('[Config] 설정 정보 로드 실패:', error);
|
||
return {};
|
||
}
|
||
}
|
||
|
||
// 메시지 리스너에 새로운 액션 추가
|
||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||
console.log('[Background] 메시지 수신:', message);
|
||
|
||
// ... existing message handlers ...
|
||
|
||
if (message.action === "addBannedWord") {
|
||
handleAddBannedWord(message, sendResponse);
|
||
return true; // 비동기 응답을 위해 true 반환
|
||
}
|
||
|
||
if (message.action === 'updateTimeAlarmSettings') {
|
||
timeAlarmManager.updateSettings(message.settings);
|
||
sendResponse({ success: true });
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getTimerStatus') {
|
||
const status = timeAlarmManager.getTimerStatus();
|
||
console.log('[Background] 타이머 상태 요청:', status);
|
||
sendResponse({ success: true, ...status });
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'startTimerAfterLogin') {
|
||
console.log('[Background] 로그인 후 타이머 시작 요청 받음');
|
||
timeAlarmManager.init().then(() => {
|
||
console.log('[Background] 로그인 후 타이머 시작 성공');
|
||
sendResponse({ success: true, message: '타이머가 시작되었습니다' });
|
||
}).catch((error) => {
|
||
console.error('[Background] 로그인 후 타이머 시작 실패:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getBackendConfig') {
|
||
console.log('[Background] 백엔드 설정 요청 받음');
|
||
try {
|
||
const config = getBackendConfig();
|
||
console.log('[Background] 백엔드 설정 응답:', config);
|
||
sendResponse({ success: true, config: config });
|
||
} catch (error) {
|
||
console.error('[Background] 백엔드 설정 오류:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
}
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'addToZzim') {
|
||
handleAddToZzim(message, sendResponse);
|
||
return true; // 비동기 응답을 위해 true 반환
|
||
}
|
||
|
||
if (message.action === 'executeBackgroundZzim') {
|
||
handleExecuteBackgroundZzim(message, sendResponse);
|
||
return true; // 비동기 응답을 위해 true 반환
|
||
}
|
||
});
|
||
|
||
// 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거)
|
||
chrome.runtime.onStartup.addListener(() => {
|
||
console.log('[Background] 확장 프로그램 시작됨');
|
||
});
|
||
|
||
chrome.runtime.onInstalled.addListener(() => {
|
||
console.log('[Background] 확장 프로그램 설치/업데이트됨');
|
||
});
|
||
|
||
// 스토리지 변경 감지하여 시간 알람 설정 업데이트
|
||
chrome.storage.onChanged.addListener((changes, namespace) => {
|
||
if (namespace === 'local' && changes.time_alarm_settings) {
|
||
console.log('[Background] 시간 알람 설정 변경 감지:', changes.time_alarm_settings);
|
||
timeAlarmManager.loadSettings();
|
||
}
|
||
});
|
||
|
||
// API 호출량 증가 및 한도 확인 함수
|
||
async function incrementApiCallsAndCheckLimit() {
|
||
try {
|
||
console.log('[background.js] API 호출량 증가 및 한도 확인 시작');
|
||
|
||
// Chrome Storage에서 사용자 정보 가져오기 (popup.js에서 저장한 정보)
|
||
const storageData = await chrome.storage.local.get([
|
||
'access_token',
|
||
'user_id',
|
||
'user_membership_level',
|
||
'user_api_limit',
|
||
'user_current_api_calls'
|
||
]);
|
||
|
||
const token = storageData.access_token;
|
||
const userId = storageData.user_id;
|
||
const membershipLevel = storageData.user_membership_level;
|
||
const apiLimit = storageData.user_api_limit;
|
||
const currentCalls = storageData.user_current_api_calls || 0;
|
||
|
||
console.log('[background.js] Storage에서 가져온 사용자 정보:', {
|
||
userId: userId,
|
||
membershipLevel: membershipLevel,
|
||
currentCalls: currentCalls,
|
||
apiLimit: apiLimit
|
||
});
|
||
|
||
// 필수 정보 확인
|
||
if (!token) {
|
||
console.error('[background.js] 토큰이 없습니다');
|
||
return { success: false, error: '로그인이 필요합니다' };
|
||
}
|
||
|
||
if (!userId) {
|
||
console.error('[background.js] 사용자 ID가 없습니다');
|
||
return { success: false, error: '사용자 정보가 없습니다. 다시 로그인해주세요.' };
|
||
}
|
||
|
||
if (apiLimit === null || apiLimit === undefined) {
|
||
console.error('[background.js] API 한도 정보가 없습니다');
|
||
return { success: false, error: 'API 한도 정보를 확인할 수 없습니다' };
|
||
}
|
||
|
||
console.log('[background.js] API 호출 현황:', {
|
||
current: currentCalls,
|
||
limit: apiLimit,
|
||
remaining: apiLimit - currentCalls
|
||
});
|
||
|
||
// 한도 확인
|
||
if (currentCalls >= apiLimit) {
|
||
console.warn('[background.js] API 호출 한도 초과:', {
|
||
current: currentCalls,
|
||
limit: apiLimit
|
||
});
|
||
return {
|
||
success: false,
|
||
error: `일일 API 호출 한도(${apiLimit}회)를 초과했습니다. 현재 ${currentCalls}회 사용했습니다.`,
|
||
current: currentCalls,
|
||
limit: apiLimit
|
||
};
|
||
}
|
||
|
||
// API 호출량 증가 (데이터베이스 업데이트)
|
||
const incrementResult = await incrementUserApiCalls(userId, token);
|
||
if (!incrementResult.success) {
|
||
console.error('[background.js] API 호출량 증가 실패:', incrementResult.error);
|
||
return { success: false, error: 'API 호출량 업데이트에 실패했습니다' };
|
||
}
|
||
|
||
const newCallCount = incrementResult.newCount;
|
||
|
||
// Chrome Storage의 현재 호출량도 업데이트
|
||
await chrome.storage.local.set({
|
||
user_current_api_calls: newCallCount
|
||
});
|
||
|
||
console.log('[background.js] API 호출량 증가 완료:', {
|
||
previous: currentCalls,
|
||
current: newCallCount,
|
||
limit: apiLimit,
|
||
remaining: apiLimit - newCallCount
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
current: newCallCount,
|
||
limit: apiLimit,
|
||
remaining: apiLimit - newCallCount
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] API 호출량 확인 중 오류:', error);
|
||
return { success: false, error: '시스템 오류가 발생했습니다' };
|
||
}
|
||
}
|
||
|
||
// 멤버십 레벨별 API 한도 가져오기 - 제거됨 (Chrome Storage 사용)
|
||
|
||
// 사용자 API 호출량 증가
|
||
async function incrementUserApiCalls(userId, token) {
|
||
try {
|
||
// 백엔드 설정 가져오기
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
console.log('[background.js] 사용자 API 호출량 증가:', userId);
|
||
|
||
// 먼저 현재 사용자 정보를 조회하여 current_api_calls 값 확인
|
||
const getUserUrl = `${SUPABASE_URL}/rest/v1/users?select=current_api_calls&id=eq.${userId}&limit=1`;
|
||
const getUserRes = await fetch(getUserUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!getUserRes.ok) {
|
||
const errorText = await getUserRes.text();
|
||
console.error('[background.js] 현재 API 호출량 조회 실패:', {
|
||
status: getUserRes.status,
|
||
statusText: getUserRes.statusText,
|
||
error: errorText
|
||
});
|
||
return { success: false, error: `현재 값 조회 실패: ${getUserRes.status}` };
|
||
}
|
||
|
||
const userData = await getUserRes.json();
|
||
if (userData.length === 0) {
|
||
console.error('[background.js] 사용자를 찾을 수 없음:', userId);
|
||
return { success: false, error: '사용자를 찾을 수 없습니다' };
|
||
}
|
||
|
||
const currentCalls = userData[0].current_api_calls || 0;
|
||
const newCalls = currentCalls + 1;
|
||
|
||
console.log('[background.js] API 호출량 업데이트:', {
|
||
userId: userId,
|
||
current: currentCalls,
|
||
new: newCalls
|
||
});
|
||
|
||
// 새로운 값으로 업데이트
|
||
const updateUrl = `${SUPABASE_URL}/rest/v1/users?id=eq.${userId}`;
|
||
const updateRes = await fetch(updateUrl, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=representation'
|
||
},
|
||
body: JSON.stringify({
|
||
current_api_calls: newCalls
|
||
})
|
||
});
|
||
|
||
if (!updateRes.ok) {
|
||
const errorText = await updateRes.text();
|
||
console.error('[background.js] API 호출량 업데이트 실패:', {
|
||
status: updateRes.status,
|
||
statusText: updateRes.statusText,
|
||
error: errorText
|
||
});
|
||
return { success: false, error: `업데이트 실패: ${updateRes.status}` };
|
||
}
|
||
|
||
const updatedData = await updateRes.json();
|
||
console.log('[background.js] API 호출량 업데이트 성공:', updatedData);
|
||
|
||
return { success: true, data: updatedData, newCount: newCalls };
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] API 호출량 증가 중 오류:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 백엔드 설정 중앙 관리
|
||
const BACKEND_CONFIG = {
|
||
SUPABASE_URL: "https://ko.wrmc.cc",
|
||
SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"
|
||
};
|
||
|
||
// 백엔드 설정 가져오기 함수
|
||
function getBackendConfig() {
|
||
return { ...BACKEND_CONFIG };
|
||
}
|
||
|
||
// 검색 결과 개선을 위한 키워드 확장 함수
|
||
|
||
// 백그라운드 찜하기 실행 함수
|
||
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: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
|
||
});
|
||
}
|
||
}
|