diff --git a/background.js b/background.js index 2c6f611..899dbd8 100644 --- a/background.js +++ b/background.js @@ -75,6 +75,38 @@ chrome.contextMenus.onClicked.addListener((info, tab) => { }); }); +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + const keyword = info.selectionText.trim(); + if (!keyword) return; + + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "로그인 필요", + message: "기능을 사용하려면 먼저 로그인하세요." + }); + return; + } + + const response = await fetch("https://your-api.com/translate", { + method: "POST", + headers: { + "Authorization": `Bearer ${access_token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ text: keyword }) + }); + + const result = await response.json(); + chrome.tabs.sendMessage(tab.id, { + action: "showTooltip", + detailInfo: result, + keyword + }); +}); + // 키워드 검색 URL (기본 코드 그대로) function buildMarkInfoUrl(keyword) { const encoded = encodeURIComponent(keyword); diff --git a/manifest.json b/manifest.json index 6a94719..d3c8037 100644 --- a/manifest.json +++ b/manifest.json @@ -1,16 +1,18 @@ { "manifest_version": 3, "name": "내차는언제타냐 - 지재권 검색 확장 (컨텍스트 메뉴)", - "version": "1.0", + "version": "1.2", "description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.", "permissions": [ "contextMenus", "storage", "notifications", - "alarms" + "alarms", + "activeTab" ], "host_permissions": [ - "https://markinfo.kr/*" + "https://markinfo.kr/*", + "http://146.56.101.199:8000/*" ], "background": { "service_worker": "background.js" @@ -23,6 +25,7 @@ } ], "action": { + "default_popup": "popup.html", "default_icon": "icon.png" } } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..76abc19 --- /dev/null +++ b/popup.html @@ -0,0 +1,400 @@ + + + + + 내차는언제타냐 통합확장기 + + + + + +

내차는언제타냐 통합확장 로그인

+ + +
+ 초기화 중... +
+ + +
+ +
+ +
가입한 이메일 주소를 입력하세요
+
+
+ +
+ + 👁️ +
영문, 숫자 포함 6자 이상 입력
+
+
+ +
+ + +
+
+ + +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..5daf929 --- /dev/null +++ b/popup.js @@ -0,0 +1,1032 @@ +// Chrome Extension 환경에서 안정적으로 동작하도록 수정 +const SUPABASE_URL = "http://146.56.101.199:8000"; +const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; + +// 디버그 모드 플래그 (개발 시에만 true로 설정) +const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김 + +// 로그 레벨 정의 +const LOG_LEVELS = { + ERROR: 'ERROR', + WARN: 'WARN', + INFO: 'INFO', + DEBUG: 'DEBUG' +}; + +// 로그 저장 및 관리 시스템 +class Logger { + constructor() { + this.maxLogs = 100; // 최대 저장할 로그 수 + } + + // 로그 저장 + async saveLog(level, message, data = null) { + const timestamp = new Date().toISOString(); + const logEntry = { + timestamp, + level, + message, + data: data ? JSON.stringify(data) : null + }; + + try { + const { loginLogs = [] } = await chrome.storage.local.get('loginLogs'); + loginLogs.unshift(logEntry); // 최신 로그를 앞에 추가 + + // 최대 로그 수 제한 + if (loginLogs.length > this.maxLogs) { + loginLogs.splice(this.maxLogs); + } + + await chrome.storage.local.set({ loginLogs }); + } catch (error) { + console.error('로그 저장 실패:', error); + } + } + + // 로그 조회 + async getLogs() { + try { + const { loginLogs = [] } = await chrome.storage.local.get('loginLogs'); + return loginLogs; + } catch (error) { + console.error('로그 조회 실패:', error); + return []; + } + } + + // 로그 삭제 + async clearLogs() { + try { + await chrome.storage.local.remove('loginLogs'); + return true; + } catch (error) { + console.error('로그 삭제 실패:', error); + return false; + } + } + + // 통합 로그 함수 + async log(level, message, data = null) { + // 콘솔에 출력 + const consoleMessage = `[${level}] ${message}`; + switch (level) { + case LOG_LEVELS.ERROR: + console.error(consoleMessage, data); + break; + case LOG_LEVELS.WARN: + console.warn(consoleMessage, data); + break; + case LOG_LEVELS.INFO: + console.info(consoleMessage, data); + break; + case LOG_LEVELS.DEBUG: + if (DEBUG_MODE) console.log(consoleMessage, data); + break; + } + + // 스토리지에 저장 + await this.saveLog(level, message, data); + + // 디버그 정보 업데이트 + if (DEBUG_MODE) { + updateDebugInfo(`[${level}] ${message}`); + } + } +} + +// 전역 로거 인스턴스 +const logger = new Logger(); + +// 디버그 정보 업데이트 함수 (수정됨) +function updateDebugInfo(message) { + const debugEl = document.getElementById("debug-info"); + if (debugEl) { + if (DEBUG_MODE) { + // 기존 로그 버튼 찾기 + const existingButton = debugEl.querySelector('button'); + + // 메시지만 업데이트 (버튼은 유지) + if (existingButton) { + // 버튼이 있으면 텍스트 노드만 업데이트 + const textNode = debugEl.firstChild; + if (textNode && textNode.nodeType === Node.TEXT_NODE) { + textNode.textContent = message; + } else { + // 텍스트 노드가 없으면 새로 생성 + debugEl.insertBefore(document.createTextNode(message), debugEl.firstChild); + } + } else { + // 버튼이 없으면 전체 내용 설정 + debugEl.textContent = message; + } + + debugEl.style.display = "block"; + console.log('디버그 정보 업데이트:', message); + } else { + debugEl.style.display = "none"; + } + } else { + console.warn('debug-info 요소를 찾을 수 없습니다'); + } +} + +// 로그 표시 함수 +async function showLogs() { + const logs = await logger.getLogs(); + const logWindow = window.open('', '_blank', 'width=800,height=600'); + + const logHtml = ` + + + + 로그인 로그 + + + +

로그인 로그

+
+ + +
+
+ ${logs.map(log => ` +
+
${new Date(log.timestamp).toLocaleString()}
+
[${log.level}]
+
${log.message}
+ ${log.data ? `
${log.data}
` : ''} +
+ `).join('')} +
+ + + + `; + + logWindow.document.write(logHtml); + logWindow.document.close(); +} + +// 이메일 형식 검증 함수 +function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +// Supabase 클라이언트 초기화 (직접 fetch 사용) +async function supabaseAuth(email, password) { + await logger.log(LOG_LEVELS.INFO, '로그인 API 호출 시작', { email }); + + const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + }, + body: JSON.stringify({ + email: email, + password: password + }) + }); + + if (!response.ok) { + const error = await response.json(); + await logger.log(LOG_LEVELS.ERROR, '로그인 API 응답 오류', { + status: response.status, + error: error + }); + throw new Error(error.error_description || error.message || '로그인 실패'); + } + + await logger.log(LOG_LEVELS.INFO, '로그인 API 응답 성공'); + return await response.json(); +} + +// DOM 로드 완료 후 실행 +document.addEventListener('DOMContentLoaded', async function() { + // 즉시 디버그 정보 표시 + updateDebugInfo('DOM 로드 완료, 초기화 시작...'); + + await logger.log(LOG_LEVELS.INFO, 'DOM 로드 완료, 초기화 시작'); + + // 디버그 정보 표시/숨김 처리 + const debugEl = document.getElementById("debug-info"); + if (debugEl && DEBUG_MODE) { + debugEl.style.display = "block"; + + // 로그 보기 버튼 생성 및 추가 + if (!debugEl.querySelector('button')) { + const logButton = document.createElement('button'); + logButton.textContent = '📋 로그 보기'; + logButton.onclick = showLogs; + + // 줄바꿈 추가 후 버튼 추가 + debugEl.appendChild(document.createElement('br')); + debugEl.appendChild(logButton); + + console.log('로그 보기 버튼 추가됨'); + } + } + + updateDebugInfo('요소 존재 확인 중...'); + + try { + // 요소 존재 확인 + const requiredElements = ['email', 'password', 'login-btn', 'password-toggle']; + const missingElements = []; + + for (const elementId of requiredElements) { + if (!document.getElementById(elementId)) { + missingElements.push(elementId); + } + } + + if (missingElements.length > 0) { + updateDebugInfo(`❌ 필수 요소 누락: ${missingElements.join(', ')}`); + await logger.log(LOG_LEVELS.ERROR, '필수 요소 누락', { missingElements }); + return; + } + + updateDebugInfo('✅ 모든 필수 요소 확인됨'); + await logger.log(LOG_LEVELS.DEBUG, '모든 필수 요소 확인됨'); + + updateDebugInfo('저장된 설정 로드 중...'); + + // 자동 입력 및 자동 로그인 + try { + const saved = await chrome.storage.local.get(["savedEmail", "savedPassword", "autoLogin"]); + await logger.log(LOG_LEVELS.DEBUG, '저장된 설정 로드 완료', { + hasSavedEmail: !!saved.savedEmail, + autoLogin: saved.autoLogin + }); + + if (saved.savedEmail) document.getElementById("email").value = saved.savedEmail; + if (saved.savedPassword) document.getElementById("password").value = saved.savedPassword; + document.getElementById("save-login").checked = !!saved.savedEmail; + document.getElementById("auto-login").checked = !!saved.autoLogin; + + if (saved.autoLogin && saved.savedEmail && saved.savedPassword) { + updateDebugInfo('🔄 자동 로그인 시도 중...'); + await logger.log(LOG_LEVELS.INFO, '자동 로그인 시도'); + setTimeout(doLogin, 500); // 약간의 지연 후 로그인 + } else { + updateDebugInfo('✅ 초기화 완료 - 로그인 준비됨'); + } + } catch (error) { + updateDebugInfo(`❌ 설정 로드 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '저장된 설정 로드 실패', { error: error.message }); + } + + // 🔐 비밀번호 보기 (눌렀을 때만 보이기) + const passwordToggle = document.getElementById("password-toggle"); + const passwordInput = document.getElementById("password"); + + if (passwordToggle && passwordInput) { + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 토글 이벤트 등록'); + + // 마우스 이벤트 + passwordToggle.addEventListener("mousedown", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 보기 - mousedown'); + passwordInput.type = "text"; + }); + + passwordToggle.addEventListener("mouseup", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 숨기기 - mouseup'); + passwordInput.type = "password"; + }); + + passwordToggle.addEventListener("mouseleave", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 숨기기 - mouseleave'); + passwordInput.type = "password"; + }); + + // 터치 이벤트 + passwordToggle.addEventListener("touchstart", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 보기 - touchstart'); + passwordInput.type = "text"; + }); + + passwordToggle.addEventListener("touchend", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 숨기기 - touchend'); + passwordInput.type = "password"; + }); + } + + // 이메일 입력 필드 이벤트 + const emailInput = document.getElementById("email"); + if (emailInput) { + await logger.log(LOG_LEVELS.DEBUG, '이메일 필드 이벤트 등록'); + + emailInput.addEventListener("keypress", async (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const password = document.getElementById("password").value.trim(); + + if (password) { + // 비밀번호가 있으면 로그인 시도 + await logger.log(LOG_LEVELS.INFO, '이메일 필드에서 Enter - 로그인 시도'); + doLogin(); + } else { + // 비밀번호가 없으면 비밀번호 필드로 포커스 이동 + await logger.log(LOG_LEVELS.DEBUG, '이메일 필드에서 Enter - 비밀번호 필드로 이동'); + document.getElementById("password").focus(); + } + } + }); + } + + // 비밀번호 입력 필드 이벤트 + if (passwordInput) { + await logger.log(LOG_LEVELS.DEBUG, '비밀번호 필드 이벤트 등록'); + + passwordInput.addEventListener("keypress", async (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '비밀번호 필드에서 Enter - 로그인 시도'); + doLogin(); + } + }); + } + + // 로그인 버튼 + const loginBtn = document.getElementById("login-btn"); + if (loginBtn) { + await logger.log(LOG_LEVELS.DEBUG, '로그인 버튼 이벤트 등록'); + + loginBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '로그인 버튼 클릭됨'); + doLogin(); + }); + } + + // 로그아웃 버튼 + const logoutBtn = document.getElementById("logout-btn"); + if (logoutBtn) { + await logger.log(LOG_LEVELS.DEBUG, '로그아웃 버튼 이벤트 등록'); + + logoutBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '로그아웃 버튼 클릭됨'); + await chrome.storage.local.remove("access_token"); + location.reload(); + }); + } + + // 금지어 관리 버튼 + const bannedWordsBtn = document.getElementById("banned-words-btn"); + if (bannedWordsBtn) { + await logger.log(LOG_LEVELS.DEBUG, '금지어 관리 버튼 이벤트 등록'); + + bannedWordsBtn.addEventListener("click", async (e) => { + e.preventDefault(); + await logger.log(LOG_LEVELS.INFO, '금지어 관리 버튼 클릭됨'); + await openBannedWordsModal(); + }); + } + + // 모달 닫기 이벤트들 + const closeBannedWords = document.getElementById("close-banned-words"); + if (closeBannedWords) { + closeBannedWords.addEventListener("click", () => { + document.getElementById("banned-words-modal").style.display = "none"; + }); + } + + const closeKipris = document.getElementById("close-kipris"); + if (closeKipris) { + closeKipris.addEventListener("click", () => { + document.getElementById("kipris-modal").style.display = "none"; + }); + } + + // 모달 외부 클릭 시 닫기 + window.addEventListener("click", (e) => { + if (e.target.classList.contains("modal")) { + e.target.style.display = "none"; + } + }); + + // 기존 토큰이 있으면 사용자 정보 로드 + try { + const { access_token } = await chrome.storage.local.get("access_token"); + if (access_token) { + updateDebugInfo('🔄 기존 토큰으로 사용자 정보 로드 중...'); + await logger.log(LOG_LEVELS.INFO, '기존 토큰 발견, 사용자 정보 로드'); + await loadUserInfo(access_token); + } else { + await logger.log(LOG_LEVELS.DEBUG, '저장된 토큰 없음'); + if (!document.getElementById("auto-login").checked) { + updateDebugInfo('✅ 초기화 완료 - 모든 기능 준비됨'); + } + } + } catch (error) { + updateDebugInfo(`❌ 토큰 확인 오류: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '토큰 확인 중 오류', { error: error.message }); + } + + await logger.log(LOG_LEVELS.INFO, '초기화 완료 - 모든 기능 준비됨'); + + } catch (error) { + updateDebugInfo(`❌ 초기화 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '초기화 중 치명적 오류', { error: error.message }); + console.error('초기화 오류:', error); + } +}); + +// 로그인 함수 +async function doLogin() { + updateDebugInfo('🔄 로그인 프로세스 시작...'); + await logger.log(LOG_LEVELS.INFO, '로그인 프로세스 시작'); + + try { + const email = document.getElementById("email").value.trim(); + const password = document.getElementById("password").value.trim(); + + await logger.log(LOG_LEVELS.DEBUG, '로그인 입력값 검증 시작', { email }); + + // 오류 메시지 초기화 + document.getElementById("email-error").textContent = ""; + document.getElementById("password-error").textContent = ""; + document.getElementById("status").textContent = ""; + document.getElementById("membership-level").textContent = ""; + + // 유효성 검사 + let hasError = false; + + // 이메일 형식 검증 강화 + if (!email) { + document.getElementById("email-error").textContent = "이메일을 입력하세요."; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '이메일 입력 누락'); + } else if (!isValidEmail(email)) { + document.getElementById("email-error").textContent = "올바른 이메일 형식을 입력하세요. (예: user@example.com)"; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '잘못된 이메일 형식', { email }); + } + + // 비밀번호 검증 + if (!password) { + document.getElementById("password-error").textContent = "비밀번호를 입력하세요."; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '비밀번호 입력 누락'); + } else if (password.length < 6) { + document.getElementById("password-error").textContent = "비밀번호는 6자 이상이어야 합니다."; + hasError = true; + await logger.log(LOG_LEVELS.WARN, '비밀번호 길이 부족', { length: password.length }); + } + + if (hasError) { + updateDebugInfo('❌ 입력 유효성 검사 실패'); + await logger.log(LOG_LEVELS.WARN, '입력 유효성 검사 실패'); + return; + } + + // UI 상태 변경 - 로딩 표시 + document.getElementById("loading").style.display = "block"; + document.getElementById("login-btn").disabled = true; + document.getElementById("login-btn").textContent = "로그인 중..."; + document.getElementById("status").textContent = "🔄 로그인 중입니다..."; + updateDebugInfo('🔄 로그인 API 호출 중...'); + await logger.log(LOG_LEVELS.INFO, '로그인 요청 전송 중...'); + + try { + // 직접 fetch를 사용한 로그인 + const authResult = await supabaseAuth(email, password); + + if (authResult.access_token) { + updateDebugInfo('✅ 로그인 성공! 사용자 정보 로드 중...'); + await logger.log(LOG_LEVELS.INFO, '로그인 성공, 토큰 저장'); + + const token = authResult.access_token; + await chrome.storage.local.set({ access_token: token }); + + // 로그인 정보 저장 처리 + if (document.getElementById("save-login").checked) { + await chrome.storage.local.set({ + savedEmail: email, + savedPassword: password + }); + await logger.log(LOG_LEVELS.INFO, '로그인 정보 저장됨'); + } else { + await chrome.storage.local.remove(["savedEmail", "savedPassword"]); + } + + // 자동 로그인 설정 처리 + if (document.getElementById("auto-login").checked) { + await chrome.storage.local.set({ autoLogin: true }); + await logger.log(LOG_LEVELS.INFO, '자동 로그인 설정됨'); + } else { + await chrome.storage.local.remove("autoLogin"); + } + + document.getElementById("status").textContent = "✅ 로그인 성공!"; + await logger.log(LOG_LEVELS.INFO, '사용자 정보 로드 시작'); + await loadUserInfo(token); + } else { + throw new Error('토큰을 받지 못했습니다'); + } + } catch (authError) { + updateDebugInfo(`❌ 로그인 실패: ${authError.message}`); + await logger.log(LOG_LEVELS.ERROR, '로그인 실패', { error: authError.message }); + document.getElementById("status").textContent = "❌ 로그인 실패: " + authError.message; + + // 입력 필드에 포커스 + if (authError.message.includes('email') || authError.message.includes('이메일')) { + document.getElementById("email").focus(); + } else { + document.getElementById("password").focus(); + } + } + } catch (err) { + updateDebugInfo(`❌ 로그인 오류: ${err.message}`); + await logger.log(LOG_LEVELS.ERROR, '로그인 중 예외', { error: err.message }); + document.getElementById("status").textContent = "❌ 알 수 없는 오류: " + err.message; + console.error('로그인 오류:', err); + } finally { + // UI 상태 복원 + document.getElementById("loading").style.display = "none"; + document.getElementById("login-btn").disabled = false; + document.getElementById("login-btn").textContent = "로그인"; + await logger.log(LOG_LEVELS.DEBUG, '로그인 프로세스 종료'); + } +} + +// 사용자 정보 로드 함수 +async function loadUserInfo(token) { + updateDebugInfo('🔄 사용자 정보 로드 중...'); + await logger.log(LOG_LEVELS.INFO, '사용자 정보 요청 중...'); + + try { + // 먼저 Auth API로 사용자 기본 정보 가져오기 + const authUrl = `${SUPABASE_URL}/auth/v1/user`; + await logger.log(LOG_LEVELS.DEBUG, '사용자 Auth 정보 API 호출', { authUrl }); + + const authRes = await fetch(authUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!authRes.ok) { + // Auth API 응답 오류 처리 + let authErrorDetails; + try { + authErrorDetails = await authRes.json(); + } catch { + authErrorDetails = await authRes.text(); + } + + await logger.log(LOG_LEVELS.ERROR, 'Auth API 응답 오류', { + status: authRes.status, + statusText: authRes.statusText, + authErrorDetails + }); + + throw new Error(`Auth API 오류 - HTTP ${authRes.status}: ${authRes.statusText}`); + } + + const authUser = await authRes.json(); + await logger.log(LOG_LEVELS.DEBUG, 'Auth API 응답 데이터', { authUser }); + + // 사용자 상세 정보와 membership_levels 조인해서 가져오기 + let userDetails = null; + try { + // membership_levels 테이블과 조인하여 api_call_limit도 함께 가져오기 + const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*,membership_levels(level,api_call_limit)&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + await logger.log(LOG_LEVELS.DEBUG, '사용자 상세 정보 API 호출 (조인)', { detailsUrl }); + + const detailsRes = await fetch(detailsUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (detailsRes.ok) { + const detailsData = await detailsRes.json(); + await logger.log(LOG_LEVELS.DEBUG, '사용자 상세 정보 응답 (조인)', { detailsData }); + userDetails = detailsData[0]; + } else { + // 조인이 실패하면 기본 users 테이블만 조회 + await logger.log(LOG_LEVELS.WARN, '조인 쿼리 실패, 기본 사용자 정보만 조회', { + status: detailsRes.status + }); + + const fallbackUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`; + const fallbackRes = await fetch(fallbackUrl, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (fallbackRes.ok) { + const fallbackData = await fallbackRes.json(); + await logger.log(LOG_LEVELS.DEBUG, '기본 사용자 정보 응답', { fallbackData }); + userDetails = fallbackData[0]; + } + } + } catch (detailsError) { + await logger.log(LOG_LEVELS.WARN, '사용자 상세 정보 조회 중 오류', { + error: detailsError.message + }); + } + + // UI 업데이트 + updateDebugInfo('✅ 사용자 정보 로드 완료'); + await logger.log(LOG_LEVELS.INFO, '사용자 정보 로드 성공', { + email: authUser.email, + hasDetails: !!userDetails, + membershipLevel: userDetails?.membership_level, + membershipLevels: userDetails?.membership_levels + }); + + document.getElementById("login-section").style.display = "none"; + document.getElementById("user-info-section").style.display = "block"; + document.getElementById("user-email").textContent = authUser.email; + + // 상세 정보 표시 + if (userDetails) { + // 회원등급 표시 (membership_levels 조인 데이터 또는 membership_level 필드) + let levelText = "기본"; + let apiLimit = null; + + if (userDetails.membership_levels) { + // 조인된 데이터가 있는 경우 + levelText = userDetails.membership_levels.level || "기본"; + apiLimit = userDetails.membership_levels.api_call_limit; + } else if (userDetails.membership_level) { + // 직접 필드가 있는 경우 + levelText = userDetails.membership_level; + } + + document.getElementById("user-level").textContent = levelText; + + // 오늘 호출량 표시 (daily_usage_count/api_call_limit 형태) + const dailyUsage = userDetails.daily_usage_count ?? 0; + let usageText = dailyUsage.toString(); + + if (apiLimit !== null && apiLimit !== undefined) { + usageText = `${dailyUsage}/${apiLimit}`; + } + + document.getElementById("user-usage").textContent = usageText; + + // 만료일 표시 + document.getElementById("user-expire").textContent = userDetails.payment_period_end + ? new Date(userDetails.payment_period_end).toLocaleDateString() + : "없음"; + + await logger.log(LOG_LEVELS.INFO, '사용자 정보 UI 업데이트 완료', { + level: levelText, + usage: usageText, + apiLimit: apiLimit + }); + } else { + // 상세 정보가 없는 경우 기본값 + document.getElementById("user-level").textContent = "기본"; + document.getElementById("user-usage").textContent = "0"; + document.getElementById("user-expire").textContent = "없음"; + + await logger.log(LOG_LEVELS.WARN, '사용자 상세 정보 없음, 기본값 사용'); + } + + } catch (error) { + updateDebugInfo(`❌ 사용자 정보 로드 실패: ${error.message}`); + await logger.log(LOG_LEVELS.ERROR, '사용자 정보 로드 실패', { error: error.message }); + document.getElementById("status").textContent = "⚠️ 사용자 정보 로드 실패: " + error.message; + console.error('사용자 정보 로드 오류:', error); + } +} + +// 금지어 관리 모달 열기 +async function openBannedWordsModal() { + await logger.log(LOG_LEVELS.INFO, '금지어 관리 모달 열기'); + + const modal = document.getElementById("banned-words-modal"); + const loading = document.getElementById("banned-words-loading"); + const tbody = document.getElementById("banned-words-tbody"); + + // 모달 표시 + modal.style.display = "block"; + loading.style.display = "block"; + tbody.innerHTML = ""; + + try { + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + throw new Error('로그인이 필요합니다'); + } + + await loadBannedWords(access_token); + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '금지어 목록 로드 실패', { error: error.message }); + tbody.innerHTML = `오류: ${error.message}`; + } finally { + loading.style.display = "none"; + } +} + +// 금지어 목록 로드 +async function loadBannedWords(token) { + await logger.log(LOG_LEVELS.INFO, '금지어 목록 API 호출'); + + // 현재 사용자의 ID 가져오기 + 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('사용자 정보를 가져올 수 없습니다'); + } + + const authUser = await authRes.json(); + + // 사용자의 user_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('사용자 정보를 찾을 수 없습니다'); + } + + const userData = await userRes.json(); + if (!userData || userData.length === 0) { + throw new Error('사용자 데이터를 찾을 수 없습니다'); + } + + const userId = userData[0].id; + await logger.log(LOG_LEVELS.DEBUG, '사용자 ID 확인', { userId }); + + // 금지어 목록 가져오기 + const bannedWordsRes = await fetch(`${SUPABASE_URL}/rest/v1/users_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!bannedWordsRes.ok) { + throw new Error('금지어 목록을 가져올 수 없습니다'); + } + + const bannedWords = await bannedWordsRes.json(); + await logger.log(LOG_LEVELS.INFO, '금지어 목록 로드 완료', { count: bannedWords.length }); + + displayBannedWords(bannedWords); +} + +// 금지어 목록 표시 +function displayBannedWords(bannedWords) { + const tbody = document.getElementById("banned-words-tbody"); + + if (bannedWords.length === 0) { + tbody.innerHTML = '등록된 금지어가 없습니다.'; + return; + } + + tbody.innerHTML = bannedWords.map(word => ` + + ${word.banned_word} + + + + + + + + + + `).join(''); + + logger.log(LOG_LEVELS.DEBUG, '금지어 목록 UI 업데이트 완료'); +} + +// 등급 수정 +async function updateGrade(wordId) { + await logger.log(LOG_LEVELS.INFO, '등급 수정 시작', { wordId }); + + const input = document.querySelector(`input[data-word-id="${wordId}"]`); + const newGrade = parseInt(input.value); + const originalGrade = parseInt(input.dataset.originalGrade); + + if (newGrade === originalGrade) { + await logger.log(LOG_LEVELS.DEBUG, '등급 변경 없음'); + return; + } + + if (isNaN(newGrade) || newGrade < 0) { + alert('올바른 등급을 입력하세요 (0 이상의 숫자)'); + input.value = originalGrade; + return; + } + + try { + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + throw new Error('로그인이 필요합니다'); + } + + const updateRes = await fetch(`${SUPABASE_URL}/rest/v1/users_banned_words?id=eq.${wordId}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ grade: newGrade }) + }); + + if (!updateRes.ok) { + throw new Error('등급 수정에 실패했습니다'); + } + + input.dataset.originalGrade = newGrade; + await logger.log(LOG_LEVELS.INFO, '등급 수정 완료', { wordId, newGrade }); + alert('등급이 성공적으로 수정되었습니다.'); + + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '등급 수정 실패', { error: error.message }); + alert('등급 수정 중 오류가 발생했습니다: ' + error.message); + input.value = originalGrade; + } +} + +// 금지어 삭제 +async function deleteBannedWord(wordId, bannedWord) { + await logger.log(LOG_LEVELS.INFO, '금지어 삭제 확인', { wordId, bannedWord }); + + if (!confirm(`'${bannedWord}' 금지어를 정말 삭제하시겠습니까?\n\n삭제된 금지어는 복구할 수 없습니다.`)) { + await logger.log(LOG_LEVELS.DEBUG, '금지어 삭제 취소됨'); + return; + } + + try { + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + throw new Error('로그인이 필요합니다'); + } + + const deleteRes = await fetch(`${SUPABASE_URL}/rest/v1/users_banned_words?id=eq.${wordId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${access_token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!deleteRes.ok) { + throw new Error('금지어 삭제에 실패했습니다'); + } + + // 테이블에서 해당 행 제거 + const row = document.querySelector(`tr[data-word-id="${wordId}"]`); + if (row) { + row.remove(); + } + + await logger.log(LOG_LEVELS.INFO, '금지어 삭제 완료', { wordId, bannedWord }); + alert('금지어가 성공적으로 삭제되었습니다.'); + + // 목록이 비어있으면 메시지 표시 + const tbody = document.getElementById("banned-words-tbody"); + if (tbody.children.length === 0) { + tbody.innerHTML = '등록된 금지어가 없습니다.'; + } + + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '금지어 삭제 실패', { error: error.message }); + alert('금지어 삭제 중 오류가 발생했습니다: ' + error.message); + } +} + +// 키프리스 결과 보기 +async function viewKiprisResults(wordId, bannedWord) { + await logger.log(LOG_LEVELS.INFO, '키프리스 결과 조회 시작', { wordId, bannedWord }); + + const modal = document.getElementById("kipris-modal"); + const loading = document.getElementById("kipris-loading"); + const results = document.getElementById("kipris-results"); + const title = document.getElementById("kipris-word-title"); + + // 모달 표시 + modal.style.display = "block"; + loading.style.display = "block"; + results.innerHTML = ""; + title.innerHTML = `

🔍 "${bannedWord}" 키프리스 검색 결과

`; + + try { + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + throw new Error('로그인이 필요합니다'); + } + + await loadKiprisResults(access_token, wordId); + } catch (error) { + await logger.log(LOG_LEVELS.ERROR, '키프리스 결과 로드 실패', { error: error.message }); + results.innerHTML = `
오류: ${error.message}
`; + } finally { + loading.style.display = "none"; + } +} + +// 키프리스 결과 로드 +async function loadKiprisResults(token, wordId) { + await logger.log(LOG_LEVELS.INFO, '키프리스 결과 API 호출', { wordId }); + + const kiprisRes = await fetch(`${SUPABASE_URL}/rest/v1/users_banned_words_for_kipris?select=*&banned_word_id=eq.${wordId}&order=created_at.desc`, { + headers: { + Authorization: `Bearer ${token}`, + apikey: SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + }); + + if (!kiprisRes.ok) { + throw new Error('키프리스 결과를 가져올 수 없습니다'); + } + + const kiprisData = await kiprisRes.json(); + await logger.log(LOG_LEVELS.INFO, '키프리스 결과 로드 완료', { count: kiprisData.length }); + + displayKiprisResults(kiprisData); +} + +// 키프리스 결과 표시 +function displayKiprisResults(kiprisData) { + const results = document.getElementById("kipris-results"); + + if (kiprisData.length === 0) { + results.innerHTML = '
키프리스 검색 결과가 없습니다.
'; + return; + } + + results.innerHTML = kiprisData.map((item, index) => ` +
+

검색 결과 ${index + 1}

+
+ 출원상태: ${item.application_status || '정보 없음'} +
+
+ 출원인: ${item.applicant_name || '정보 없음'} +
+
+ 분류: ${item.category_description || '정보 없음'} +
+ ${item.drawing ? ` +
+ 도면:
+ 상표 도면 +
+ ` : ''} +
+ `).join(''); + + logger.log(LOG_LEVELS.DEBUG, '키프리스 결과 UI 업데이트 완료'); +} + \ No newline at end of file