// 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 }); // 새 어록 감지 알람 생성 (1분마다) chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); // 초기 마지막 확인 시간 설정 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: () => { return window.getSelection().toString().trim(); } }); const selectedText = results[0]?.result; if (!selectedText) { chrome.notifications.create({ type: 'basic', iconUrl: 'icon.png', title: '텍스트 선택 필요', message: '먼저 텍스트를 선택해주세요.' }); 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); } } catch (error) { console.error(`[background.js] 단축키 처리 중 오류:`, error); chrome.notifications.create({ type: 'basic', iconUrl: 'icon.png', title: '오류 발생', message: '단축키 처리 중 문제가 발생했습니다.' }); } }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "keepAlive") { console.log("[background.js] 서비스 워커 유지 알람 실행됨"); } else if (alarm.name === "checkNewSayings") { checkForNewSayings(); } }); // 새 어록 확인 함수 async function checkForNewSayings() { try { console.log("[background.js] 새 어록 확인 시작"); // Chrome 확장 프로그램 컨텍스트 확인 if (!chrome || !chrome.storage || !chrome.storage.local) { console.error("[background.js] Chrome 확장 프로그램 API에 접근할 수 없습니다."); return; } // 마지막 확인 시간 가져오기 (안전한 방식) let lastSayingsCheck; try { const result = await chrome.storage.local.get("lastSayingsCheck"); lastSayingsCheck = result.lastSayingsCheck; } catch (storageError) { console.error("[background.js] 스토리지 접근 오류:", storageError); lastSayingsCheck = Date.now() - 60000; // 기본값: 1분 전 } const lastCheckTime = lastSayingsCheck || Date.now() - 60000; // 액세스 토큰 가져오기 (안전한 방식) 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 = "http://146.56.101.199:8000"; const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; // 새 어록 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 = "http://146.56.101.199:8000"; const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; // 사용자 기본 정보 가져오기 (토큰 검증) 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 = /]*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 = "http://146.56.101.199:8000"; const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; // 먼저 간단한 테스트 호출 (인증 없이) 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 = /]*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', 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', title: '지원하지 않는 언어', message: '한국어 또는 중국어 텍스트를 선택해주세요.' }); return; } // 선택된 텍스트를 번역된 텍스트로 대체 await chrome.scripting.executeScript({ target: { tabId: tab.id }, function: replaceSelectedText, args: [translatedText] }); chrome.notifications.create({ type: 'basic', title: '번역 완료', message: `${direction}로 변환되었습니다.` }); } catch (error) { console.error('[background.js] 한국어↔중국어 번역 중 오류:', error); chrome.notifications.create({ type: 'basic', title: '번역 오류', message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.' }); } } // 범용 번역 함수 async function translateText(text, sourceLang, targetLang) { const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`); 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]; } // 선택된 텍스트를 새로운 텍스트로 대체하는 함수 (content script에서 실행) function replaceSelectedText(newText) { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); // 선택된 텍스트가 input이나 textarea인지 확인 const activeElement = document.activeElement; if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { // 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; // 변경 이벤트 트리거 activeElement.dispatchEvent(new Event('input', { bubbles: true })); activeElement.dispatchEvent(new Event('change', { bubbles: true })); } else { // 일반 텍스트 노드의 경우 range.deleteContents(); range.insertNode(document.createTextNode(newText)); // 새로운 텍스트 선택 range.setStart(range.startContainer, range.startOffset - newText.length); range.setEnd(range.startContainer, range.startOffset); selection.removeAllRanges(); selection.addRange(range); } } } // ==================== 시간 알람 기능 ==================== class TimeAlarmManager { constructor() { this.workTimer = null; this.breakTimer = null; this.isBreakTime = false; this.settings = { enabled: true, workTime: 60, // 분 restTime: 5, // 분 autoZzim: false }; this.startTime = null; } async init() { console.log('[TimeAlarm] 타이머 초기화 시작'); // 로그인 상태 확인 try { const { access_token } = await chrome.storage.local.get('access_token'); if (!access_token) { console.log('[TimeAlarm] 로그인되지 않음 - 타이머 시작하지 않음'); return; } console.log('[TimeAlarm] 로그인 상태 확인됨'); } catch (error) { console.error('[TimeAlarm] 로그인 상태 확인 실패:', error); return; } await this.loadSettings(); if (this.settings.enabled) { this.startWorkTimer(); console.log('[TimeAlarm] 시간 알람 시작:', this.settings); } else { console.log('[TimeAlarm] 시간 알람이 비활성화되어 있음'); } } async loadSettings() { try { const result = await chrome.storage.local.get('time_alarm_settings'); this.settings = { ...this.settings, ...result.time_alarm_settings }; console.log('[TimeAlarm] 설정 로드:', this.settings); // 설정 로드 후 타이머 재시작 if (this.settings.enabled) { console.log('[TimeAlarm] 설정 로드 후 타이머 재시작'); this.startWorkTimer(); } else { console.log('[TimeAlarm] 시간 알람 비활성화 - 타이머 중지'); this.stopAllTimers(); } } catch (error) { console.error('[TimeAlarm] 설정 로드 실패:', error); } } startWorkTimer() { if (this.workTimer) { clearTimeout(this.workTimer); } this.startTime = Date.now(); const workTimeMs = this.settings.workTime * 60 * 1000; console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}분`); this.workTimer = setTimeout(() => { this.showRestModal(); }, workTimeMs); } async showRestModal() { try { console.log('[TimeAlarm] 휴식 모달 표시'); // 휴식 모달 창 열기 const popup = await chrome.windows.create({ url: chrome.runtime.getURL('rest-modal.html'), type: 'popup', width: 500, height: 910, focused: true }); // 휴식 시간 타이머 시작 this.startBreakTimer(popup.id); } catch (error) { console.error('[TimeAlarm] 휴식 모달 표시 실패:', error); // 폴백: 알림으로 표시 this.showRestNotification(); } } startBreakTimer(popupId) { const breakTimeMs = this.settings.restTime * 60 * 1000; console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}분`); this.breakTimer = setTimeout(async () => { try { // 팝업 창 닫기 await chrome.windows.remove(popupId); } catch (error) { console.log('[TimeAlarm] 팝업 창이 이미 닫혔습니다:', error); } // 작업 완료 알림 this.showWorkCompleteNotification(); // 다음 작업 타이머 시작 this.startWorkTimer(); }, breakTimeMs); } showRestNotification() { chrome.notifications.create({ type: 'basic', iconUrl: 'icon.png', title: '휴식 시간입니다! 🧘‍♀️', message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.` }); } showWorkCompleteNotification() { chrome.notifications.create({ type: 'basic', iconUrl: 'icon.png', title: '휴식 완료! 🚀', message: '이제 다시 열심히 월매출 1억을 향해 달려가세요! 💪' }); } async updateSettings(newSettings) { this.settings = { ...this.settings, ...newSettings }; try { await chrome.storage.local.set({ time_alarm_settings: this.settings }); console.log('[TimeAlarm] 설정 업데이트:', this.settings); // 타이머 재시작 if (this.settings.enabled) { this.startWorkTimer(); } else { this.stopAllTimers(); } } catch (error) { console.error('[TimeAlarm] 설정 저장 실패:', error); } } stopAllTimers() { if (this.workTimer) { clearTimeout(this.workTimer); this.workTimer = null; } if (this.breakTimer) { clearTimeout(this.breakTimer); this.breakTimer = null; } console.log('[TimeAlarm] 모든 타이머 중지'); } // 현재 타이머 상태 반환 getTimerStatus() { if (!this.settings.enabled) { return { isRunning: false, reason: '시간 알림이 비활성화됨' }; } if (!this.workTimer || !this.startTime) { return { isRunning: false, reason: '작업 타이머가 실행 중이 아님' }; } // 남은 시간 계산 const now = Date.now(); const elapsed = now - this.startTime; const totalWorkTime = this.settings.workTime * 60 * 1000; const remainingTime = totalWorkTime - elapsed; if (remainingTime <= 0) { return { isRunning: false, reason: '작업 시간이 이미 완료됨' }; } return { isRunning: true, remainingTime: remainingTime, workTime: this.settings.workTime, restTime: this.settings.restTime, startTime: this.startTime, elapsed: elapsed }; } } // 시간 알람 매니저 인스턴스 const timeAlarmManager = new TimeAlarmManager(); // 찜하기 기능 처리 async function handleAddToZzim(message, sendResponse) { try { const config = await getStoredConfig(); if (!config.ACCESS_TOKEN) { throw new Error('로그인이 필요합니다'); } const SUPABASE_URL = config.SUPABASE_URL || "http://146.56.101.199:8000"; const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; // 사용자 정보 가져오기 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 반환 } }); // 확장 프로그램 시작 시 로그만 출력 (자동 타이머 시작 제거) 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: "http://146.56.101.199:8000", SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" }; // 백엔드 설정 가져오기 함수 function getBackendConfig() { return { ...BACKEND_CONFIG }; } // 검색 결과 개선을 위한 키워드 확장 함수