SearchTrademark/background.js

4558 lines
168 KiB
JavaScript
Raw Permalink 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: "지재권검색 (Ctrl+Shift+S)",
contexts: ["selection"]
});
chrome.contextMenus.create({
id: "multiTranslate",
title: "멀티번역 (Ctrl+Shift+E)",
contexts: ["selection"]
});
chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
// 새 어록 감지 알람 생성 (5분마다)
chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 });
// 1시간마다 자동 품앗이 찜 알람
chrome.alarms.create("autoMutualZzim", { periodInMinutes: 60 });
// 초기 마지막 확인 시간 설정
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: () => {
// 다양한 방식으로 선택된 텍스트 감지
function getSelectedTextAdvanced() {
let selectedText = '';
console.log('[텍스트선택] 선택된 텍스트 감지 시작');
// 1. 기본 window.getSelection() 시도
const selection = window.getSelection();
if (selection && selection.toString().trim()) {
selectedText = selection.toString().trim();
console.log('[텍스트선택] window.getSelection()으로 감지:', selectedText);
return selectedText;
}
// 2. document.getSelection() 시도
if (document.getSelection) {
const docSelection = document.getSelection();
if (docSelection && docSelection.toString().trim()) {
selectedText = docSelection.toString().trim();
console.log('[텍스트선택] document.getSelection()으로 감지:', selectedText);
return selectedText;
}
}
// 3. activeElement에서 선택된 텍스트 확인 (input, textarea)
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
const start = activeElement.selectionStart;
const end = activeElement.selectionEnd;
if (start !== end && start !== null && end !== null) {
selectedText = activeElement.value.substring(start, end).trim();
console.log('[텍스트선택] activeElement에서 감지:', selectedText);
return selectedText;
}
}
// 4. contentEditable 요소에서 선택된 텍스트 확인
const editableElements = document.querySelectorAll('[contenteditable="true"]');
for (const element of editableElements) {
const elementSelection = element.ownerDocument.getSelection();
if (elementSelection && elementSelection.toString().trim()) {
selectedText = elementSelection.toString().trim();
console.log('[텍스트선택] contentEditable에서 감지:', selectedText);
return selectedText;
}
}
// 5. iframe 내부 확인
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const iframeSelection = iframeDoc.getSelection();
if (iframeSelection && iframeSelection.toString().trim()) {
selectedText = iframeSelection.toString().trim();
console.log('[텍스트선택] iframe에서 감지:', selectedText);
return selectedText;
}
} catch (e) {
// 크로스 오리진 iframe은 접근 불가
console.log('[텍스트선택] iframe 접근 제한:', e.message);
}
}
// 6. Shadow DOM 확인
function checkShadowDom(element) {
if (element.shadowRoot) {
const shadowSelection = element.shadowRoot.getSelection();
if (shadowSelection && shadowSelection.toString().trim()) {
return shadowSelection.toString().trim();
}
}
for (const child of element.children) {
const result = checkShadowDom(child);
if (result) return result;
}
return null;
}
const shadowResult = checkShadowDom(document.body);
if (shadowResult) {
console.log('[텍스트선택] Shadow DOM에서 감지:', shadowResult);
return shadowResult;
}
// 7. 특정 컨테이너 내부 확인 (ice-container 등)
const containers = document.querySelectorAll('.ice-container, [class*="container"], [class*="chat"], [class*="message"], [class*="dialog"], [class*="modal"], [class*="content"], [data-testid*="chat"], [data-testid*="message"], [role="textbox"], [contenteditable]');
for (const container of containers) {
try {
const containerSelection = container.ownerDocument.getSelection();
if (containerSelection && containerSelection.toString().trim()) {
// 선택 범위가 해당 컨테이너 내부인지 확인
const range = containerSelection.getRangeAt(0);
if (range && (container.contains(range.commonAncestorContainer) ||
container.contains(range.startContainer) ||
container.contains(range.endContainer))) {
selectedText = containerSelection.toString().trim();
console.log('[텍스트선택] 특정 컨테이너에서 감지:', selectedText);
return selectedText;
}
}
} catch (e) {
console.log('[텍스트선택] 컨테이너 확인 오류:', e.message);
}
}
// 8. 모든 요소에서 선택 상태 확인 (최후의 수단)
const allElements = document.querySelectorAll('*');
for (const element of allElements) {
try {
// 요소가 포커스되어 있고 선택된 텍스트가 있는지 확인
if (element === document.activeElement || element.contains(document.activeElement)) {
const computedStyle = window.getComputedStyle(element);
if (computedStyle.userSelect !== 'none') {
const elementText = element.textContent || element.innerText;
if (elementText && elementText.trim()) {
// 마우스로 선택된 영역이 이 요소 내부에 있는지 확인
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
if (element.contains(range.commonAncestorContainer)) {
selectedText = selection.toString().trim();
if (selectedText) {
console.log('[텍스트선택] 활성 요소에서 감지:', selectedText);
return selectedText;
}
}
}
}
}
}
} catch (e) {
// 무시
}
}
// 9. 클립보드에서 최근 복사된 텍스트 확인 (사용자가 Ctrl+C를 눌렀을 가능성)
try {
// 참고: 보안상의 이유로 클립보드 읽기는 제한적임
console.log('[텍스트선택] 클립보드 확인은 보안 제약으로 제한됨');
} catch (e) {
console.log('[텍스트선택] 클립보드 접근 불가:', e.message);
}
console.log('[텍스트선택] 선택된 텍스트를 찾을 수 없음');
return '';
}
return getSelectedTextAdvanced();
}
});
const selectedText = results[0]?.result;
if (!selectedText) {
// 클립보드에서 텍스트 가져오기 시도
try {
const clipboardText = await navigator.clipboard.readText();
if (clipboardText && clipboardText.trim() && clipboardText.length < 1000) {
console.log('[단축키] 클립보드에서 텍스트 사용:', clipboardText.substring(0, 50));
// 클립보드 텍스트로 처리 계속
await ensureContentScriptReady(tab.id);
if (command === 'trademark-search') {
try {
await chrome.tabs.sendMessage(tab.id, {
action: "showLoading",
message: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 지재권 검색 중... (클립보드 사용)`
});
} catch (loadingError) {
console.log('[단축키-지재권검색] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
}
await handleTrademarkSearch(clipboardText.trim(), 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: `🔄 "${clipboardText.substring(0, 20)}${clipboardText.length > 20 ? '...' : ''}" 번역 중... (클립보드 사용)`
});
} catch (loadingError) {
console.log('[단축키-멀티번역] 로딩 인디케이터 표시 실패 (무시):', loadingError.message);
}
await handleMultiTranslate({ selectionText: clipboardText.trim() });
try {
await chrome.tabs.sendMessage(tab.id, { action: "hideLoading" });
} catch (e) {
console.log('[단축키-멀티번역] 로딩 인디케이터 제거 실패 (무시):', e.message);
}
} else if (command === 'korean-to-chinese') {
await handleKoreanToChinese(clipboardText.trim(), tab);
} else if (command === 'direct-translate') {
await handleDirectTranslation(clipboardText.trim(), tab);
}
return; // 클립보드 처리 완료
}
} catch (clipboardError) {
console.log('[단축키] 클립보드 읽기 실패:', clipboardError.message);
}
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '텍스트 선택 필요',
message: 'AliWangWang 등의 앱에서는 텍스트를 정확히 드래그 선택 후 단축키를 눌러주세요. 선택이 안 되면 Ctrl+C로 복사 후 다시 시도해보세요.'
});
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);
} else if (command === 'direct-translate') {
await handleDirectTranslation(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();
} else if (alarm.name === "autoMutualZzim") {
console.log("[background.js] autoMutualZzim 주기 실행");
chrome.runtime.sendMessage({ action: "autoMutualZzim" });
}
});
// 새 어록 확인 함수
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() - 300000; // 기본값: 1분 전
}
const lastCheckTime = lastSayingsCheck || Date.now() - 300000;
// 액세스 토큰 가져오기 (안전한 방식)
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 = "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 새 어록 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 = "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 사용자 기본 정보 가져오기 (토큰 검증)
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 = "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 먼저 간단한 테스트 호출 (인증 없이)
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',
iconUrl: 'icon.png',
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',
iconUrl: 'icon.png',
title: '지원하지 않는 언어',
message: '한국어 또는 중국어 텍스트를 선택해주세요.'
});
return;
}
// 선택된 텍스트를 번역된 텍스트로 대체
console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText);
try {
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
function: replaceSelectedText,
args: [translatedText]
});
console.log('[background.js] replaceSelectedText 실행 결과:', result);
if (result && result[0] && result[0].result !== undefined) {
console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result);
} else {
console.log('[background.js] 스크립트 실행 완료, 반환값 없음');
}
} catch (scriptError) {
console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError);
}
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '번역 완료',
message: `${direction}로 변환되었습니다.`
});
} catch (error) {
console.error('[background.js] 한국어↔중국어 번역 중 오류:', error);
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '번역 오류',
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
});
}
}
// 범용 번역 함수
async function translateText(text, sourceLang, targetLang) {
console.log(`[translateText] 번역 시작: ${sourceLang} -> ${targetLang}, 텍스트: ${text}`);
// 1. Google 번역 시도
try {
const googleUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
console.log('[translateText] Google 번역 시도:', googleUrl);
const response = await fetch(googleUrl, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (response.ok) {
const data = await response.json();
if (data && data[0] && data[0][0] && data[0][0][0]) {
const translatedText = data[0][0][0];
console.log('[translateText] Google 번역 성공:', translatedText);
return translatedText;
}
}
console.log('[translateText] Google 번역 응답 형식 오류');
} catch (error) {
console.error('[translateText] Google 번역 실패:', error);
}
// 2. MyMemory 번역 시도
try {
console.log('[translateText] MyMemory 번역 시도');
const myMemoryUrl = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`;
const response = await fetch(myMemoryUrl);
if (response.ok) {
const data = await response.json();
if (data && data.responseData && data.responseData.translatedText) {
const translatedText = data.responseData.translatedText;
console.log('[translateText] MyMemory 번역 성공:', translatedText);
return translatedText;
}
}
console.log('[translateText] MyMemory 번역 응답 형식 오류');
} catch (error) {
console.error('[translateText] MyMemory 번역 실패:', error);
}
// 3. 간단한 사전 기반 번역 (한중/중한만)
if ((sourceLang === 'ko' && targetLang === 'zh') || (sourceLang === 'zh' && targetLang === 'ko')) {
try {
console.log('[translateText] 사전 기반 번역 시도');
const dictResult = await simpleDictionaryTranslate(text, sourceLang, targetLang);
if (dictResult) {
console.log('[translateText] 사전 기반 번역 성공:', dictResult);
return dictResult;
}
} catch (error) {
console.error('[translateText] 사전 기반 번역 실패:', error);
}
}
// 4. 모든 번역 실패 시 원본 텍스트 반환
console.log('[translateText] 모든 번역 방법 실패, 원본 텍스트 반환');
throw new Error('모든 번역 엔진에서 번역에 실패했습니다');
}
// 간단한 사전 기반 번역 함수
async function simpleDictionaryTranslate(text, sourceLang, targetLang) {
// 기본적인 한중/중한 단어 사전
const koreanToChinese = {
'안녕하세요': '你好',
'감사합니다': '谢谢',
'죄송합니다': '对不起',
'네': '是',
'아니요': '不是',
'좋습니다': '好的',
'플래그십': '旗舰',
'스토어': '店',
'상점': '商店',
'가게': '店铺'
};
const chineseToKorean = {
'你好': '안녕하세요',
'谢谢': '감사합니다',
'对不起': '죄송합니다',
'是': '네',
'不是': '아니요',
'好的': '좋습니다',
'旗舰': '플래그십',
'店': '스토어',
'商店': '상점',
'店铺': '가게',
'旗舰店': '플래그십 스토어'
};
if (sourceLang === 'ko' && targetLang === 'zh') {
return koreanToChinese[text.trim()] || null;
} else if (sourceLang === 'zh' && targetLang === 'ko') {
return chineseToKorean[text.trim()] || null;
}
return null;
}
// 선택된 텍스트를 새로운 텍스트로 대체하는 함수 (content script에서 실행)
function replaceSelectedText(newText) {
console.log('[replaceSelectedText] 텍스트 대체 시작:', newText);
console.log('[replaceSelectedText] 현재 URL:', window.location.href);
console.log('[replaceSelectedText] activeElement:', document.activeElement);
const selection = window.getSelection();
const activeElement = document.activeElement;
let textReplaced = false;
// 선택된 텍스트의 위치 정보 저장
let selectionRect = null;
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
selectionRect = range.getBoundingClientRect();
}
// 알리왕왕 특화 처리 - 더 광범위한 선택자 사용
if (window.location.href.includes('aliexpress') || window.location.href.includes('1688') ||
document.querySelector('.ice-container') || document.querySelector('[class*="aliwangwang"]') ||
window.location.href.includes('alibaba') || document.querySelector('[class*="im-"]')) {
console.log('[replaceSelectedText] 알리왕왕/알리바바 환경 감지됨');
// 더 광범위한 입력창 선택자
const inputSelectors = [
// 기본 입력 요소
'textarea', 'input[type="text"]', 'input:not([type])',
// contentEditable 요소
'[contenteditable="true"]', '[contenteditable=""]', 'div[contenteditable]',
// 알리왕왕 특화 선택자
'.ice-container textarea', '.ice-container input', '.ice-container [contenteditable]',
'.chat-input textarea', '.chat-input input', '.chat-input [contenteditable]',
'[class*="input"] textarea', '[class*="input"] input', '[class*="input"] [contenteditable]',
'[class*="chat"] textarea', '[class*="chat"] input', '[class*="chat"] [contenteditable]',
'[class*="message"] textarea', '[class*="message"] input', '[class*="message"] [contenteditable]',
'[class*="editor"] textarea', '[class*="editor"] input', '[class*="editor"] [contenteditable]',
// 역할 기반 선택자
'div[role="textbox"]', '[role="textbox"]', '[role="combobox"]',
// 플레이스홀더 기반
'[placeholder*="메시지"]', '[placeholder*="message"]', '[placeholder*="输入"]', '[placeholder*="请输入"]',
// 클래스명 기반
'.editor', '.input-box', '.text-input', '.message-input', '.chat-editor'
];
const allInputs = document.querySelectorAll(inputSelectors.join(','));
console.log('[replaceSelectedText] 찾은 모든 입력 요소 수:', allInputs.length);
// 각 입력 요소 상세 검사
for (let i = 0; i < allInputs.length; i++) {
const input = allInputs[i];
const rect = input.getBoundingClientRect();
const isVisible = rect.width > 0 && rect.height > 0;
const isFocused = input.matches(':focus') || input === activeElement;
const hasText = input.value || input.textContent || input.innerText;
console.log(`[replaceSelectedText] 입력창 ${i+1}:`, {
tagName: input.tagName,
className: input.className,
id: input.id,
isVisible: isVisible,
isFocused: isFocused,
hasText: !!hasText,
placeholder: input.placeholder,
contentEditable: input.contentEditable
});
// 활성화된 또는 보이는 입력창에 텍스트 삽입 시도
if (isVisible && (isFocused || rect.width > 100)) {
console.log(`[replaceSelectedText] 입력창 ${i+1} 처리 시도`);
if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
try {
// 포커스 설정
input.focus();
input.click();
// 기존 값 백업
const originalValue = input.value || '';
console.log('[replaceSelectedText] 기존 값:', originalValue);
// 선택 영역 확인
const start = input.selectionStart || 0;
const end = input.selectionEnd || originalValue.length;
// 텍스트 대체 - 여러 방법 시도
// 방법 1: 직접 value 설정
if (start !== end && start < end) {
// 선택된 텍스트 대체
input.value = originalValue.substring(0, start) + newText + originalValue.substring(end);
input.selectionStart = start;
input.selectionEnd = start + newText.length;
} else {
// 전체 텍스트 대체 또는 추가
if (originalValue.trim()) {
input.value = newText; // 기존 텍스트를 새 텍스트로 완전 대체
} else {
input.value = newText; // 빈 입력창에 새 텍스트 입력
}
input.selectionStart = input.selectionEnd = newText.length;
}
console.log('[replaceSelectedText] 새로운 값:', input.value);
// 방법 2: execCommand 시도 (폴백)
if (input.value !== newText && !input.value.includes(newText)) {
input.select();
document.execCommand('insertText', false, newText);
}
// 다양한 이벤트 발생
const events = [
'focus', 'input', 'change', 'keydown', 'keyup', 'keypress',
'textInput', 'compositionstart', 'compositionend', 'blur'
];
events.forEach(eventType => {
try {
let event;
if (eventType.includes('key')) {
event = new KeyboardEvent(eventType, {
key: 'a',
code: 'KeyA',
bubbles: true,
cancelable: true
});
} else {
event = new Event(eventType, {
bubbles: true,
cancelable: true
});
}
input.dispatchEvent(event);
} catch (e) {
console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e);
}
});
// React/Vue 등의 프레임워크를 위한 특별 처리
if (input._valueTracker) {
input._valueTracker.setValue(originalValue);
}
// 강제로 다시 포커스
setTimeout(() => {
input.focus();
input.selectionStart = input.selectionEnd = input.value.length;
}, 100);
// 텍스트가 실제로 변경되었는지 확인
if (input.value.includes(newText)) {
textReplaced = true;
console.log('[replaceSelectedText] INPUT/TEXTAREA 처리 완료');
return true;
}
} catch (error) {
console.error('[replaceSelectedText] INPUT/TEXTAREA 처리 중 오류:', error);
}
} else if (input.contentEditable === 'true' || input.contentEditable === '' ||
input.getAttribute('contenteditable') === 'true') {
try {
console.log('[replaceSelectedText] contentEditable 요소 처리');
// 포커스 설정
input.focus();
input.click();
// 기존 내용 백업
const originalContent = input.textContent || input.innerHTML || '';
console.log('[replaceSelectedText] 기존 내용:', originalContent);
// 방법 1: 선택 후 대체
const sel = window.getSelection();
// 전체 내용 선택
const range = document.createRange();
range.selectNodeContents(input);
sel.removeAllRanges();
sel.addRange(range);
// execCommand로 텍스트 삽입
if (document.execCommand('insertText', false, newText)) {
console.log('[replaceSelectedText] execCommand insertText 성공');
} else {
// 방법 2: 직접 DOM 조작
input.innerHTML = '';
input.textContent = newText;
// 커서를 끝으로 이동
const newRange = document.createRange();
newRange.selectNodeContents(input);
newRange.collapse(false);
sel.removeAllRanges();
sel.addRange(newRange);
}
console.log('[replaceSelectedText] 새로운 내용:', input.textContent || input.innerHTML);
// 이벤트 발생
const events = [
'focus', 'input', 'textInput', 'keyup', 'compositionend',
'DOMCharacterDataModified', 'DOMSubtreeModified', 'blur'
];
events.forEach(eventType => {
try {
const event = new Event(eventType, { bubbles: true, cancelable: true });
input.dispatchEvent(event);
} catch (e) {
console.log(`[replaceSelectedText] 이벤트 ${eventType} 발생 실패:`, e);
}
});
// 강제로 다시 포커스
setTimeout(() => {
input.focus();
const finalRange = document.createRange();
finalRange.selectNodeContents(input);
finalRange.collapse(false);
const finalSel = window.getSelection();
finalSel.removeAllRanges();
finalSel.addRange(finalRange);
}, 100);
// 텍스트가 실제로 변경되었는지 확인
if (input.textContent.includes(newText) || input.innerHTML.includes(newText)) {
textReplaced = true;
console.log('[replaceSelectedText] contentEditable 처리 완료');
return true;
}
} catch (error) {
console.error('[replaceSelectedText] contentEditable 처리 중 오류:', error);
}
}
}
}
}
// 일반 사이트 처리 (기존 로직)
if (!textReplaced) {
console.log('[replaceSelectedText] 일반 사이트 처리 시작');
// 1. input/textarea 처리
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
console.log('[replaceSelectedText] 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;
// 다양한 이벤트 트리거
['input', 'change', 'keyup', 'blur'].forEach(eventType => {
activeElement.dispatchEvent(new Event(eventType, { bubbles: true }));
});
if (activeElement.value.includes(newText)) {
textReplaced = true;
console.log('[replaceSelectedText] INPUT/TEXTAREA 텍스트 대체 완료');
return true;
}
}
// 2. contentEditable 요소 처리
if (activeElement && activeElement.contentEditable === 'true') {
console.log('[replaceSelectedText] contentEditable 요소 처리');
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(newText));
// 커서 위치 조정
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
// contentEditable 이벤트 트리거
['input', 'textInput', 'keyup'].forEach(eventType => {
activeElement.dispatchEvent(new Event(eventType, { bubbles: true }));
});
textReplaced = true;
console.log('[replaceSelectedText] contentEditable 텍스트 대체 완료');
return true;
}
}
// 3. 일반 선택 영역 처리
if (selection.rangeCount > 0) {
console.log('[replaceSelectedText] 일반 선택 영역 처리');
const range = selection.getRangeAt(0);
// 선택된 텍스트가 있는지 확인
if (range.toString().trim()) {
range.deleteContents();
range.insertNode(document.createTextNode(newText));
// 새로운 텍스트 선택
const newRange = document.createRange();
newRange.setStart(range.startContainer, range.startOffset - newText.length);
newRange.setEnd(range.startContainer, range.startOffset);
selection.removeAllRanges();
selection.addRange(newRange);
textReplaced = true;
console.log('[replaceSelectedText] 일반 텍스트 대체 완료');
return true;
}
}
}
// 텍스트 대체가 실패한 경우 팝업 표시
if (!textReplaced) {
console.log('[replaceSelectedText] 텍스트 대체 실패, 팝업 표시');
showTranslationPopup(newText, selectionRect);
// 클립보드에도 복사
try {
navigator.clipboard.writeText(newText).then(() => {
console.log('[replaceSelectedText] 클립보드에 복사 완료');
}).catch(err => {
console.error('[replaceSelectedText] 클립보드 복사 실패:', err);
});
} catch (error) {
console.error('[replaceSelectedText] 클립보드 접근 실패:', error);
}
}
return textReplaced;
}
// 번역 결과 팝업을 표시하는 함수
function showTranslationPopup(translatedText, selectionRect) {
console.log('[showTranslationPopup] 팝업 표시 시작:', translatedText);
// 기존 팝업이 있으면 제거
const existingPopup = document.getElementById('translation-popup-extension');
if (existingPopup) {
existingPopup.remove();
}
// 팝업 컨테이너 생성
const popup = document.createElement('div');
popup.id = 'translation-popup-extension';
popup.style.cssText = `
position: fixed;
z-index: 999999;
background: #ffffff;
border: 2px solid #4CAF50;
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: 'Arial', 'Malgun Gothic', sans-serif;
font-size: 14px;
line-height: 1.4;
max-width: 300px;
word-wrap: break-word;
animation: fadeInScale 0.3s ease-out;
`;
// 애니메이션 CSS 추가
if (!document.getElementById('translation-popup-styles')) {
const style = document.createElement('style');
style.id = 'translation-popup-styles';
style.textContent = `
@keyframes fadeInScale {
0% {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes fadeOut {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.9);
}
}
`;
document.head.appendChild(style);
}
// 팝업 내용 생성
const content = document.createElement('div');
content.innerHTML = `
<div style="margin-bottom: 8px; font-weight: bold; color: #4CAF50; font-size: 12px;">
🌐 번역 결과
</div>
<div style="color: #333; margin-bottom: 10px; padding: 8px; background: #f9f9f9; border-radius: 4px;">
${translatedText}
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="copy-translation" style="
background: #4CAF50;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
">복사</button>
<button id="close-popup" style="
background: #666;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
">닫기</button>
</div>
`;
popup.appendChild(content);
// 팝업 위치 계산
let left = 50;
let top = 50;
if (selectionRect && selectionRect.width > 0 && selectionRect.height > 0) {
// 선택된 텍스트 위치 기준으로 팝업 위치 설정
left = selectionRect.left + selectionRect.width / 2;
top = selectionRect.top - 10; // 선택된 텍스트 위쪽에 표시
// 화면 경계 확인 및 조정
const maxLeft = window.innerWidth - 320; // 팝업 너비 고려
const maxTop = window.innerHeight - 150; // 팝업 높이 고려
if (left > maxLeft) left = maxLeft;
if (left < 10) left = 10;
if (top < 10) {
// 위쪽 공간이 부족하면 선택된 텍스트 아래쪽에 표시
top = selectionRect.bottom + 10;
}
if (top > maxTop) top = maxTop;
} else {
// 선택 영역 정보가 없으면 화면 중앙에 표시
left = (window.innerWidth - 300) / 2;
top = (window.innerHeight - 150) / 2;
}
popup.style.left = left + 'px';
popup.style.top = top + 'px';
// 문서에 팝업 추가
document.body.appendChild(popup);
// 이벤트 리스너 추가
const copyButton = popup.querySelector('#copy-translation');
const closeButton = popup.querySelector('#close-popup');
copyButton.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(translatedText);
copyButton.textContent = '복사됨!';
copyButton.style.background = '#2196F3';
setTimeout(() => {
copyButton.textContent = '복사';
copyButton.style.background = '#4CAF50';
}, 1500);
} catch (err) {
console.error('클립보드 복사 실패:', err);
copyButton.textContent = '실패';
copyButton.style.background = '#f44336';
}
});
closeButton.addEventListener('click', () => {
popup.style.animation = 'fadeOut 0.2s ease-in';
setTimeout(() => {
if (popup.parentNode) {
popup.remove();
}
}, 200);
});
// 5초 후 자동으로 팝업 제거
setTimeout(() => {
if (popup.parentNode) {
popup.style.animation = 'fadeOut 0.3s ease-in';
setTimeout(() => {
if (popup.parentNode) {
popup.remove();
}
}, 300);
}
}, 5000);
// 팝업 외부 클릭 시 닫기
document.addEventListener('click', function closeOnOutsideClick(e) {
if (!popup.contains(e.target)) {
popup.style.animation = 'fadeOut 0.2s ease-in';
setTimeout(() => {
if (popup.parentNode) {
popup.remove();
}
}, 200);
document.removeEventListener('click', closeOnOutsideClick);
}
});
console.log('[showTranslationPopup] 팝업 표시 완료');
}
// ==================== 시간 알람 기능 ====================
class TimeAlarmManager {
constructor() {
this.workTimer = null;
this.breakTimer = null;
this.isBreakTime = false;
this.settings = {
enabled: true,
workTime: 60, // 분
restTime: 5, // 분
autoZzim: false,
pomodoro: false,
cycle: 0 // 포모도로 현재 사이클(0~3)
};
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 workMin = this.settings.pomodoro ? 35 : this.settings.workTime;
const workTimeMs = workMin * 60 * 1000;
console.log(`[TimeAlarm] 작업 타이머 시작: ${workMin}`);
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 breakTimeMin = this.settings.restTime;
const breakTimeMs = breakTimeMin * 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();
// 포모도로 모드: 사이클 증가 및 리셋
if (this.settings.pomodoro) {
this.settings.cycle = (this.settings.cycle + 1) % 4;
}
// 다음 작업 타이머 시작
this.startWorkTimer();
}, breakTimeMs);
}
showRestNotification() {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '휴식 시간입니다! 🧘‍♀️',
message: `${this.settings.pomodoro ? 35 : 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.pomodoro ? 35 : this.settings.workTime) * 60 * 1000;
const remainingTime = totalWorkTime - elapsed;
if (remainingTime <= 0) {
return {
isRunning: false,
reason: '작업 시간이 이미 완료됨'
};
}
return {
isRunning: true,
remainingTime: remainingTime,
workTime: this.settings.pomodoro ? 35 : 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 || "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 사용자 정보 가져오기
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 반환
}
if (message.action === 'executeBackgroundZzim') {
handleExecuteBackgroundZzim(message, sendResponse);
return true; // 비동기 응답을 위해 true 반환
}
// ===== 통합 사용자 정보 및 데이터 핸들러 =====
if (message.action === 'getUserInfo') {
handleGetUserInfo(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getZzimStats') {
handleGetZzimStats(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getMyMarkets') {
handleGetMyMarkets(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
// ===== 타냐대장경(Sayings) 핸들러 =====
if (message.action === 'getSayingsCategories') {
handleGetSayingsCategories(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getSayingsTargets') {
handleGetSayingsTargets(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getSayings') {
handleGetSayings(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'createSaying') {
handleCreateSaying(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'updateSaying') {
handleUpdateSaying(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'deleteSaying') {
handleDeleteSaying(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
// ===== 휴식 모달 (Rest Modal) 핸들러 =====
if (message.action === 'getRestActivities') {
handleGetRestActivities(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getRandomSaying') {
handleGetRandomSaying(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
// ===== 금지어 관리 (Banned Words) 핸들러 =====
if (message.action === 'validateToken') {
handleValidateToken(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getBannedWords') {
handleGetBannedWords(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'updateBannedWordGrade') {
handleUpdateBannedWordGrade(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'deleteBannedWord') {
handleDeleteBannedWord(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getKiprisResults') {
handleGetKiprisResults(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'addBannedWords') {
handleAddBannedWords(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
if (message.action === 'getExistingBannedWords') {
handleGetExistingBannedWords(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return 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: "https://ko.wrmc.cc",
SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"
};
// 백엔드 설정 가져오기 함수
function getBackendConfig() {
return { ...BACKEND_CONFIG };
}
// 검색 결과 개선을 위한 키워드 확장 함수
// 이 함수는 삭제됩니다. content.js를 직접 주입하여 사용하므로 중복 코드가 제거됩니다.
// zzimScript 제거됨
async function handleExecuteBackgroundZzim(message, sendResponse) {
// 위쪽에서 이미 재정의되었습니다. 이 부분은 이전 코드의 잔재를 제거하기 위한 플레이스홀더입니다.
// 실제로는 위에서 재정의한 handleExecuteBackgroundZzim이 사용됩니다.
}
// 메시지 리스너 추가 - content.js에서 보내는 메시지 처리
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[Background] 메시지 수신:', message);
// 지재권 검색 요청
if (message.action === "searchTrademark") {
console.log('[Background] 지재권 검색 요청 처리:', message.keyword);
handleTrademarkSearch(message.keyword, sender.tab)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 지재권 검색 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 멀티번역 요청
if (message.action === "translateText") {
console.log('[Background] 멀티번역 요청 처리:', message.text);
handleMultiTranslate({ selectionText: message.text })
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 멀티번역 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 직번역 요청
if (message.action === "handleDirectTranslation") {
const selectedText = message.selectedText || message.text;
console.log('[Background] 직번역 요청 처리:', selectedText);
handleDirectTranslation(selectedText, sender.tab)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 직번역 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 한중번역 요청
if (message.action === "handleKoreanToChinese") {
const selectedText = message.selectedText || message.text;
console.log('[Background] 한중번역 요청 처리:', selectedText);
handleKoreanToChinese(selectedText, sender.tab)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('[Background] 한중번역 처리 실패:', error);
sendResponse({ success: false, error: error.message });
});
return true; // 비동기 응답
}
// 금지어 추가 요청
if (message.action === "addBannedWord") {
handleAddBannedWord(message, sendResponse);
return true; // 비동기 응답
}
});
// 직번역 처리 함수 (선택된 텍스트를 바로 번역된 텍스트로 대체)
async function handleDirectTranslation(selectedText, tab) {
if (!selectedText) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '텍스트 선택 필요',
message: '번역할 텍스트를 먼저 선택해주세요.'
});
return;
}
console.log('[background.js] 직번역 요청:', selectedText);
try {
// 언어 감지 및 번역 방향 결정
const isKorean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(selectedText);
const isChinese = /[\u4e00-\u9fff]/.test(selectedText);
const isEnglish = /^[a-zA-Z\s.,!?'"()-]+$/.test(selectedText.trim());
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 (isEnglish && !isKorean && !isChinese) {
// 영어 → 한국어
translatedText = await translateText(selectedText, 'en', '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 {
// 기타 언어는 한국어로 번역
translatedText = await translateText(selectedText, 'auto', 'ko');
direction = '자동감지 → 한국어';
}
// 선택된 텍스트를 번역된 텍스트로 대체
console.log('[background.js] replaceSelectedText 실행 시작, 번역된 텍스트:', translatedText);
try {
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
function: replaceSelectedText,
args: [translatedText]
});
console.log('[background.js] replaceSelectedText 실행 결과:', result);
if (result && result[0] && result[0].result !== undefined) {
console.log('[background.js] 스크립트 실행 성공, 반환값:', result[0].result);
} else {
console.log('[background.js] 스크립트 실행 완료, 반환값 없음');
}
} catch (scriptError) {
console.error('[background.js] replaceSelectedText 실행 중 오류:', scriptError);
}
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '직번역 완료',
message: `${direction}로 번역되어 텍스트가 대체되었습니다.`
});
} catch (error) {
console.error('[background.js] 직번역 중 오류:', error);
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '직번역 오류',
message: '번역 중 문제가 발생했습니다. 다시 시도해 주세요.'
});
}
}
// 앱 설치/업데이트 시 알람 등록
chrome.runtime.onInstalled.addListener(() => {
console.log('[Background] 확장 프로그램 설치/업데이트됨');
// 1시간마다 실행되는 알람 등록 (접속 보상용)
chrome.alarms.create('hourlyReward', { periodInMinutes: 60 });
});
// 알람 이벤트 리스너
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'hourlyReward') {
handleHourlyReward();
}
});
// 시간당 접속 보상 처리 함수
async function handleHourlyReward() {
try {
// 1. 토큰 및 사용자 정보 확인
const { access_token, user_id, time_reward_state } = await chrome.storage.local.get(['access_token', 'user_id', 'time_reward_state']);
if (!access_token || !user_id) {
console.log('[TimeReward] 사용자 로그인 정보 없음, 보상 건너뜀');
return;
}
// 2. 오늘 날짜 및 상태 확인
const today = new Date().toISOString().split('T')[0];
let currentState = time_reward_state || { date: today, count: 0 };
// 날짜가 바뀌었으면 초기화
if (currentState.date !== today) {
currentState = { date: today, count: 0 };
}
// 3. 일일 제한 확인 (하루 최대 10회 = 10시간)
if (currentState.count >= 10) {
console.log(`[TimeReward] 오늘 접속 보상 한도 도달 (${currentState.count}/10회)`);
return;
}
// 4. 보상 지급 (2 마일리지)
const REWARD_AMOUNT = 2;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 현재 마일리지 조회
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=available_zzim_mile`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
});
if (userRes.ok) {
const userData = (await userRes.json())[0];
const newMileage = (userData.available_zzim_mile || 0) + REWARD_AMOUNT;
// 마일리지 업데이트
const updateRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}`, {
method: 'PATCH',
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Prefer': 'return=minimal'
},
body: JSON.stringify({ available_zzim_mile: newMileage })
});
if (updateRes.ok) {
// 5. 상태 업데이트 및 저장
currentState.count += 1;
await chrome.storage.local.set({ time_reward_state: currentState });
console.log(`[TimeReward] 접속 보상 지급 완료: +${REWARD_AMOUNT}마일리지 (오늘 ${currentState.count}/10회)`);
} else {
console.error('[TimeReward] 마일리지 업데이트 실패');
}
}
} catch (error) {
console.error('[TimeReward] 처리 중 오류:', error);
}
}
// ===== 메시지 리스너: 통계 업데이트 및 백그라운드 찜 =====
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.action === 'autoMutualZzim') {
// 메시지에서 delay 등 옵션 전달
autoMutualZzim({ delay: message.delay })
.then(() => sendResponse({ success: true }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
// 찜하기 완료 메시지 (탭 닫기 등 후처리)
if (message.action === 'zzimComplete') {
console.log('[Background] 찜하기 완료 메시지 수신, 탭 닫기 시도', sender);
if (sender.tab && sender.tab.id) {
setTimeout(() => {
chrome.tabs.remove(sender.tab.id).catch(e => console.log('탭 닫기 실패(이미 닫힘?):', e));
}, 3000); // 사용자가 결과를 볼 수 있게 3초 대기 후 닫기
}
return true;
}
// 찜 통계 업데이트 (content.js 완료 후 호출)
if (message.action === 'updateZzimStats') {
handleUpdateZzimStats(message)
.then(result => sendResponse(result))
.catch(err => sendResponse({ success: false, error: err.message }));
return true;
}
});
// 찜 통계 업데이트 처리 (Supabase)
async function handleUpdateZzimStats(message) {
try {
console.log('[Background] 찜 통계 업데이트 요청:', message);
const { count, zzimType } = message;
if (!count || count <= 0) {
console.log('[Background] 찜 개수가 0이므로 통계 업데이트 생략');
return { success: true, skipped: true };
}
// 토큰 및 사용자 정보 가져오기
const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
if (!access_token || !user_id) {
throw new Error('사용자 인증 정보가 없습니다.');
}
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const today = new Date().toISOString().split('T')[0];
// 1. 내 마켓 찜하기: 오늘 찜한 개수 + 내 마켓 총 찜 개수 증가
if (zzimType === 'my_market' || !zzimType) { // 기본값은 내 마켓
console.log(`[Background] 내 마켓 통계 업데이트: +${count}`);
// users 테이블 조회
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=today_zzim_count,my_zzim,today_zzim_date`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
});
if (!userRes.ok) throw new Error('사용자 정보 조회 실패');
const userData = (await userRes.json())[0];
let newTodayCount = (userData.today_zzim_date === today ? userData.today_zzim_count : 0) + count;
let newMyZzim = (userData.my_zzim || 0) + count;
const updateRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}`, {
method: 'PATCH',
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Prefer': 'return=minimal'
},
body: JSON.stringify({
today_zzim_count: newTodayCount,
today_zzim_date: today,
my_zzim: newMyZzim
})
});
if (!updateRes.ok) throw new Error('통계 업데이트 실패');
} else if (zzimType === 'mutual') {
// 2. 품앗이(상대방) 찜하기: 내 마일리지 증가(보상) + 상대방 마일리지 차감(비용)
console.log(`[Background] 품앗이 통계 업데이트: +${count}`);
// 2-1. 내 마일리지 증가 (Worker)
// 시스템 보너스 적용: 품앗이한 갯수의 50% 추가 지급 (총 150% 보상)
const bonus = Math.ceil(count * 0.5);
const totalReward = count + bonus;
console.log(`[Background] 품앗이 보상: 기본 ${count} + 보너스 ${bonus} (50%) = 총 ${totalReward} 마일리지`);
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=today_zzim_count,today_zzim_date,available_zzim_mile`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
});
if (!userRes.ok) throw new Error('사용자 정보 조회 실패');
const userData = (await userRes.json())[0];
let newTodayCount = (userData.today_zzim_date === today ? userData.today_zzim_count : 0) + count;
let newAvailableMile = (userData.available_zzim_mile || 0) + totalReward; // 보너스 포함 보상 지급
await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}`, {
method: 'PATCH',
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Prefer': 'return=minimal'
},
body: JSON.stringify({
today_zzim_count: newTodayCount,
today_zzim_date: today,
available_zzim_mile: newAvailableMile
})
});
// 2-2. 상대방 마일리지 차감 (Owner) - target_user_id 필요
if (message.market && message.market.owner_user_id) {
const targetUserId = message.market.owner_user_id;
// 상대방 현재 마일리지 조회 (RPC 없이 직접 처리 시 동시성 문제 가능성 있음)
const targetRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${targetUserId}&select=available_zzim_mile`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
});
if (targetRes.ok) {
const targetData = (await targetRes.json())[0];
let targetNewMile = (targetData.available_zzim_mile || 0) - count;
if (targetNewMile < 0) targetNewMile = 0; // 0 미만 방지
await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${targetUserId}`, {
method: 'PATCH',
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Prefer': 'return=minimal'
},
body: JSON.stringify({
available_zzim_mile: targetNewMile
})
});
console.log(`[Background] 상대방(${targetUserId}) 마일리지 차감 완료: -${count}`);
}
}
}
return { success: true, updated: count };
} catch (error) {
console.error('[Background] 통계 업데이트 오류:', error);
return { success: false, error: error.message };
}
}
// 자동 품앗이 실행 함수 (휴식 시간 또는 주기적 실행용)
async function autoMutualZzim(options = {}) {
try {
const { delay = 300 } = options;
console.log('[Background] autoMutualZzim 실행, delay:', delay);
// 1. 사용자 정보 가져오기
const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
if (!access_token || !user_id) {
console.log('[Background] 로그인 정보 없음, 품앗이 중단');
return;
}
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 2. 품앗이 대상 마켓 가져오기 (랜덤 또는 순차)
// 자신의 마켓은 제외
const response = await fetch(`${SUPABASE_URL}/rest/v1/user_markets?user_id=neq.${user_id}&for_mutual_zzim=eq.true&limit=10`, {
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${access_token}`
}
});
if (!response.ok) throw new Error('품앗이 마켓 목록 로드 실패');
const markets = await response.json();
if (markets.length === 0) {
console.log('[Background] 품앗이 대상 마켓이 없습니다.');
return;
}
// 3. 랜덤하게 하나 선택하여 실행
const randomMarket = markets[Math.floor(Math.random() * markets.length)];
console.log('[Background] 품앗이 대상 선택:', randomMarket.market_nickname);
// 4. 백그라운드 찜하기 실행
// handleExecuteBackgroundZzim 호출
await handleExecuteBackgroundZzim({
market: randomMarket,
zzimType: 'mutual',
settings: {
maxZzim: 50, // 한 번에 최대 50개
maxZzimMileage: 5000,
totalDelay: delay // 지연 시간 전달
},
delay: delay // 명시적 전달
}, () => {}); // 콜백 더미
} catch (error) {
console.error('[Background] autoMutualZzim 실행 오류:', error);
throw error;
}
}
// 백그라운드 찜하기 실행 함수 (개선됨: content.js 재사용)
async function handleExecuteBackgroundZzim(message, sendResponse) {
try {
console.log('[Background] 백그라운드 찜하기 시작:', message);
const { market, zzimType } = message;
if (!market || !market.market_url) throw new Error('마켓 정보가 없습니다.');
// 1. 백그라운드 탭 생성 (활성화하지 않음)
const tab = await chrome.tabs.create({
url: market.target_url || market.market_url, // 이미 파라미터가 붙은 URL 사용
active: false
});
console.log('[Background] 탭 생성됨:', tab.id);
// 2. 페이지 로드 대기 및 스크립트 주입
const listener = async (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.status === 'complete') {
chrome.tabs.onUpdated.removeListener(listener);
try {
console.log('[Background] 페이지 로드 완료, content.js 주입');
// content.js 주입 (이미 manifest에 의해 주입되었을 수도 있지만 확실히 하기 위해)
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
// 팝업 차단 스크립트 강제 주입 (메인 월드)
await chrome.scripting.executeScript({
target: { tabId: tab.id },
world: 'MAIN',
func: () => {
window.alert = function(msg) { console.log('[AutoZzim-BG] Alert blocked:', msg); return true; };
window.confirm = function(msg) { console.log('[AutoZzim-BG] Confirm blocked:', msg); return true; };
window.prompt = function(msg) { console.log('[AutoZzim-BG] Prompt blocked:', msg); return null; };
}
});
// 약간 대기 후 찜하기 시작 메시지 전송
setTimeout(() => {
chrome.tabs.sendMessage(tab.id, {
action: "startAutoZzim",
maxZzim: message.settings?.maxZzim || 50,
delay: message.delay || 300,
zzimType: zzimType || 'my_market', // 타입 전달
market: message.market // 마켓 정보 전달 (owner_user_id 포함)
}).catch(e => console.log('메시지 전송 실패(무시):', e));
}, 2000);
} catch (e) {
console.error('스크립트 실행 오류:', e);
chrome.tabs.remove(tab.id);
}
}
};
chrome.tabs.onUpdated.addListener(listener);
// 응답은 비동기로 처리됨 (content.js가 완료 후 updateZzimStats 메시지를 보낼 것임)
sendResponse({ success: true, message: '백그라운드 작업이 시작되었습니다.' });
// 안전장치: 3분 후 탭 강제 종료
setTimeout(() => {
chrome.tabs.get(tab.id, (t) => {
if (t) chrome.tabs.remove(tab.id);
});
}, 180000);
} catch (error) {
console.error('[Background] 실행 오류:', error);
sendResponse({ success: false, error: error.message });
}
}
// 사용자 정보 조회 핸들러
async function handleGetUserInfo(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
if (!token) throw new Error('토큰이 필요합니다.');
// 1. Auth API로 기본 정보 조회 (토큰 검증 포함)
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) throw new Error('토큰이 유효하지 않습니다.');
const authUser = await authRes.json();
// 2. Users 테이블 상세 정보 조회
// membership_levels 조인
const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*,membership_levels(level,api_call_limit)&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
const detailsRes = await fetch(detailsUrl, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
let userDetails = {};
if (detailsRes.ok) {
const data = await detailsRes.json();
if (data.length > 0) userDetails = data[0];
}
return {
success: true,
user: { ...authUser, ...userDetails }
};
} catch (error) {
console.error('[Background] 사용자 정보 조회 실패:', error);
return { success: false, error: error.message };
}
}
// 찜 통계 조회 핸들러
async function handleGetZzimStats(message) {
try {
const { userId, token } = message;
if (!userId || !token) throw new Error('필수 정보 누락');
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 1. 사용자 통계
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${userId}&select=my_zzim,zzim_mile,available_zzim_mile,membership_level,today_zzim_count,today_zzim_date`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!userRes.ok) throw new Error('통계 조회 실패');
const userData = (await userRes.json())[0];
// 2. 멤버십 레벨 정보
const membershipLevel = userData.membership_level || 'basic';
const limitsRes = await fetch(`${SUPABASE_URL}/rest/v1/membership_levels?level=eq.${membershipLevel}`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
// 기본값 상향 조정 (Basic: 300, Premium: 600, VIP: 1000)
let limits = { daily_zzim_limit: 300, max_zzim_mileage: 5000, mileage_per_zzim: 1, max_markets: 20, mutual_zzim_enabled: true, background_zzim_enabled: true };
if (limitsRes.ok) {
const limitsData = await limitsRes.json();
if (limitsData.length > 0) {
limits = { ...limits, ...limitsData[0] };
// DB값이 기존 낮은 값일 경우를 대비해 강제 상향 (임시 로직)
// 실제로는 DB membership_levels 테이블 값을 업데이트해야 함.
// 여기서는 기본값만 수정
if (limits.daily_zzim_limit === 100) limits.daily_zzim_limit = 300;
if (limits.daily_zzim_limit === 250) limits.daily_zzim_limit = 600;
if (limits.daily_zzim_limit === 500) limits.daily_zzim_limit = 1000;
}
}
// 사용자 편의를 위해 백그라운드 모드는 항상 활성화 (등급과 무관하게)
limits.background_zzim_enabled = true;
// 3. 오늘 날짜 체크 및 리셋 로직 (Background에서 처리)
const today = new Date().toISOString().split('T')[0];
if (userData.today_zzim_date !== today) {
// 날짜가 다르면 리셋 (비동기로 처리하고 현재는 0으로 반환)
fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${userId}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ today_zzim_count: 0, today_zzim_date: today })
}).catch(e => console.error('일일 리셋 실패:', e));
userData.today_zzim_count = 0;
}
return { success: true, stats: userData, limits };
} catch (error) {
return { success: false, error: error.message };
}
}
// 마켓 목록 조회 핸들러
async function handleGetMyMarkets(message) {
try {
const { userId, token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${userId}&select=my_markets`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error('마켓 조회 실패');
const data = await res.json();
const markets = data.length > 0 ? (data[0].my_markets || []) : [];
return { success: true, markets };
} catch (error) {
return { success: false, error: error.message };
}
}
// ===== 타냐대장경(Sayings) API 핸들러 =====
// 타냐대장경 카테고리 목록 조회
async function handleGetSayingsCategories(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/sayings_cat?select=saying_cat&order=saying_cat.asc`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`카테고리 로드 실패: ${res.status} - ${errorText}`);
}
const categories = await res.json();
console.log('[Background] 카테고리 로드 성공:', categories.length);
return { success: true, categories };
} catch (error) {
console.error('[Background] 카테고리 로드 실패:', error);
return { success: false, error: error.message };
}
}
// 타냐대장경 타겟 목록 조회
async function handleGetSayingsTargets(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/sayings_target?select=target&order=target.asc`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`타겟 로드 실패: ${res.status} - ${errorText}`);
}
const targets = await res.json();
console.log('[Background] 타겟 로드 성공:', targets.length);
return { success: true, targets };
} catch (error) {
console.error('[Background] 타겟 로드 실패:', error);
return { success: false, error: error.message };
}
}
// 타냐대장경 목록 조회
async function handleGetSayings(message) {
try {
const { token, adminApproval = true } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
let query = `${SUPABASE_URL}/rest/v1/tanya_sayings?order=created_at.desc`;
if (adminApproval) {
query += '&admin_approval=eq.true';
}
const res = await fetch(query, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`타냐대장경 로드 실패: ${res.status} - ${errorText}`);
}
const sayings = await res.json();
console.log('[Background] 타냐대장경 로드 성공:', sayings.length);
return { success: true, sayings };
} catch (error) {
console.error('[Background] 타냐대장경 로드 실패:', error);
return { success: false, error: error.message };
}
}
// 타냐대장경 등록
async function handleCreateSaying(message) {
try {
const { token, sayingData } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
body: JSON.stringify(sayingData)
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`타냐대장경 등록 실패: ${res.status} - ${errorText}`);
}
const result = await res.json();
console.log('[Background] 타냐대장경 등록 성공');
return { success: true, saying: result };
} catch (error) {
console.error('[Background] 타냐대장경 등록 실패:', error);
return { success: false, error: error.message };
}
}
// 타냐대장경 수정
async function handleUpdateSaying(message) {
try {
const { token, sayingId, sayingData } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings?id=eq.${sayingId}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
body: JSON.stringify(sayingData)
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`타냐대장경 수정 실패: ${res.status} - ${errorText}`);
}
const result = await res.json();
console.log('[Background] 타냐대장경 수정 성공');
return { success: true, saying: result };
} catch (error) {
console.error('[Background] 타냐대장경 수정 실패:', error);
return { success: false, error: error.message };
}
}
// 타냐대장경 삭제
async function handleDeleteSaying(message) {
try {
const { token, sayingId } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings?id=eq.${sayingId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`타냐대장경 삭제 실패: ${res.status} - ${errorText}`);
}
console.log('[Background] 타냐대장경 삭제 성공');
return { success: true };
} catch (error) {
console.error('[Background] 타냐대장경 삭제 실패:', error);
return { success: false, error: error.message };
}
}
// ===== 휴식 모달 (Rest Modal) 핸들러 =====
// 휴식 활동 목록 조회
async function handleGetRestActivities(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/events?select=message&event_type=eq.rest_time&order=created_at.desc&limit=50`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(`휴식 활동 조회 실패: ${res.status}`);
}
const events = await res.json();
console.log('[Background] 휴식 활동 조회 성공:', events.length);
return { success: true, activities: events };
} catch (error) {
console.error('[Background] 휴식 활동 조회 실패:', error);
return { success: false, error: error.message };
}
}
// 랜덤 어록 조회
async function handleGetRandomSaying(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 최근 1달 이내의 승인된 어록 가져오기
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const res = await fetch(`${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${oneMonthAgo.toISOString()}&admin_approval=eq.true&order=created_at.desc&limit=50`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(`어록 조회 실패: ${res.status}`);
}
const sayings = await res.json();
console.log('[Background] 랜덤 어록 조회 성공:', sayings.length);
return { success: true, sayings: sayings };
} catch (error) {
console.error('[Background] 랜덤 어록 조회 실패:', error);
return { success: false, error: error.message };
}
}
// ===== 금지어 관리 (Banned Words) 핸들러 =====
// 토큰 유효성 검증
async function handleValidateToken(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(`토큰 검증 실패: ${res.status}`);
}
const userData = await res.json();
console.log('[Background] 토큰 검증 성공:', userData.email);
return { success: true, user: userData };
} catch (error) {
console.error('[Background] 토큰 검증 실패:', error);
return { success: false, error: error.message };
}
}
// 금지어 목록 조회
async function handleGetBannedWords(message) {
try {
const { token } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 1. 사용자 정보 가져오기
const authRes = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!authRes.ok) {
throw new Error(`사용자 인증 실패: ${authRes.status}`);
}
const authUser = await authRes.json();
// 2. 사용자 ID 가져오기
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!userRes.ok) {
throw new Error(`사용자 조회 실패: ${userRes.status}`);
}
const userData = await userRes.json();
if (!userData || userData.length === 0) {
throw new Error('사용자 데이터를 찾을 수 없습니다');
}
const userId = userData[0].id;
// 3. 금지어 목록 가져오기
const bannedRes = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!bannedRes.ok) {
throw new Error(`금지어 목록 조회 실패: ${bannedRes.status}`);
}
const bannedWords = await bannedRes.json();
console.log('[Background] 금지어 목록 조회 성공:', bannedWords.length);
return { success: true, bannedWords: bannedWords, userId: userId };
} catch (error) {
console.error('[Background] 금지어 목록 조회 실패:', error);
return { success: false, error: error.message };
}
}
// 금지어 등급 수정
async function handleUpdateBannedWordGrade(message) {
try {
const { token, wordId, grade } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
grade: grade,
updated_at: new Date().toISOString()
})
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`등급 수정 실패: ${res.status} - ${errorText}`);
}
console.log('[Background] 금지어 등급 수정 성공');
return { success: true };
} catch (error) {
console.error('[Background] 금지어 등급 수정 실패:', error);
return { success: false, error: error.message };
}
}
// 금지어 삭제
async function handleDeleteBannedWord(message) {
try {
const { token, wordId } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`금지어 삭제 실패: ${res.status} - ${errorText}`);
}
console.log('[Background] 금지어 삭제 성공');
return { success: true };
} catch (error) {
console.error('[Background] 금지어 삭제 실패:', error);
return { success: false, error: error.message };
}
}
// 키프리스 결과 조회
async function handleGetKiprisResults(message) {
try {
const { token, wordId } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordId}&order=created_at.desc`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`키프리스 결과 조회 실패: ${res.status} - ${errorText}`);
}
const kiprisData = await res.json();
console.log('[Background] 키프리스 결과 조회 성공:', kiprisData.length);
return { success: true, kiprisData: kiprisData };
} catch (error) {
console.error('[Background] 키프리스 결과 조회 실패:', error);
return { success: false, error: error.message };
}
}
// 금지어 추가 (배치)
async function handleAddBannedWords(message) {
try {
const { token, userId, wordsData } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify(wordsData)
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`금지어 추가 실패: ${res.status} - ${errorText}`);
}
console.log('[Background] 금지어 추가 성공:', wordsData.length);
return { success: true };
} catch (error) {
console.error('[Background] 금지어 추가 실패:', error);
return { success: false, error: error.message };
}
}
// 기존 금지어 목록 조회 (중복 체크용)
async function handleGetExistingBannedWords(message) {
try {
const { token, userId } = message;
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
const res = await fetch(`${SUPABASE_URL}/rest/v1/user_banned_words?select=banned_word&user_id=eq.${userId}`, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(`기존 금지어 조회 실패: ${res.status}`);
}
const existingWords = await res.json();
console.log('[Background] 기존 금지어 조회 성공:', existingWords.length);
return { success: true, existingWords: existingWords };
} catch (error) {
console.error('[Background] 기존 금지어 조회 실패:', error);
return { success: false, error: error.message };
}
}