374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
// 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]";
|
||
}
|