4558 lines
168 KiB
JavaScript
4558 lines
168 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 });
|
||
|
||
// 1시간마다 자동 품앗이 찜 알람
|
||
chrome.alarms.create("autoMutualZzim", { periodInMinutes: 60 });
|
||
|
||
// 초기 마지막 확인 시간 설정
|
||
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();
|
||
} else if (alarm.name === "autoMutualZzim") {
|
||
console.log("[background.js] autoMutualZzim 주기 실행");
|
||
chrome.runtime.sendMessage({ action: "autoMutualZzim" });
|
||
}
|
||
});
|
||
|
||
// 새 어록 확인 함수
|
||
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,
|
||
pomodoro: false,
|
||
cycle: 0 // 포모도로 현재 사이클(0~3)
|
||
};
|
||
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 workMin = this.settings.pomodoro ? 35 : this.settings.workTime;
|
||
const workTimeMs = workMin * 60 * 1000;
|
||
|
||
console.log(`[TimeAlarm] 작업 타이머 시작: ${workMin}분`);
|
||
|
||
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 breakTimeMin = this.settings.restTime;
|
||
const breakTimeMs = breakTimeMin * 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();
|
||
|
||
// 포모도로 모드: 사이클 증가 및 리셋
|
||
if (this.settings.pomodoro) {
|
||
this.settings.cycle = (this.settings.cycle + 1) % 4;
|
||
}
|
||
|
||
// 다음 작업 타이머 시작
|
||
this.startWorkTimer();
|
||
|
||
}, breakTimeMs);
|
||
}
|
||
|
||
showRestNotification() {
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '휴식 시간입니다! 🧘♀️',
|
||
message: `${this.settings.pomodoro ? 35 : 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.pomodoro ? 35 : this.settings.workTime) * 60 * 1000;
|
||
const remainingTime = totalWorkTime - elapsed;
|
||
|
||
if (remainingTime <= 0) {
|
||
return {
|
||
isRunning: false,
|
||
reason: '작업 시간이 이미 완료됨'
|
||
};
|
||
}
|
||
|
||
return {
|
||
isRunning: true,
|
||
remainingTime: remainingTime,
|
||
workTime: this.settings.pomodoro ? 35 : 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 반환
|
||
}
|
||
|
||
// ===== 통합 사용자 정보 및 데이터 핸들러 =====
|
||
if (message.action === 'getUserInfo') {
|
||
handleGetUserInfo(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getZzimStats') {
|
||
handleGetZzimStats(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getMyMarkets') {
|
||
handleGetMyMarkets(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
// ===== 타냐대장경(Sayings) 핸들러 =====
|
||
if (message.action === 'getSayingsCategories') {
|
||
handleGetSayingsCategories(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getSayingsTargets') {
|
||
handleGetSayingsTargets(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getSayings') {
|
||
handleGetSayings(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'createSaying') {
|
||
handleCreateSaying(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'updateSaying') {
|
||
handleUpdateSaying(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'deleteSaying') {
|
||
handleDeleteSaying(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
// ===== 휴식 모달 (Rest Modal) 핸들러 =====
|
||
if (message.action === 'getRestActivities') {
|
||
handleGetRestActivities(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getRandomSaying') {
|
||
handleGetRandomSaying(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
// ===== 금지어 관리 (Banned Words) 핸들러 =====
|
||
if (message.action === 'validateToken') {
|
||
handleValidateToken(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getBannedWords') {
|
||
handleGetBannedWords(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'updateBannedWordGrade') {
|
||
handleUpdateBannedWordGrade(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'deleteBannedWord') {
|
||
handleDeleteBannedWord(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getKiprisResults') {
|
||
handleGetKiprisResults(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'addBannedWords') {
|
||
handleAddBannedWords(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
if (message.action === 'getExistingBannedWords') {
|
||
handleGetExistingBannedWords(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return 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 };
|
||
}
|
||
|
||
// 검색 결과 개선을 위한 키워드 확장 함수
|
||
|
||
// 이 함수는 삭제됩니다. content.js를 직접 주입하여 사용하므로 중복 코드가 제거됩니다.
|
||
// zzimScript 제거됨
|
||
async function handleExecuteBackgroundZzim(message, sendResponse) {
|
||
// 위쪽에서 이미 재정의되었습니다. 이 부분은 이전 코드의 잔재를 제거하기 위한 플레이스홀더입니다.
|
||
// 실제로는 위에서 재정의한 handleExecuteBackgroundZzim이 사용됩니다.
|
||
}
|
||
|
||
// 메시지 리스너 추가 - content.js에서 보내는 메시지 처리
|
||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||
console.log('[Background] 메시지 수신:', message);
|
||
|
||
// 지재권 검색 요청
|
||
if (message.action === "searchTrademark") {
|
||
console.log('[Background] 지재권 검색 요청 처리:', message.keyword);
|
||
handleTrademarkSearch(message.keyword, sender.tab)
|
||
.then(() => {
|
||
sendResponse({ success: true });
|
||
})
|
||
.catch(error => {
|
||
console.error('[Background] 지재권 검색 처리 실패:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
});
|
||
return true; // 비동기 응답
|
||
}
|
||
|
||
// 멀티번역 요청
|
||
if (message.action === "translateText") {
|
||
console.log('[Background] 멀티번역 요청 처리:', message.text);
|
||
handleMultiTranslate({ selectionText: message.text })
|
||
.then(() => {
|
||
sendResponse({ success: true });
|
||
})
|
||
.catch(error => {
|
||
console.error('[Background] 멀티번역 처리 실패:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
});
|
||
return true; // 비동기 응답
|
||
}
|
||
|
||
// 직번역 요청
|
||
if (message.action === "handleDirectTranslation") {
|
||
const selectedText = message.selectedText || message.text;
|
||
console.log('[Background] 직번역 요청 처리:', selectedText);
|
||
handleDirectTranslation(selectedText, sender.tab)
|
||
.then(() => {
|
||
sendResponse({ success: true });
|
||
})
|
||
.catch(error => {
|
||
console.error('[Background] 직번역 처리 실패:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
});
|
||
return true; // 비동기 응답
|
||
}
|
||
|
||
// 한중번역 요청
|
||
if (message.action === "handleKoreanToChinese") {
|
||
const selectedText = message.selectedText || message.text;
|
||
console.log('[Background] 한중번역 요청 처리:', selectedText);
|
||
handleKoreanToChinese(selectedText, sender.tab)
|
||
.then(() => {
|
||
sendResponse({ success: true });
|
||
})
|
||
.catch(error => {
|
||
console.error('[Background] 한중번역 처리 실패:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
});
|
||
return true; // 비동기 응답
|
||
}
|
||
|
||
// 금지어 추가 요청
|
||
if (message.action === "addBannedWord") {
|
||
handleAddBannedWord(message, sendResponse);
|
||
return true; // 비동기 응답
|
||
}
|
||
});
|
||
|
||
// 직번역 처리 함수 (선택된 텍스트를 바로 번역된 텍스트로 대체)
|
||
async function handleDirectTranslation(selectedText, tab) {
|
||
if (!selectedText) {
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '텍스트 선택 필요',
|
||
message: '번역할 텍스트를 먼저 선택해주세요.'
|
||
});
|
||
return;
|
||
}
|
||
|
||
console.log('[background.js] 직번역 요청:', selectedText);
|
||
|
||
try {
|
||
// 언어 감지 및 번역 방향 결정
|
||
const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(selectedText);
|
||
const isChinese = /[\u4e00-\u9fff]/.test(selectedText);
|
||
const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(selectedText.trim());
|
||
|
||
let translatedText;
|
||
let direction;
|
||
|
||
if (isKorean && !isChinese) {
|
||
// 한국어 → 중국어
|
||
translatedText = await translateText(selectedText, 'ko', 'zh');
|
||
direction = '한국어 → 중국어';
|
||
} else if (isChinese && !isKorean) {
|
||
// 중국어 → 한국어
|
||
translatedText = await translateText(selectedText, 'zh', 'ko');
|
||
direction = '중국어 → 한국어';
|
||
} else if (isEnglish && !isKorean && !isChinese) {
|
||
// 영어 → 한국어
|
||
translatedText = await translateText(selectedText, 'en', 'ko');
|
||
direction = '영어 → 한국어';
|
||
} else if (isKorean && isChinese) {
|
||
// 한국어와 중국어가 섞여있는 경우 - 한국어 비율로 판단
|
||
const koreanChars = (selectedText.match(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g) || []).length;
|
||
const chineseChars = (selectedText.match(/[\u4e00-\u9fff]/g) || []).length;
|
||
|
||
if (koreanChars >= chineseChars) {
|
||
// 한국어가 더 많으면 중국어로 번역
|
||
translatedText = await translateText(selectedText, 'ko', 'zh');
|
||
direction = '한국어 → 중국어';
|
||
} else {
|
||
// 중국어가 더 많으면 한국어로 번역
|
||
translatedText = await translateText(selectedText, 'zh', 'ko');
|
||
direction = '중국어 → 한국어';
|
||
}
|
||
} else {
|
||
// 기타 언어는 한국어로 번역
|
||
translatedText = await translateText(selectedText, 'auto', 'ko');
|
||
direction = '자동감지 → 한국어';
|
||
}
|
||
|
||
// 선택된 텍스트를 번역된 텍스트로 대체
|
||
console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText);
|
||
|
||
try {
|
||
const result = await chrome.scripting.executeScript({
|
||
target: { tabId: tab.id },
|
||
function: replaceSelectedText,
|
||
args: [translatedText]
|
||
});
|
||
|
||
console.log('[background.js] replaceSelectedText 실행 결과:', result);
|
||
|
||
if (result && result[0] && result[0].result !== undefined) {
|
||
console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result);
|
||
} else {
|
||
console.log('[background.js] 스크립트 실행 완료, 반환값 없음');
|
||
}
|
||
} catch (scriptError) {
|
||
console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError);
|
||
}
|
||
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '직번역 완료',
|
||
message: `${direction}로 번역되어 텍스트가 대체되었습니다.`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('[background.js] 직번역 중 오류:', error);
|
||
chrome.notifications.create({
|
||
type: 'basic',
|
||
iconUrl: 'icon.png',
|
||
title: '직번역 오류',
|
||
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 앱 설치/업데이트 시 알람 등록
|
||
chrome.runtime.onInstalled.addListener(() => {
|
||
console.log('[Background] 확장 프로그램 설치/업데이트됨');
|
||
// 1시간마다 실행되는 알람 등록 (접속 보상용)
|
||
chrome.alarms.create('hourlyReward', { periodInMinutes: 60 });
|
||
});
|
||
|
||
// 알람 이벤트 리스너
|
||
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
||
if (alarm.name === 'hourlyReward') {
|
||
handleHourlyReward();
|
||
}
|
||
});
|
||
|
||
// 시간당 접속 보상 처리 함수
|
||
async function handleHourlyReward() {
|
||
try {
|
||
// 1. 토큰 및 사용자 정보 확인
|
||
const { access_token, user_id, time_reward_state } = await chrome.storage.local.get(['access_token', 'user_id', 'time_reward_state']);
|
||
|
||
if (!access_token || !user_id) {
|
||
console.log('[TimeReward] 사용자 로그인 정보 없음, 보상 건너뜀');
|
||
return;
|
||
}
|
||
|
||
// 2. 오늘 날짜 및 상태 확인
|
||
const today = new Date().toISOString().split('T')[0];
|
||
let currentState = time_reward_state || { date: today, count: 0 };
|
||
|
||
// 날짜가 바뀌었으면 초기화
|
||
if (currentState.date !== today) {
|
||
currentState = { date: today, count: 0 };
|
||
}
|
||
|
||
// 3. 일일 제한 확인 (하루 최대 10회 = 10시간)
|
||
if (currentState.count >= 10) {
|
||
console.log(`[TimeReward] 오늘 접속 보상 한도 도달 (${currentState.count}/10회)`);
|
||
return;
|
||
}
|
||
|
||
// 4. 보상 지급 (2 마일리지)
|
||
const REWARD_AMOUNT = 2;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 현재 마일리지 조회
|
||
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=available_zzim_mile`, {
|
||
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
|
||
});
|
||
|
||
if (userRes.ok) {
|
||
const userData = (await userRes.json())[0];
|
||
const newMileage = (userData.available_zzim_mile || 0) + REWARD_AMOUNT;
|
||
|
||
// 마일리지 업데이트
|
||
const updateRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
apikey: SUPABASE_ANON_KEY,
|
||
Authorization: `Bearer ${access_token}`,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=minimal'
|
||
},
|
||
body: JSON.stringify({ available_zzim_mile: newMileage })
|
||
});
|
||
|
||
if (updateRes.ok) {
|
||
// 5. 상태 업데이트 및 저장
|
||
currentState.count += 1;
|
||
await chrome.storage.local.set({ time_reward_state: currentState });
|
||
console.log(`[TimeReward] 접속 보상 지급 완료: +${REWARD_AMOUNT}마일리지 (오늘 ${currentState.count}/10회)`);
|
||
} else {
|
||
console.error('[TimeReward] 마일리지 업데이트 실패');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[TimeReward] 처리 중 오류:', error);
|
||
}
|
||
}
|
||
|
||
// ===== 메시지 리스너: 통계 업데이트 및 백그라운드 찜 =====
|
||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||
if (message && message.action === 'autoMutualZzim') {
|
||
// 메시지에서 delay 등 옵션 전달
|
||
autoMutualZzim({ delay: message.delay })
|
||
.then(() => sendResponse({ success: true }))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
|
||
// 찜하기 완료 메시지 (탭 닫기 등 후처리)
|
||
if (message.action === 'zzimComplete') {
|
||
console.log('[Background] 찜하기 완료 메시지 수신, 탭 닫기 시도', sender);
|
||
if (sender.tab && sender.tab.id) {
|
||
setTimeout(() => {
|
||
chrome.tabs.remove(sender.tab.id).catch(e => console.log('탭 닫기 실패(이미 닫힘?):', e));
|
||
}, 3000); // 사용자가 결과를 볼 수 있게 3초 대기 후 닫기
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 찜 통계 업데이트 (content.js 완료 후 호출)
|
||
if (message.action === 'updateZzimStats') {
|
||
handleUpdateZzimStats(message)
|
||
.then(result => sendResponse(result))
|
||
.catch(err => sendResponse({ success: false, error: err.message }));
|
||
return true;
|
||
}
|
||
});
|
||
|
||
// 찜 통계 업데이트 처리 (Supabase)
|
||
async function handleUpdateZzimStats(message) {
|
||
try {
|
||
console.log('[Background] 찜 통계 업데이트 요청:', message);
|
||
const { count, zzimType } = message;
|
||
|
||
if (!count || count <= 0) {
|
||
console.log('[Background] 찜 개수가 0이므로 통계 업데이트 생략');
|
||
return { success: true, skipped: true };
|
||
}
|
||
|
||
// 토큰 및 사용자 정보 가져오기
|
||
const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
|
||
if (!access_token || !user_id) {
|
||
throw new Error('사용자 인증 정보가 없습니다.');
|
||
}
|
||
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
const today = new Date().toISOString().split('T')[0];
|
||
|
||
// 1. 내 마켓 찜하기: 오늘 찜한 개수 + 내 마켓 총 찜 개수 증가
|
||
if (zzimType === 'my_market' || !zzimType) { // 기본값은 내 마켓
|
||
console.log(`[Background] 내 마켓 통계 업데이트: +${count}`);
|
||
|
||
// users 테이블 조회
|
||
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=today_zzim_count,my_zzim,today_zzim_date`, {
|
||
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
|
||
});
|
||
|
||
if (!userRes.ok) throw new Error('사용자 정보 조회 실패');
|
||
const userData = (await userRes.json())[0];
|
||
|
||
let newTodayCount = (userData.today_zzim_date === today ? userData.today_zzim_count : 0) + count;
|
||
let newMyZzim = (userData.my_zzim || 0) + count;
|
||
|
||
const updateRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
apikey: SUPABASE_ANON_KEY,
|
||
Authorization: `Bearer ${access_token}`,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=minimal'
|
||
},
|
||
body: JSON.stringify({
|
||
today_zzim_count: newTodayCount,
|
||
today_zzim_date: today,
|
||
my_zzim: newMyZzim
|
||
})
|
||
});
|
||
|
||
if (!updateRes.ok) throw new Error('통계 업데이트 실패');
|
||
|
||
} else if (zzimType === 'mutual') {
|
||
// 2. 품앗이(상대방) 찜하기: 내 마일리지 증가(보상) + 상대방 마일리지 차감(비용)
|
||
console.log(`[Background] 품앗이 통계 업데이트: +${count}`);
|
||
|
||
// 2-1. 내 마일리지 증가 (Worker)
|
||
// 시스템 보너스 적용: 품앗이한 갯수의 50% 추가 지급 (총 150% 보상)
|
||
const bonus = Math.ceil(count * 0.5);
|
||
const totalReward = count + bonus;
|
||
|
||
console.log(`[Background] 품앗이 보상: 기본 ${count} + 보너스 ${bonus} (50%) = 총 ${totalReward} 마일리지`);
|
||
|
||
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=today_zzim_count,today_zzim_date,available_zzim_mile`, {
|
||
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
|
||
});
|
||
|
||
if (!userRes.ok) throw new Error('사용자 정보 조회 실패');
|
||
const userData = (await userRes.json())[0];
|
||
|
||
let newTodayCount = (userData.today_zzim_date === today ? userData.today_zzim_count : 0) + count;
|
||
let newAvailableMile = (userData.available_zzim_mile || 0) + totalReward; // 보너스 포함 보상 지급
|
||
|
||
await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
apikey: SUPABASE_ANON_KEY,
|
||
Authorization: `Bearer ${access_token}`,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=minimal'
|
||
},
|
||
body: JSON.stringify({
|
||
today_zzim_count: newTodayCount,
|
||
today_zzim_date: today,
|
||
available_zzim_mile: newAvailableMile
|
||
})
|
||
});
|
||
|
||
// 2-2. 상대방 마일리지 차감 (Owner) - target_user_id 필요
|
||
if (message.market && message.market.owner_user_id) {
|
||
const targetUserId = message.market.owner_user_id;
|
||
|
||
// 상대방 현재 마일리지 조회 (RPC 없이 직접 처리 시 동시성 문제 가능성 있음)
|
||
const targetRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${targetUserId}&select=available_zzim_mile`, {
|
||
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
|
||
});
|
||
|
||
if (targetRes.ok) {
|
||
const targetData = (await targetRes.json())[0];
|
||
let targetNewMile = (targetData.available_zzim_mile || 0) - count;
|
||
if (targetNewMile < 0) targetNewMile = 0; // 0 미만 방지
|
||
|
||
await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${targetUserId}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
apikey: SUPABASE_ANON_KEY,
|
||
Authorization: `Bearer ${access_token}`,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=minimal'
|
||
},
|
||
body: JSON.stringify({
|
||
available_zzim_mile: targetNewMile
|
||
})
|
||
});
|
||
|
||
console.log(`[Background] 상대방(${targetUserId}) 마일리지 차감 완료: -${count}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
return { success: true, updated: count };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 통계 업데이트 오류:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 자동 품앗이 실행 함수 (휴식 시간 또는 주기적 실행용)
|
||
async function autoMutualZzim(options = {}) {
|
||
try {
|
||
const { delay = 300 } = options;
|
||
console.log('[Background] autoMutualZzim 실행, delay:', delay);
|
||
|
||
// 1. 사용자 정보 가져오기
|
||
const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
|
||
if (!access_token || !user_id) {
|
||
console.log('[Background] 로그인 정보 없음, 품앗이 중단');
|
||
return;
|
||
}
|
||
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 2. 품앗이 대상 마켓 가져오기 (랜덤 또는 순차)
|
||
// 자신의 마켓은 제외
|
||
const response = await fetch(`${SUPABASE_URL}/rest/v1/user_markets?user_id=neq.${user_id}&for_mutual_zzim=eq.true&limit=10`, {
|
||
headers: {
|
||
apikey: SUPABASE_ANON_KEY,
|
||
Authorization: `Bearer ${access_token}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) throw new Error('품앗이 마켓 목록 로드 실패');
|
||
|
||
const markets = await response.json();
|
||
if (markets.length === 0) {
|
||
console.log('[Background] 품앗이 대상 마켓이 없습니다.');
|
||
return;
|
||
}
|
||
|
||
// 3. 랜덤하게 하나 선택하여 실행
|
||
const randomMarket = markets[Math.floor(Math.random() * markets.length)];
|
||
console.log('[Background] 품앗이 대상 선택:', randomMarket.market_nickname);
|
||
|
||
// 4. 백그라운드 찜하기 실행
|
||
// handleExecuteBackgroundZzim 호출
|
||
await handleExecuteBackgroundZzim({
|
||
market: randomMarket,
|
||
zzimType: 'mutual',
|
||
settings: {
|
||
maxZzim: 50, // 한 번에 최대 50개
|
||
maxZzimMileage: 5000,
|
||
totalDelay: delay // 지연 시간 전달
|
||
},
|
||
delay: delay // 명시적 전달
|
||
}, () => {}); // 콜백 더미
|
||
|
||
} catch (error) {
|
||
console.error('[Background] autoMutualZzim 실행 오류:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 백그라운드 찜하기 실행 함수 (개선됨: content.js 재사용)
|
||
async function handleExecuteBackgroundZzim(message, sendResponse) {
|
||
try {
|
||
console.log('[Background] 백그라운드 찜하기 시작:', message);
|
||
const { market, zzimType } = message;
|
||
|
||
if (!market || !market.market_url) throw new Error('마켓 정보가 없습니다.');
|
||
|
||
// 1. 백그라운드 탭 생성 (활성화하지 않음)
|
||
const tab = await chrome.tabs.create({
|
||
url: market.target_url || market.market_url, // 이미 파라미터가 붙은 URL 사용
|
||
active: false
|
||
});
|
||
|
||
console.log('[Background] 탭 생성됨:', tab.id);
|
||
|
||
// 2. 페이지 로드 대기 및 스크립트 주입
|
||
const listener = async (tabId, changeInfo) => {
|
||
if (tabId === tab.id && changeInfo.status === 'complete') {
|
||
chrome.tabs.onUpdated.removeListener(listener);
|
||
|
||
try {
|
||
console.log('[Background] 페이지 로드 완료, content.js 주입');
|
||
|
||
// content.js 주입 (이미 manifest에 의해 주입되었을 수도 있지만 확실히 하기 위해)
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId: tab.id },
|
||
files: ['content.js']
|
||
});
|
||
|
||
// 팝업 차단 스크립트 강제 주입 (메인 월드)
|
||
await chrome.scripting.executeScript({
|
||
target: { tabId: tab.id },
|
||
world: 'MAIN',
|
||
func: () => {
|
||
window.alert = function(msg) { console.log('[AutoZzim-BG] Alert blocked:', msg); return true; };
|
||
window.confirm = function(msg) { console.log('[AutoZzim-BG] Confirm blocked:', msg); return true; };
|
||
window.prompt = function(msg) { console.log('[AutoZzim-BG] Prompt blocked:', msg); return null; };
|
||
}
|
||
});
|
||
|
||
// 약간 대기 후 찜하기 시작 메시지 전송
|
||
setTimeout(() => {
|
||
chrome.tabs.sendMessage(tab.id, {
|
||
action: "startAutoZzim",
|
||
maxZzim: message.settings?.maxZzim || 50,
|
||
delay: message.delay || 300,
|
||
zzimType: zzimType || 'my_market', // 타입 전달
|
||
market: message.market // 마켓 정보 전달 (owner_user_id 포함)
|
||
}).catch(e => console.log('메시지 전송 실패(무시):', e));
|
||
}, 2000);
|
||
|
||
} catch (e) {
|
||
console.error('스크립트 실행 오류:', e);
|
||
chrome.tabs.remove(tab.id);
|
||
}
|
||
}
|
||
};
|
||
|
||
chrome.tabs.onUpdated.addListener(listener);
|
||
|
||
// 응답은 비동기로 처리됨 (content.js가 완료 후 updateZzimStats 메시지를 보낼 것임)
|
||
sendResponse({ success: true, message: '백그라운드 작업이 시작되었습니다.' });
|
||
|
||
// 안전장치: 3분 후 탭 강제 종료
|
||
setTimeout(() => {
|
||
chrome.tabs.get(tab.id, (t) => {
|
||
if (t) chrome.tabs.remove(tab.id);
|
||
});
|
||
}, 180000);
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 실행 오류:', error);
|
||
sendResponse({ success: false, error: error.message });
|
||
}
|
||
}
|
||
|
||
// 사용자 정보 조회 핸들러
|
||
async function handleGetUserInfo(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
if (!token) throw new Error('토큰이 필요합니다.');
|
||
|
||
// 1. Auth API로 기본 정보 조회 (토큰 검증 포함)
|
||
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) throw new Error('토큰이 유효하지 않습니다.');
|
||
const authUser = await authRes.json();
|
||
|
||
// 2. Users 테이블 상세 정보 조회
|
||
// membership_levels 조인
|
||
const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*,membership_levels(level,api_call_limit)&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
|
||
const detailsRes = await fetch(detailsUrl, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
let userDetails = {};
|
||
if (detailsRes.ok) {
|
||
const data = await detailsRes.json();
|
||
if (data.length > 0) userDetails = data[0];
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
user: { ...authUser, ...userDetails }
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 사용자 정보 조회 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 찜 통계 조회 핸들러
|
||
async function handleGetZzimStats(message) {
|
||
try {
|
||
const { userId, token } = message;
|
||
if (!userId || !token) throw new Error('필수 정보 누락');
|
||
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 1. 사용자 통계
|
||
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${userId}&select=my_zzim,zzim_mile,available_zzim_mile,membership_level,today_zzim_count,today_zzim_date`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!userRes.ok) throw new Error('통계 조회 실패');
|
||
const userData = (await userRes.json())[0];
|
||
|
||
// 2. 멤버십 레벨 정보
|
||
const membershipLevel = userData.membership_level || 'basic';
|
||
const limitsRes = await fetch(`${SUPABASE_URL}/rest/v1/membership_levels?level=eq.${membershipLevel}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
// 기본값 상향 조정 (Basic: 300, Premium: 600, VIP: 1000)
|
||
let limits = { daily_zzim_limit: 300, max_zzim_mileage: 5000, mileage_per_zzim: 1, max_markets: 20, mutual_zzim_enabled: true, background_zzim_enabled: true };
|
||
if (limitsRes.ok) {
|
||
const limitsData = await limitsRes.json();
|
||
if (limitsData.length > 0) {
|
||
limits = { ...limits, ...limitsData[0] };
|
||
|
||
// DB값이 기존 낮은 값일 경우를 대비해 강제 상향 (임시 로직)
|
||
// 실제로는 DB membership_levels 테이블 값을 업데이트해야 함.
|
||
// 여기서는 기본값만 수정
|
||
if (limits.daily_zzim_limit === 100) limits.daily_zzim_limit = 300;
|
||
if (limits.daily_zzim_limit === 250) limits.daily_zzim_limit = 600;
|
||
if (limits.daily_zzim_limit === 500) limits.daily_zzim_limit = 1000;
|
||
}
|
||
}
|
||
|
||
// 사용자 편의를 위해 백그라운드 모드는 항상 활성화 (등급과 무관하게)
|
||
limits.background_zzim_enabled = true;
|
||
|
||
// 3. 오늘 날짜 체크 및 리셋 로직 (Background에서 처리)
|
||
const today = new Date().toISOString().split('T')[0];
|
||
if (userData.today_zzim_date !== today) {
|
||
// 날짜가 다르면 리셋 (비동기로 처리하고 현재는 0으로 반환)
|
||
fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${userId}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ today_zzim_count: 0, today_zzim_date: today })
|
||
}).catch(e => console.error('일일 리셋 실패:', e));
|
||
|
||
userData.today_zzim_count = 0;
|
||
}
|
||
|
||
return { success: true, stats: userData, limits };
|
||
|
||
} catch (error) {
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 마켓 목록 조회 핸들러
|
||
async function handleGetMyMarkets(message) {
|
||
try {
|
||
const { userId, token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${userId}&select=my_markets`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) throw new Error('마켓 조회 실패');
|
||
const data = await res.json();
|
||
const markets = data.length > 0 ? (data[0].my_markets || []) : [];
|
||
|
||
return { success: true, markets };
|
||
|
||
} catch (error) {
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// ===== 타냐대장경(Sayings) API 핸들러 =====
|
||
|
||
// 타냐대장경 카테고리 목록 조회
|
||
async function handleGetSayingsCategories(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/sayings_cat?select=saying_cat&order=saying_cat.asc`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`카테고리 로드 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
const categories = await res.json();
|
||
console.log('[Background] 카테고리 로드 성공:', categories.length);
|
||
return { success: true, categories };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 카테고리 로드 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 타냐대장경 타겟 목록 조회
|
||
async function handleGetSayingsTargets(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/sayings_target?select=target&order=target.asc`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`타겟 로드 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
const targets = await res.json();
|
||
console.log('[Background] 타겟 로드 성공:', targets.length);
|
||
return { success: true, targets };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 타겟 로드 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 타냐대장경 목록 조회
|
||
async function handleGetSayings(message) {
|
||
try {
|
||
const { token, adminApproval = true } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
let query = `${SUPABASE_URL}/rest/v1/tanya_sayings?order=created_at.desc`;
|
||
if (adminApproval) {
|
||
query += '&admin_approval=eq.true';
|
||
}
|
||
|
||
const res = await fetch(query, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`타냐대장경 로드 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
const sayings = await res.json();
|
||
console.log('[Background] 타냐대장경 로드 성공:', sayings.length);
|
||
return { success: true, sayings };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 타냐대장경 로드 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 타냐대장경 등록
|
||
async function handleCreateSaying(message) {
|
||
try {
|
||
const { token, sayingData } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings`, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=representation'
|
||
},
|
||
body: JSON.stringify(sayingData)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`타냐대장경 등록 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
const result = await res.json();
|
||
console.log('[Background] 타냐대장경 등록 성공');
|
||
return { success: true, saying: result };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 타냐대장경 등록 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 타냐대장경 수정
|
||
async function handleUpdateSaying(message) {
|
||
try {
|
||
const { token, sayingId, sayingData } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings?id=eq.${sayingId}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json',
|
||
'Prefer': 'return=representation'
|
||
},
|
||
body: JSON.stringify(sayingData)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`타냐대장경 수정 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
const result = await res.json();
|
||
console.log('[Background] 타냐대장경 수정 성공');
|
||
return { success: true, saying: result };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 타냐대장경 수정 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 타냐대장경 삭제
|
||
async function handleDeleteSaying(message) {
|
||
try {
|
||
const { token, sayingId } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings?id=eq.${sayingId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`타냐대장경 삭제 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
console.log('[Background] 타냐대장경 삭제 성공');
|
||
return { success: true };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 타냐대장경 삭제 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// ===== 휴식 모달 (Rest Modal) 핸들러 =====
|
||
|
||
// 휴식 활동 목록 조회
|
||
async function handleGetRestActivities(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/events?select=message&event_type=eq.rest_time&order=created_at.desc&limit=50`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`휴식 활동 조회 실패: ${res.status}`);
|
||
}
|
||
|
||
const events = await res.json();
|
||
console.log('[Background] 휴식 활동 조회 성공:', events.length);
|
||
return { success: true, activities: events };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 휴식 활동 조회 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 랜덤 어록 조회
|
||
async function handleGetRandomSaying(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 최근 1달 이내의 승인된 어록 가져오기
|
||
const oneMonthAgo = new Date();
|
||
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${oneMonthAgo.toISOString()}&admin_approval=eq.true&order=created_at.desc&limit=50`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`어록 조회 실패: ${res.status}`);
|
||
}
|
||
|
||
const sayings = await res.json();
|
||
console.log('[Background] 랜덤 어록 조회 성공:', sayings.length);
|
||
return { success: true, sayings: sayings };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 랜덤 어록 조회 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// ===== 금지어 관리 (Banned Words) 핸들러 =====
|
||
|
||
// 토큰 유효성 검증
|
||
async function handleValidateToken(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`토큰 검증 실패: ${res.status}`);
|
||
}
|
||
|
||
const userData = await res.json();
|
||
console.log('[Background] 토큰 검증 성공:', userData.email);
|
||
return { success: true, user: userData };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 토큰 검증 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 금지어 목록 조회
|
||
async function handleGetBannedWords(message) {
|
||
try {
|
||
const { token } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
// 1. 사용자 정보 가져오기
|
||
const authRes = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!authRes.ok) {
|
||
throw new Error(`사용자 인증 실패: ${authRes.status}`);
|
||
}
|
||
|
||
const authUser = await authRes.json();
|
||
|
||
// 2. 사용자 ID 가져오기
|
||
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!userRes.ok) {
|
||
throw new Error(`사용자 조회 실패: ${userRes.status}`);
|
||
}
|
||
|
||
const userData = await userRes.json();
|
||
if (!userData || userData.length === 0) {
|
||
throw new Error('사용자 데이터를 찾을 수 없습니다');
|
||
}
|
||
|
||
const userId = userData[0].id;
|
||
|
||
// 3. 금지어 목록 가져오기
|
||
const bannedRes = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!bannedRes.ok) {
|
||
throw new Error(`금지어 목록 조회 실패: ${bannedRes.status}`);
|
||
}
|
||
|
||
const bannedWords = await bannedRes.json();
|
||
console.log('[Background] 금지어 목록 조회 성공:', bannedWords.length);
|
||
return { success: true, bannedWords: bannedWords, userId: userId };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 금지어 목록 조회 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 금지어 등급 수정
|
||
async function handleUpdateBannedWordGrade(message) {
|
||
try {
|
||
const { token, wordId, grade } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
grade: grade,
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`등급 수정 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
console.log('[Background] 금지어 등급 수정 성공');
|
||
return { success: true };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 금지어 등급 수정 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 금지어 삭제
|
||
async function handleDeleteBannedWord(message) {
|
||
try {
|
||
const { token, wordId } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`금지어 삭제 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
console.log('[Background] 금지어 삭제 성공');
|
||
return { success: true };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 금지어 삭제 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 키프리스 결과 조회
|
||
async function handleGetKiprisResults(message) {
|
||
try {
|
||
const { token, wordId } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordId}&order=created_at.desc`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`키프리스 결과 조회 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
const kiprisData = await res.json();
|
||
console.log('[Background] 키프리스 결과 조회 성공:', kiprisData.length);
|
||
return { success: true, kiprisData: kiprisData };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 키프리스 결과 조회 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 금지어 추가 (배치)
|
||
async function handleAddBannedWords(message) {
|
||
try {
|
||
const { token, userId, wordsData } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words`, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(wordsData)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
throw new Error(`금지어 추가 실패: ${res.status} - ${errorText}`);
|
||
}
|
||
|
||
console.log('[Background] 금지어 추가 성공:', wordsData.length);
|
||
return { success: true };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 금지어 추가 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 기존 금지어 목록 조회 (중복 체크용)
|
||
async function handleGetExistingBannedWords(message) {
|
||
try {
|
||
const { token, userId } = message;
|
||
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
|
||
|
||
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?select=banned_word&user_id=eq.${userId}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
apikey: SUPABASE_ANON_KEY,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`기존 금지어 조회 실패: ${res.status}`);
|
||
}
|
||
|
||
const existingWords = await res.json();
|
||
console.log('[Background] 기존 금지어 조회 성공:', existingWords.length);
|
||
return { success: true, existingWords: existingWords };
|
||
|
||
} catch (error) {
|
||
console.error('[Background] 기존 금지어 조회 실패:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|