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 @@
+
+
+
+
+ 내차는언제타냐 통합확장기
+
+
+
+
+
+ 내차는언제타냐 통합확장 로그인
+
+
+
+ 초기화 중...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🔄 로그인 중입니다...
+
+
+
+
+
+
+
+
👋 내차는언제타냐 통합확장기
+
이메일:
+
회원등급:
+
오늘 호출량: 회
+
등급 만료일:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 금지어 |
+ 등급 |
+ 작업 |
+
+
+
+
+
+
+
+
+ 🔄 금지어 목록을 불러오는 중...
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔄 키프리스 결과를 불러오는 중...
+
+
+
+
+
+
+
+
+
+
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