SearchTrademark/background.js

374 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = /<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) {
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 = /<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]";
}