SearchTrademark/background.js

251 lines
9.2 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 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepAlive") {
console.log("[background.js] 서비스 워커 유지 알람 실행됨");
}
});
// 컨텍스트 메뉴 클릭 시 키워드 검색 후 각 결과의 출원번호로 상세 조회 진행
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
});
});
});
// 키워드 검색 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]";
}