// background.js (Service Worker) chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: "searchTrademark", title: "지재권 검색", contexts: ["selection"] }); chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); // 새 어록 감지 알람 생성 (1분마다) chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); // 초기 마지막 확인 시간 설정 chrome.storage.local.set({ lastSayingsCheck: Date.now() }); }); 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] 새 어록 확인 시작"); // 마지막 확인 시간 가져오기 const { lastSayingsCheck } = await chrome.storage.local.get("lastSayingsCheck"); const lastCheckTime = lastSayingsCheck || Date.now() - 60000; // 기본값: 1분 전 // Supabase에서 새 어록 확인 const { access_token } = await chrome.storage.local.get("access_token"); if (!access_token) { console.log("[background.js] 액세스 토큰이 없어 새 어록 확인을 건너뜁니다."); return; } const response = await fetch('https://kbvpvbabvlzjfgcnfxsg.supabase.co/rest/v1/sayings?select=*,sayings_cat(name),sayings_target(name)&created_at=gte.' + new Date(lastCheckTime).toISOString() + '&order=created_at.desc', { method: 'GET', headers: { 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtidnB2YmFidmx6amZnY25meHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzU2NDcxMjEsImV4cCI6MjA1MTIyMzEyMX0.BrPBMGI_zz6-UZpUJGQdJGCFKLEGJBE7CdNLKJgMZNM', '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}개의 새 어록을 발견했습니다.`); // 브라우저 알림 표시 chrome.notifications.create('newSayings', { type: 'basic', iconUrl: 'icon.png', title: '새 어록 알림', message: `${newSayings.length}개의 새로운 어록이 등록되었습니다.`, buttons: [ { title: '확인하기' }, { title: '나중에' } ] }); // 새 어록 데이터를 스토리지에 저장 chrome.storage.local.set({ pendingNewSayings: newSayings, hasNewSayings: true }); } else { console.log("[background.js] 새 어록이 없습니다."); } // 마지막 확인 시간 업데이트 chrome.storage.local.set({ lastSayingsCheck: Date.now() }); } else { console.error("[background.js] 새 어록 확인 실패:", response.status, response.statusText); } } catch (error) { console.error("[background.js] 새 어록 확인 중 오류:", error); } } // 알림 클릭 처리 chrome.notifications.onClicked.addListener((notificationId) => { if (notificationId === 'newSayings') { // 어록 관리 페이지 열기 chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') }); 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((info, tab) => { const keyword = info.selectionText.trim(); if (!keyword) return; const url = buildMarkInfoUrl(keyword); // 1. 키워드 검색: 기존 방식대로 __NUXT_DATA__ 파싱 (재귀적 인덱스 재활용) fetch(url) .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 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) { throw new Error("키워드 검색 결과가 없습니다."); } // 최대 10건까지만 처리 const limitedResults = keywordResults.slice(0, 10); // 각 결과의 출원번호(appNum)로 상세 조회 진행 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; }); }); return Promise.all(detailPromises); }) .then(allResults => { chrome.tabs.sendMessage(tab.id, { action: "showTooltip", detailInfo: allResults, keyword: keyword }); }) .catch(err => { chrome.tabs.sendMessage(tab.id, { action: "showTooltip", detailInfo: { error: err.toString() }, keyword: keyword }); }); }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { const keyword = info.selectionText.trim(); if (!keyword) return; const { access_token } = await chrome.storage.local.get("access_token"); if (!access_token) { chrome.notifications.create({ type: "basic", iconUrl: "icon.png", title: "로그인 필요", message: "기능을 사용하려면 먼저 로그인하세요." }); return; } const response = await fetch("https://your-api.com/translate", { method: "POST", headers: { "Authorization": `Bearer ${access_token}`, "Content-Type": "application/json" }, body: JSON.stringify({ text: keyword }) }); const result = await response.json(); chrome.tabs.sendMessage(tab.id, { action: "showTooltip", detailInfo: result, keyword }); }); // 키워드 검색 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`; } /* ===== 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]"; }