2428 lines
91 KiB
JavaScript
2428 lines
91 KiB
JavaScript
// 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 = /<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) {
|
||
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 = /<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]";
|
||
}
|
||
|
||
// 금지어 추가 메시지 리스너 (중복 제거됨 - 삭제 완료)
|
||
|
||
// 금지어 추가 처리 함수
|
||
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 };
|
||
}
|
||
|
||
// 검색 결과 개선을 위한 키워드 확장 함수
|