From 87da00a872e70ecf3ff184ddd3b965daffded66c Mon Sep 17 00:00:00 2001 From: 9700X_PC <9700X_PC@gmail.com> Date: Sun, 22 Jun 2025 23:58:16 +0900 Subject: [PATCH] =?UTF-8?q?=EC=83=88=20=EC=96=B4=EB=A1=9D=20=EA=B0=90?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=AC=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0,=20=EC=9E=90=EB=8F=99=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 91 +++ bannedWords.html | 244 +++++++ bannedWords.js | 1615 +++++++++++++++++++++++++++++++++++++++++++ manifest.json | 8 +- popup.html | 151 ++-- popup.js | 791 +++++++++++---------- sayings.html | 688 +++++++++++++++++++ sayings.js | 1699 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 4867 insertions(+), 420 deletions(-) create mode 100644 bannedWords.html create mode 100644 bannedWords.js create mode 100644 sayings.html create mode 100644 sayings.js diff --git a/background.js b/background.js index 899dbd8..faf9a44 100644 --- a/background.js +++ b/background.js @@ -7,11 +7,102 @@ chrome.runtime.onInstalled.addListener(() => { contexts: ["selection"] }); chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); + + // 새 어록 감지 알람 생성 (1분마다) + chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 }); + + // 초기 마지막 확인 시간 설정 + chrome.storage.local.set({ lastSayingsCheck: Date.now() }); }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "keepAlive") { console.log("[background.js] 서비스 워커 유지 알람 실행됨"); + } else if (alarm.name === "checkNewSayings") { + checkForNewSayings(); + } +}); + +// 새 어록 확인 함수 +async function checkForNewSayings() { + try { + console.log("[background.js] 새 어록 확인 시작"); + + // 마지막 확인 시간 가져오기 + const { lastSayingsCheck } = await chrome.storage.local.get("lastSayingsCheck"); + const lastCheckTime = lastSayingsCheck || Date.now() - 60000; // 기본값: 1분 전 + + // Supabase에서 새 어록 확인 + const { access_token } = await chrome.storage.local.get("access_token"); + if (!access_token) { + console.log("[background.js] 액세스 토큰이 없어 새 어록 확인을 건너뜁니다."); + return; + } + + const response = await fetch('https://kbvpvbabvlzjfgcnfxsg.supabase.co/rest/v1/sayings?select=*,sayings_cat(name),sayings_target(name)&created_at=gte.' + new Date(lastCheckTime).toISOString() + '&order=created_at.desc', { + method: 'GET', + headers: { + 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtidnB2YmFidmx6amZnY25meHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzU2NDcxMjEsImV4cCI6MjA1MTIyMzEyMX0.BrPBMGI_zz6-UZpUJGQdJGCFKLEGJBE7CdNLKJgMZNM', + 'Authorization': `Bearer ${access_token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const newSayings = await response.json(); + + if (newSayings && newSayings.length > 0) { + console.log(`[background.js] ${newSayings.length}개의 새 어록을 발견했습니다.`); + + // 브라우저 알림 표시 + chrome.notifications.create('newSayings', { + type: 'basic', + iconUrl: 'icon.png', + title: '새 어록 알림', + message: `${newSayings.length}개의 새로운 어록이 등록되었습니다.`, + buttons: [ + { title: '확인하기' }, + { title: '나중에' } + ] + }); + + // 새 어록 데이터를 스토리지에 저장 + chrome.storage.local.set({ + pendingNewSayings: newSayings, + hasNewSayings: true + }); + } else { + console.log("[background.js] 새 어록이 없습니다."); + } + + // 마지막 확인 시간 업데이트 + chrome.storage.local.set({ lastSayingsCheck: Date.now() }); + + } else { + console.error("[background.js] 새 어록 확인 실패:", response.status, response.statusText); + } + + } catch (error) { + console.error("[background.js] 새 어록 확인 중 오류:", error); + } +} + +// 알림 클릭 처리 +chrome.notifications.onClicked.addListener((notificationId) => { + if (notificationId === 'newSayings') { + // 어록 관리 페이지 열기 + chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') }); + chrome.notifications.clear(notificationId); + } +}); + +// 알림 버튼 클릭 처리 +chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => { + if (notificationId === 'newSayings') { + if (buttonIndex === 0) { // 확인하기 + chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') }); + } + chrome.notifications.clear(notificationId); } }); diff --git a/bannedWords.html b/bannedWords.html new file mode 100644 index 0000000..2609bd9 --- /dev/null +++ b/bannedWords.html @@ -0,0 +1,244 @@ + + + + + 금지어 관리 + + + +

🚫 금지어 관리

+ + +
+ 초기화 중... +
+ +
+ + +
+ +
+ + +
+ + + + + + + + + + + + +
순번금지어등급작업
+
+ + + + + + + + + \ No newline at end of file diff --git a/bannedWords.js b/bannedWords.js new file mode 100644 index 0000000..93ddf49 --- /dev/null +++ b/bannedWords.js @@ -0,0 +1,1615 @@ +// 금지어 관리 모듈 +class BannedWordsManager { + constructor() { + // 초기값 설정 (chrome.storage에서 로드될 때까지 임시) + this.SUPABASE_URL = null; + this.SUPABASE_ANON_KEY = null; + this.DEBUG_MODE = true; + this.ACCESS_TOKEN = null; + this.isConfigLoaded = false; + this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지 + + this.debugLog('BannedWordsManager 생성자 시작 - 설정 로드 대기 중'); + + // 키프리스 모달 닫기 버튼 이벤트 등록 + const closeKipris = document.getElementById("close-kipris"); + if (closeKipris) { + closeKipris.addEventListener("click", () => { + document.getElementById("kipris-modal").style.display = "none"; + }); + } + + // 키프리스 모달 외부 클릭 시 닫기 + window.addEventListener("click", (e) => { + const modal = document.getElementById("kipris-modal"); + if (e.target === modal) { + modal.style.display = "none"; + } + }); + + // ESC 키로 모달 닫기 이벤트 등록 + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + // 키프리스 모달 닫기 + const kiprisModal = document.getElementById("kipris-modal"); + if (kiprisModal && kiprisModal.style.display === "block") { + kiprisModal.style.display = "none"; + e.preventDefault(); + return; + } + + // 현재 창이 별도 창인 경우 닫기 + if (window.location.href.includes('bannedWords.html') || window.location.href.includes('sayings.html')) { + window.close(); + e.preventDefault(); + } + } + }); + + this.debugLog('BannedWordsManager 생성자 완료'); + } + + // 설정 로드 함수 + async loadConfig() { + try { + this.debugLog('chrome.storage에서 설정 로드 시작'); + + // 1. bannedWords_config 우선 확인 + const configData = await chrome.storage.local.get('bannedWords_config'); + + if (configData.bannedWords_config) { + const config = configData.bannedWords_config; + this.debugLog('bannedWords_config에서 설정 로드', { + hasUrl: !!config.SUPABASE_URL, + hasKey: !!config.SUPABASE_ANON_KEY, + hasToken: !!config.ACCESS_TOKEN, + debugMode: config.DEBUG_MODE, + timestamp: config.timestamp, + age: Date.now() - config.timestamp + }); + + // 설정이 너무 오래된 경우 (5분 이상) 경고 + if (Date.now() - config.timestamp > 5 * 60 * 1000) { + this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp }); + } + + this.SUPABASE_URL = config.SUPABASE_URL; + this.SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY; + this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true; + this.ACCESS_TOKEN = config.ACCESS_TOKEN; + + } else { + // 2. 개별 설정 확인 (fallback) + this.debugLog('bannedWords_config가 없음, 개별 설정 확인 중'); + + const storageData = await chrome.storage.local.get([ + 'access_token', + 'SUPABASE_URL', + 'SUPABASE_ANON_KEY' + ]); + + this.debugLog('개별 설정 조회 결과', { + hasToken: !!storageData.access_token, + hasUrl: !!storageData.SUPABASE_URL, + hasKey: !!storageData.SUPABASE_ANON_KEY + }); + + // 기본값 설정 + this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000'; + this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'; + this.ACCESS_TOKEN = storageData.access_token; + } + + this.isConfigLoaded = true; + + this.debugLog('설정 로드 완료', { + SUPABASE_URL: this.SUPABASE_URL, + hasToken: !!this.ACCESS_TOKEN, + tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, + DEBUG_MODE: this.DEBUG_MODE, + isConfigLoaded: this.isConfigLoaded + }); + + // 설정 검증 + if (!this.SUPABASE_URL) { + throw new Error('SUPABASE_URL이 설정되지 않았습니다'); + } + if (!this.SUPABASE_ANON_KEY) { + throw new Error('SUPABASE_ANON_KEY가 설정되지 않았습니다'); + } + if (!this.ACCESS_TOKEN) { + throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다'); + } + + return true; + + } catch (error) { + this.debugLog('설정 로드 실패', { error: error.message }); + throw error; + } + } + + // 디버그 로깅 함수 + debugLog(message, data = null) { + if (this.DEBUG_MODE) { + console.log(`[BannedWords] ${message}`, data || ''); + this.updateDebugUI(`[${new Date().toLocaleTimeString()}] ${message}`); + } + } + + // 디버그 UI 업데이트 + updateDebugUI(message) { + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + if (this.DEBUG_MODE) { + // 디버그 모드일 때만 표시 + // 기존 버튼 유지하면서 로그 메시지만 업데이트 + const existingButton = debugElement.querySelector('button'); + const buttonHtml = existingButton ? existingButton.outerHTML : ''; + + debugElement.innerHTML = `${message}
${buttonHtml}`; + debugElement.style.display = "block"; + + // 버튼 이벤트 다시 등록 + if (existingButton) { + const newButton = debugElement.querySelector('button'); + if (newButton && !newButton.onclick) { + newButton.addEventListener('click', this.showDebugLogs.bind(this)); + } + } + } else { + // 디버그 모드가 아닐 때는 숨김 + debugElement.style.display = "none"; + debugElement.innerHTML = ""; + } + } + } + + // 초기화 및 데이터 로드 + async initialize() { + try { + this.debugLog('BannedWordsManager 초기화 시작'); + + // 1) 설정 로드 (chrome.storage에서) + this.debugLog('설정 로드 중...'); + await this.loadConfig(); + + // 2) 토큰 유효성 검증 + this.debugLog('토큰 유효성 검증 중...'); + await this.validateToken(); + + // 3) 버튼 이벤트 리스너 등록 (한 번만) + this.debugLog('버튼 이벤트 리스너 등록 중...'); + this.attachButtonEventListeners(); + + // 4) 금지어 목록 로드 + this.debugLog('금지어 목록 로드 시작'); + await this.loadBannedWords(); + + this.debugLog('BannedWordsManager 초기화 완료'); + + } catch (error) { + this.debugLog('초기화 실패', { error: error.message }); + this.renderError(error.message); + } + } + + // 토큰 유효성 검증 + async validateToken() { + try { + this.debugLog('토큰 검증 API 호출 준비', { + url: `${this.SUPABASE_URL}/auth/v1/user`, + hasToken: !!this.ACCESS_TOKEN, + tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, + tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none' + }); + + const headers = { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + this.debugLog('요청 헤더 정보', { + hasAuthorization: !!headers.Authorization, + hasApikey: !!headers.apikey, + authPreview: headers.Authorization ? headers.Authorization.substring(0, 20) + '...' : 'none' + }); + + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { + method: 'GET', + headers: headers, + mode: 'cors', // CORS 모드 명시적 설정 + credentials: 'omit' // 크로스 오리진 요청에서 자격 증명 제외 + }); + + this.debugLog('토큰 검증 API 응답', { + status: authRes.status, + statusText: authRes.statusText, + ok: authRes.ok, + type: authRes.type, + url: authRes.url, + headers: Object.fromEntries(authRes.headers.entries()) + }); + + if (!authRes.ok) { + let errorText = ''; + let errorJson = null; + + try { + const responseText = await authRes.text(); + errorText = responseText; + + // JSON 파싱 시도 + if (responseText.trim().startsWith('{')) { + errorJson = JSON.parse(responseText); + } + } catch (parseError) { + this.debugLog('응답 파싱 실패', { parseError: parseError.message }); + } + + this.debugLog('토큰 검증 실패 - 상세 정보', { + status: authRes.status, + statusText: authRes.statusText, + errorText: errorText, + errorJson: errorJson + }); + + throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorText}`); + } + + const userData = await authRes.json(); + this.debugLog('토큰 검증 성공', { + email: userData.email, + id: userData.id, + userData: userData + }); + + return userData; + + } catch (error) { + this.debugLog('토큰 검증 중 오류 - 상세 분석', { + errorName: error.name, + errorMessage: error.message, + errorStack: error.stack, + isNetworkError: error.name === 'TypeError' && error.message.includes('fetch'), + isCorsError: error.message.includes('CORS') || error.message.includes('cors'), + isTimeoutError: error.message.includes('timeout') || error.message.includes('Timeout') + }); + + // 에러 타입별 상세 메시지 + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.\n- URL: ${this.SUPABASE_URL}/auth/v1/user\n- 원본 에러: ${error.message}`); + } else if (error.message.includes('CORS')) { + throw new Error(`CORS 오류: 크로스 오리진 요청이 차단되었습니다.\n- 서버 CORS 설정을 확인해주세요\n- 원본 에러: ${error.message}`); + } else if (error.message.includes('timeout')) { + throw new Error(`요청 시간 초과: 서버 응답이 지연되고 있습니다.\n- 원본 에러: ${error.message}`); + } + + throw error; + } + } + + // 에러 렌더링 헬퍼 함수 + renderError(message) { + const tbody = document.getElementById("banned-words-tbody"); + const statsDiv = document.getElementById("banned-words-stats"); + + if (tbody) { + tbody.innerHTML = `오류: ${message}`; + } + + if (statsDiv) { + statsDiv.innerHTML = `
오류: ${message}
`; + } + + this.debugLog('에러 렌더링 완료', { message }); + } + + // 금지어 목록 로드 + async loadBannedWords() { + this.debugLog('금지어 목록 로드 시작'); + + const loading = document.getElementById("banned-words-loading"); + const tbody = document.getElementById("banned-words-tbody"); + + loading.style.display = "block"; + tbody.innerHTML = ""; + + try { + // 공통 헤더 설정 + const headers = { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + const fetchOptions = { + method: 'GET', + headers: headers, + mode: 'cors', + credentials: 'omit' + }; + + // 현재 사용자의 ID 가져오기 + this.debugLog('Auth API 호출 중...'); + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, fetchOptions); + + this.debugLog('Auth API 응답', { + status: authRes.status, + statusText: authRes.statusText, + ok: authRes.ok, + url: authRes.url + }); + + if (!authRes.ok) { + const errorDetail = await authRes.text(); + this.debugLog('Auth API 에러 상세', { + status: authRes.status, + error: errorDetail + }); + throw new Error(`사용자 정보를 가져올 수 없습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorDetail}`); + } + + const authUser = await authRes.json(); + this.debugLog('Auth 사용자 정보 수신', { + email: authUser.email, + id: authUser.id + }); + + // 사용자의 user_id 가져오기 + this.debugLog('Users API 호출 중...'); + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, fetchOptions); + + this.debugLog('Users API 응답', { + status: userRes.status, + statusText: userRes.statusText, + ok: userRes.ok, + url: userRes.url + }); + + if (!userRes.ok) { + const errorDetail = await userRes.text(); + this.debugLog('Users API 에러 상세', { + status: userRes.status, + error: errorDetail + }); + throw new Error(`사용자 정보를 찾을 수 없습니다 (${userRes.status}: ${userRes.statusText})\n응답: ${errorDetail}`); + } + + const userData = await userRes.json(); + this.debugLog('Users 데이터 수신', { + count: userData.length, + data: userData + }); + + if (!userData || userData.length === 0) { + throw new Error('사용자 데이터를 찾을 수 없습니다'); + } + + const userId = userData[0].id; + this.debugLog('사용자 ID 확인', { userId }); + + // 금지어 목록 가져오기 + this.debugLog('BannedWords API 호출 중...'); + const bannedWordsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, fetchOptions); + + this.debugLog('BannedWords API 응답', { + status: bannedWordsRes.status, + statusText: bannedWordsRes.statusText, + ok: bannedWordsRes.ok, + url: bannedWordsRes.url + }); + + if (!bannedWordsRes.ok) { + const errorDetail = await bannedWordsRes.text(); + this.debugLog('BannedWords API 에러 상세', { + status: bannedWordsRes.status, + error: errorDetail + }); + throw new Error(`금지어 목록을 가져올 수 없습니다 (${bannedWordsRes.status}: ${bannedWordsRes.statusText})\n응답: ${errorDetail}`); + } + + const bannedWords = await bannedWordsRes.json(); + this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length }); + + this.displayBannedWords(bannedWords); + } catch (error) { + this.debugLog('금지어 목록 로드 실패', { + errorName: error.name, + errorMessage: error.message, + isNetworkError: error.name === 'TypeError' && error.message.includes('fetch') + }); + + let errorMessage = error.message; + if (error.name === 'TypeError' && error.message.includes('fetch')) { + errorMessage = `네트워크 연결 오류: ${error.message}`; + } + + tbody.innerHTML = `오류: ${errorMessage}`; + } finally { + loading.style.display = "none"; + } + } + + // 금지어 목록 표시 + displayBannedWords(bannedWords) { + this.debugLog('금지어 목록 UI 업데이트 시작', { count: bannedWords.length }); + + const tbody = document.getElementById("banned-words-tbody"); + const statsDiv = document.getElementById("banned-words-stats"); + + if (bannedWords.length === 0) { + tbody.innerHTML = '등록된 금지어가 없습니다.'; + statsDiv.innerHTML = '
총 금지어 개수: 0개
'; + this.debugLog('금지어 없음 - 빈 상태 표시'); + return; + } + + // 통계 계산 + const totalCount = bannedWords.length; + const prohibitedCount = bannedWords.filter(word => word.grade === '금지').length; + const notAllowedCount = bannedWords.filter(word => word.grade === '비허용').length; + const otherCount = totalCount - prohibitedCount - notAllowedCount; + + this.debugLog('금지어 통계 계산', { + totalCount, + prohibitedCount, + notAllowedCount, + otherCount + }); + + // 통계 정보 표시 + let statsText = `총 금지어 개수: ${totalCount}개`; + if (prohibitedCount > 0 || notAllowedCount > 0) { + statsText += ` (금지: ${prohibitedCount}개, 비허용: ${notAllowedCount}개`; + if (otherCount > 0) { + statsText += `, 기타: ${otherCount}개`; + } + statsText += ')'; + } + + // 통계 정보와 추가 버튼을 함께 표시 + statsDiv.innerHTML = ` +
+
${statsText}
+ +
+ `; + + // 금지어 추가 버튼 이벤트 등록 + const addBtn = document.getElementById('add-banned-word-btn'); + if (addBtn) { + addBtn.addEventListener('click', () => this.showAddBannedWordModal()); + } + + // 테이블 데이터 표시 (word_id를 기본키로 사용) + tbody.innerHTML = bannedWords.map((word, index) => { + // 등급에 따른 배경색 설정 + let gradeStyle = 'background-color: #f8f9fa; color: #495057;'; // 기본 스타일 + let gradeBgColor = '#f8f9fa'; // 기본 배경색 + + if (word.grade === '금지') { + gradeStyle = 'background-color: #ff8c00; color: white; font-weight: bold;'; // 주황색 + gradeBgColor = '#fff3e0'; // 연한 주황색 배경 + } else if (word.grade === '비허용') { + gradeStyle = 'background-color: #ffd700; color: #333; font-weight: bold;'; // 노란색 + gradeBgColor = '#fffbf0'; // 연한 노란색 배경 + } + + return ` + + ${index + 1} + ${word.banned_word} + + ${word.grade || '값 없음'} + + + + + + + + `; + }).join(''); + + // 버튼 이벤트 리스너는 한 번만 등록 (초기화에서 처리) + // this.attachButtonEventListeners(); // 이 줄 제거 + + this.debugLog('금지어 목록 UI 업데이트 완료'); + } + + // 버튼 이벤트 리스너 등록 (한 번만 실행) + attachButtonEventListeners() { + this.debugLog('버튼 이벤트 리스너 등록 시작'); + + const tbody = document.getElementById("banned-words-tbody"); + if (!tbody) { + this.debugLog('tbody 요소를 찾을 수 없음'); + return; + } + + // 기존 이벤트 리스너 제거 (중복 방지) + if (this.buttonEventListenerAttached) { + this.debugLog('이미 이벤트 리스너가 등록되어 있음'); + return; + } + + // 이벤트 위임을 사용하여 동적으로 생성된 버튼들에 이벤트 등록 + const clickHandler = async (e) => { + const button = e.target.closest('button[data-action]'); + if (!button) return; + + e.preventDefault(); + + const action = button.getAttribute('data-action'); + const wordId = button.getAttribute('data-word-id'); + const wordWordId = button.getAttribute('data-word-word-id'); + const wordText = button.getAttribute('data-word-text'); + const wordGrade = button.getAttribute('data-word-grade'); + + this.debugLog('버튼 클릭 감지', { action, wordId, wordWordId, wordText, wordGrade }); + + // 버튼 비활성화 (중복 클릭 방지) + const originalText = button.textContent; + button.disabled = true; + + try { + switch (action) { + case 'view': + button.textContent = '🔄 로딩...'; + await this.viewKiprisResults(wordWordId, wordText); + break; + + case 'edit': + button.textContent = '🔄 수정 중...'; + await this.editGrade(wordId, wordGrade); + break; + + case 'delete': + button.textContent = '🔄 삭제 중...'; + await this.deleteBannedWord(wordId, wordText); + break; + + default: + this.debugLog('알 수 없는 액션', { action }); + } + } catch (error) { + this.debugLog('버튼 액션 처리 중 오류', { action, error: error.message }); + alert(`❌ 작업 중 오류가 발생했습니다: ${error.message}`); + } finally { + // 버튼 상태 복원 + button.textContent = originalText; + button.disabled = false; + } + }; + + tbody.addEventListener('click', clickHandler); + this.buttonEventListenerAttached = true; + + this.debugLog('버튼 이벤트 리스너 등록 완료'); + } + + // 등급 수정 (드롭박스 개선 버전) + async editGrade(wordId, currentGrade) { + this.debugLog('등급 수정 시작', { wordId, currentGrade }); + + // 모달 HTML 생성 + const modalId = 'grade-edit-modal'; + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + const modalHtml = ` +
+
+

🔧 등급 수정

+

현재 등급: ${currentGrade || '값 없음'}

+ +
+ + +
+ +
+ + +
+
+
+ `; + + // 모달을 body에 추가 + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modal = document.getElementById(modalId); + const gradeSelect = document.getElementById('grade-select'); + const confirmBtn = document.getElementById('grade-confirm-btn'); + const cancelBtn = document.getElementById('grade-cancel-btn'); + + // 드롭박스 변경 감지 + let hasChanged = false; + const originalValue = currentGrade; + + gradeSelect.addEventListener('change', () => { + hasChanged = gradeSelect.value !== originalValue; + if (hasChanged) { + confirmBtn.textContent = '수정하시겠습니까? (Enter)'; + confirmBtn.style.background = '#28a745'; + confirmBtn.style.borderColor = '#28a745'; + } else { + confirmBtn.textContent = '확인 (Enter)'; + confirmBtn.style.background = '#007bff'; + confirmBtn.style.borderColor = '#007bff'; + } + }); + + // 확인/취소 처리 함수 + const handleConfirm = async () => { + const selectedGrade = gradeSelect.value; + + if (selectedGrade === originalValue) { + this.debugLog('등급 변경 없음', { selectedGrade, originalValue }); + modal.remove(); + return; + } + + try { + confirmBtn.textContent = '🔄 수정 중...'; + confirmBtn.disabled = true; + cancelBtn.disabled = true; + + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + this.debugLog('등급 업데이트 API 호출', { wordId, selectedGrade }); + + const updateRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + grade: selectedGrade, + updated_at: new Date().toISOString() + }) + }); + + this.debugLog('등급 업데이트 API 응답', { + status: updateRes.status, + statusText: updateRes.statusText, + ok: updateRes.ok + }); + + if (!updateRes.ok) { + const errorDetail = await updateRes.text(); + this.debugLog('등급 업데이트 실패', { + status: updateRes.status, + error: errorDetail + }); + throw new Error(`등급 업데이트 실패 (${updateRes.status}: ${updateRes.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('등급 수정 성공', { wordId, selectedGrade }); + alert(`✅ 등급이 "${selectedGrade}"로 변경되었습니다.`); + + // 모달 닫기 + modal.remove(); + + // 목록 새로고침 + await this.loadBannedWords(); + + } catch (error) { + this.debugLog('등급 수정 실패', { + error: error.message, + wordId, + selectedGrade: gradeSelect.value + }); + alert(`❌ 등급 수정 중 오류가 발생했습니다:\n${error.message}`); + + // 버튼 상태 복원 + confirmBtn.textContent = '확인 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + } + }; + + const handleCancel = () => { + this.debugLog('등급 수정 취소됨'); + modal.remove(); + }; + + // 이벤트 리스너 등록 + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + + // 키보드 이벤트 처리 + const handleKeydown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleConfirm(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancel(); + } + }; + + modal.addEventListener('keydown', handleKeydown); + + // 모달 외부 클릭 시 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + handleCancel(); + } + }); + + // 드롭박스에 포커스 + gradeSelect.focus(); + } + + // 금지어 삭제 (개선된 버전) + async deleteBannedWord(wordId, word) { + this.debugLog('금지어 삭제 요청', { wordId, word }); + + // 사용자 확인 + const confirmMessage = `⚠️ 금지어 삭제 확인\n\n단어: "${word}"\n\n정말로 삭제하시겠습니까?\n\n※ 삭제된 데이터는 복구할 수 없습니다.`; + + if (!confirm(confirmMessage)) { + this.debugLog('금지어 삭제 취소됨', { wordId, word }); + return; + } + + try { + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + this.debugLog('금지어 삭제 API 호출', { wordId, word }); + + const deleteRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + this.debugLog('금지어 삭제 API 응답', { + status: deleteRes.status, + statusText: deleteRes.statusText, + ok: deleteRes.ok + }); + + if (!deleteRes.ok) { + const errorDetail = await deleteRes.text(); + this.debugLog('금지어 삭제 실패', { + status: deleteRes.status, + error: errorDetail + }); + throw new Error(`금지어 삭제 실패 (${deleteRes.status}: ${deleteRes.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('금지어 삭제 성공', { wordId, word }); + alert(`✅ "${word}" 금지어가 삭제되었습니다.`); + + // 목록 새로고침 + await this.loadBannedWords(); + + } catch (error) { + this.debugLog('금지어 삭제 실패', { + error: error.message, + wordId, + word + }); + alert(`❌ 금지어 삭제 중 오류가 발생했습니다:\n${error.message}`); + } + } + + // 키프리스 결과 보기 (완전히 개선된 버전) + async viewKiprisResults(wordWordId, word) { + this.debugLog('키프리스 결과 보기 시작', { wordWordId, word }); + + 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 = `

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

`; + + // 모달 스크롤 스타일 적용 + modal.style.overflow = "auto"; + modal.style.maxHeight = "100vh"; + + // 모달 내용 컨테이너에 스크롤 스타일 적용 + const modalContent = modal.querySelector('.modal-content') || modal.querySelector('#kipris-modal > div'); + if (modalContent) { + modalContent.style.maxHeight = "90vh"; + modalContent.style.overflowY = "auto"; + modalContent.style.padding = "20px"; + modalContent.style.margin = "5vh auto"; + } + + // 결과 컨테이너에도 스크롤 스타일 적용 + results.style.maxHeight = "70vh"; + results.style.overflowY = "auto"; + results.style.padding = "10px"; + + try { + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + // word_id가 없거나 undefined인 경우 처리 + if (!wordWordId || wordWordId === 'undefined' || wordWordId === '') { + throw new Error('word_id가 없습니다. 데이터베이스에서 word_id 값을 확인해주세요.'); + } + + this.debugLog('키프리스 데이터 조회 API 호출', { + wordWordId, + word, + queryUrl: `${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc` + }); + + // 올바른 테이블 이름과 필드명 사용: word_id → banned_word_id + const resultsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + this.debugLog('키프리스 데이터 API 응답', { + status: resultsRes.status, + statusText: resultsRes.statusText, + ok: resultsRes.ok, + url: resultsRes.url + }); + + if (!resultsRes.ok) { + const errorDetail = await resultsRes.text(); + this.debugLog('키프리스 데이터 조회 실패', { + status: resultsRes.status, + error: errorDetail, + wordWordId, + word + }); + throw new Error(`키프리스 결과를 가져올 수 없습니다 (${resultsRes.status}: ${resultsRes.statusText})\n응답: ${errorDetail}`); + } + + const kiprisData = await resultsRes.json(); + this.debugLog('키프리스 결과 로드 성공', { + count: kiprisData.length, + wordWordId, + word, + sampleData: kiprisData.length > 0 ? { + hasApplicationStatus: !!kiprisData[0].application_status, + hasRegistrationDate: !!kiprisData[0].registration_date, + hasApplicantName: !!kiprisData[0].applicant_name, + hasClassificationCode: !!kiprisData[0].classification_code, + hasCategoryDescription: !!kiprisData[0].category_description, + hasDrawing: !!kiprisData[0].drawing, + bannedWordId: kiprisData[0].banned_word_id + } : null + }); + + if (kiprisData.length === 0) { + results.innerHTML = ` +
+
📭
+
키프리스 검색 결과가 없습니다
+
해당 금지어에 대한 상표 검색 결과가 없습니다.
+
검색 조건: banned_word_id = ${wordWordId}
+
+ `; + return; + } + + // 키프리스 결과 표시 (스크롤 가능한 컨테이너 내에) + results.innerHTML = ` +
+
+ 📊 검색 결과 요약: 총 ${kiprisData.length}개의 키프리스 데이터를 찾았습니다. +
+ ${kiprisData.map((item, index) => ` +
+
+

📋 검색 결과 ${index + 1}

+ 등록일: ${new Date(item.created_at).toLocaleDateString()} +
+ +
+
+
+ 📋 출원상태: +
+ ${item.application_status || '정보 없음'} +
+
+ +
+ 📅 등록일: +
+ ${item.registration_date ? new Date(item.registration_date).toLocaleDateString() : '정보 없음'} +
+
+ +
+ 👤 출원인: +
+ ${item.applicant_name || '정보 없음'} +
+
+ +
+ 🏷️ 분류코드: +
+ ${item.classification_code || '정보 없음'} +
+
+
+ +
+ ${item.drawing ? ` +
+ 🖼️ 상표 도면: +
+ 상표 도면 +
+ 🖼️ 이미지를 불러올 수 없습니다 +
+
+
+ ` : ` +
+ 🖼️ 상표 도면: +
+ 📷 도면 정보 없음 +
+
+ `} +
+
+ + +
+ 📝 분류설명: +
+ ${item.category_description || '정보 없음'} +
+
+
+ `).join('')} +
+ `; + + this.debugLog('키프리스 결과 UI 렌더링 완료', { count: kiprisData.length }); + + } catch (error) { + this.debugLog('키프리스 결과 로드 실패', { + error: error.message, + wordWordId, + word + }); + + results.innerHTML = ` +
+
+
오류가 발생했습니다
+
+ ${error.message} +
+
+ 디버그 정보: wordWordId=${wordWordId}, word="${word}" +
+
+ `; + } finally { + loading.style.display = "none"; + } + } + + // 로그 확인 함수 (토큰 상태 확인 대신) + showDebugLogs() { + this.debugLog('=== 디버그 로그 확인 ==='); + + // 현재 상태 정보 수집 + const statusInfo = { + '초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료', + 'SUPABASE_URL': this.SUPABASE_URL || '❌ 설정되지 않음', + 'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음', + 'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화', + '현재 시간': new Date().toLocaleString() + }; + + // 로그 정보를 문자열로 변환 + let logMessage = '🔍 현재 상태 정보:\n\n'; + for (const [key, value] of Object.entries(statusInfo)) { + logMessage += `${key}: ${value}\n`; + } + + // chrome.storage 상태 확인 + chrome.storage.local.get(null, (allData) => { + logMessage += '\n📦 Chrome Storage 내용:\n'; + logMessage += `- access_token: ${allData.access_token ? '있음' : '없음'}\n`; + logMessage += `- bannedWords_config: ${allData.bannedWords_config ? '있음' : '없음'}\n`; + + if (allData.bannedWords_config) { + const config = allData.bannedWords_config; + const age = Date.now() - config.timestamp; + logMessage += ` - 설정 나이: ${Math.floor(age / 1000)}초 전\n`; + logMessage += ` - URL: ${config.SUPABASE_URL ? '있음' : '없음'}\n`; + logMessage += ` - TOKEN: ${config.ACCESS_TOKEN ? '있음' : '없음'}\n`; + } + + // 콘솔에도 출력 + console.log('=== 디버그 로그 ==='); + console.log(statusInfo); + console.log('Chrome Storage:', allData); + + // 사용자에게 표시 + alert(logMessage); + + this.debugLog('디버그 로그 확인 완료'); + }); + } + + // 금지어 추가 모달 표시 + showAddBannedWordModal() { + this.debugLog('금지어 추가 모달 표시 시작'); + + // 모달 HTML 생성 + const modalId = 'add-banned-word-modal'; + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + const modalHtml = ` +
+
+

➕ 금지어 추가

+ +
+ + + + 💡 팁: 여러 단어를 한번에 등록하려면 콤마(,)로 구분하세요. 예: "단어1, 단어2, 단어3" + +
+ +
+ + +
+ +
+ + +
+
+
+ `; + + // 모달을 body에 추가 + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modal = document.getElementById(modalId); + const wordInput = document.getElementById('new-banned-word'); + const gradeSelect = document.getElementById('new-word-grade'); + const confirmBtn = document.getElementById('add-confirm-btn'); + const cancelBtn = document.getElementById('add-cancel-btn'); + + // 확인/취소 처리 함수 + const handleConfirm = async () => { + const inputText = wordInput.value.trim(); + const selectedGrade = gradeSelect.value; + + if (!inputText) { + alert('❌ 금지어를 입력해주세요.'); + wordInput.focus(); + return; + } + + // 콤마로 구분하여 여러 단어 처리 + const wordsToAdd = inputText.split(',').map(word => word.trim()).filter(word => word.length > 0); + + if (wordsToAdd.length === 0) { + alert('❌ 유효한 금지어를 입력해주세요.'); + wordInput.focus(); + return; + } + + this.debugLog('추가할 금지어 목록', { wordsToAdd, selectedGrade, count: wordsToAdd.length }); + + try { + confirmBtn.textContent = '🔄 추가 중...'; + confirmBtn.disabled = true; + cancelBtn.disabled = true; + + if (!this.ACCESS_TOKEN) { + throw new Error('로그인이 필요합니다'); + } + + // 현재 사용자 정보 가져오기 + const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!authRes.ok) { + throw new Error('사용자 인증 실패'); + } + + const authUser = await authRes.json(); + + // 사용자 ID 가져오기 + const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': '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; + + // 기존 금지어 목록 가져오기 (중복 검사용) + const existingRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=banned_word&user_id=eq.${userId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (!existingRes.ok) { + throw new Error('기존 금지어 목록 조회 실패'); + } + + const existingWords = await existingRes.json(); + const existingWordSet = new Set(existingWords.map(item => item.banned_word.toLowerCase())); + + // 중복 검사 + const duplicateWords = []; + const newWords = []; + + wordsToAdd.forEach(word => { + if (existingWordSet.has(word.toLowerCase())) { + duplicateWords.push(word); + } else { + newWords.push(word); + } + }); + + this.debugLog('중복 검사 결과', { + totalWords: wordsToAdd.length, + newWords: newWords.length, + duplicateWords: duplicateWords.length, + duplicates: duplicateWords + }); + + // 중복된 단어가 있으면 사용자에게 알림 + if (duplicateWords.length > 0) { + const duplicateMessage = `⚠️ 다음 단어는 이미 등록되어 있습니다:\n${duplicateWords.join(', ')}\n\n`; + + if (newWords.length > 0) { + const proceed = confirm(`${duplicateMessage}새로운 단어만 추가하시겠습니까?\n추가될 단어: ${newWords.join(', ')}`); + if (!proceed) { + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + return; + } + } else { + alert(`${duplicateMessage}추가할 새로운 단어가 없습니다.`); + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + return; + } + } + + if (newWords.length === 0) { + alert('❌ 추가할 새로운 금지어가 없습니다.'); + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + return; + } + + // 새로운 금지어들을 배치로 추가 + const wordsData = newWords.map(word => ({ + banned_word: word, + grade: selectedGrade, + user_id: userId, + created_at: new Date().toISOString() + })); + + this.debugLog('금지어 배치 추가 API 호출', { count: wordsData.length, grade: selectedGrade }); + + const addRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, + 'apikey': this.SUPABASE_ANON_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(wordsData) + }); + + this.debugLog('금지어 추가 API 응답', { + status: addRes.status, + statusText: addRes.statusText, + ok: addRes.ok + }); + + if (!addRes.ok) { + const errorDetail = await addRes.text(); + this.debugLog('금지어 추가 실패', { + status: addRes.status, + error: errorDetail + }); + throw new Error(`금지어 추가 실패 (${addRes.status}: ${addRes.statusText})\n응답: ${errorDetail}`); + } + + this.debugLog('금지어 추가 성공', { newWords, selectedGrade }); + + let successMessage = `✅ ${newWords.length}개의 금지어가 추가되었습니다.\n\n`; + successMessage += `등급: ${selectedGrade}\n`; + successMessage += `추가된 단어: ${newWords.join(', ')}`; + + if (duplicateWords.length > 0) { + successMessage += `\n\n⚠️ 중복으로 제외된 단어: ${duplicateWords.join(', ')}`; + } + + alert(successMessage); + + // 모달 닫기 + modal.remove(); + + // 목록 새로고침 + await this.loadBannedWords(); + + } catch (error) { + this.debugLog('금지어 추가 실패', { + error: error.message, + wordsToAdd, + selectedGrade + }); + alert(`❌ 금지어 추가 중 오류가 발생했습니다:\n${error.message}`); + + // 버튼 상태 복원 + confirmBtn.textContent = '➕ 추가 (Enter)'; + confirmBtn.disabled = false; + cancelBtn.disabled = false; + } + }; + + const handleCancel = () => { + this.debugLog('금지어 추가 취소됨'); + modal.remove(); + }; + + // 이벤트 리스너 등록 + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + + // 키보드 이벤트 처리 + const handleKeydown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleConfirm(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancel(); + } + }; + + modal.addEventListener('keydown', handleKeydown); + + // 모달 외부 클릭 시 닫기 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + handleCancel(); + } + }); + + // 입력란에 포커스 + wordInput.focus(); + } +} + +// 페이지 로드 시 초기화 +document.addEventListener('DOMContentLoaded', async () => { + console.log('=== 금지어 관리 페이지 초기화 시작 ==='); + console.log('현재 시간:', new Date().toLocaleString()); + + // chrome.storage에서 설정 확인 + let debugMode = false; // 기본값 + try { + const configData = await chrome.storage.local.get('bannedWords_config'); + console.log('chrome.storage 설정 확인:', { + hasBannedWordsConfig: !!configData.bannedWords_config, + config: configData.bannedWords_config ? { + hasUrl: !!configData.bannedWords_config.SUPABASE_URL, + hasKey: !!configData.bannedWords_config.SUPABASE_ANON_KEY, + hasToken: !!configData.bannedWords_config.ACCESS_TOKEN, + debugMode: configData.bannedWords_config.DEBUG_MODE, + timestamp: configData.bannedWords_config.timestamp + } : null + }); + + // DEBUG_MODE 설정 확인 + if (configData.bannedWords_config && configData.bannedWords_config.DEBUG_MODE !== undefined) { + debugMode = configData.bannedWords_config.DEBUG_MODE; + } + + } catch (error) { + console.error('chrome.storage 설정 확인 실패:', error); + } + + // 디버그 요소 표시/숨김 처리 + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + if (debugMode) { + debugElement.style.display = "block"; + console.log('디버그 모드 활성화 - 디버그 정보 표시'); + } else { + debugElement.style.display = "none"; + console.log('디버그 모드 비활성화 - 디버그 정보 숨김'); + } + } + + // 로그 확인 버튼 이벤트 등록 (DEBUG_MODE일 때만) + const logCheckBtn = document.getElementById('token-check-btn'); // ID는 그대로 유지 (HTML 변경 최소화) + if (logCheckBtn) { + if (debugMode) { + console.log('디버그 모드: 로그 확인 버튼 이벤트 등록'); + logCheckBtn.textContent = '📋 로그 확인'; // 버튼 텍스트 변경 + logCheckBtn.style.display = "inline-block"; + + // 기존 이벤트 리스너 제거 후 새로 등록 + logCheckBtn.replaceWith(logCheckBtn.cloneNode(true)); + const newLogCheckBtn = document.getElementById('token-check-btn'); + + newLogCheckBtn.addEventListener('click', async (e) => { + e.preventDefault(); + console.log('로그 확인 버튼 클릭됨'); + + // 버튼 상태 변경 + const originalText = newLogCheckBtn.textContent; + newLogCheckBtn.textContent = '🔄 확인 중...'; + newLogCheckBtn.disabled = true; + + try { + // BannedWordsManager가 초기화되었는지 확인 + if (window.bannedWordsManager && typeof window.bannedWordsManager.showDebugLogs === 'function') { + await window.bannedWordsManager.showDebugLogs(); + } else { + console.error('BannedWordsManager가 초기화되지 않았습니다'); + alert('❌ 관리자가 초기화되지 않았습니다. 페이지를 새로고침해주세요.'); + } + } finally { + // 버튼 상태 복원 + newLogCheckBtn.textContent = originalText; + newLogCheckBtn.disabled = false; + } + }); + console.log('로그 확인 버튼 이벤트 등록 완료'); + } else { + console.log('디버그 모드 비활성화: 로그 확인 버튼 숨김'); + logCheckBtn.style.display = "none"; + } + } else { + if (debugMode) { + console.warn('로그 확인 버튼을 찾을 수 없음'); + } + } + + try { + // BannedWordsManager 초기화 + console.log('BannedWordsManager 인스턴스 생성 중...'); + window.bannedWordsManager = new BannedWordsManager(); + + console.log('BannedWordsManager 초기화 시작...'); + await window.bannedWordsManager.initialize(); + + console.log('✅ 금지어 관리 페이지 초기화 완료'); + + } catch (error) { + console.error('❌ 초기화 실패:', error); + + // UI에 오류 표시 + const statsElement = document.getElementById('banned-words-stats'); + if (statsElement) { + statsElement.innerHTML = ` +
+ ❌ 초기화 실패
+ ${error.message}
+ 로그인 후 다시 시도해주세요. +
+ `; + } + + const tbody = document.getElementById("banned-words-tbody"); + if (tbody) { + tbody.innerHTML = `초기화 실패: ${error.message}`; + } + + // 디버그 정보에도 표시 (DEBUG_MODE일 때만) + if (debugMode) { + const debugElement = document.getElementById('debug-info'); + if (debugElement) { + debugElement.innerHTML = `❌ 초기화 실패: ${error.message}
`; + debugElement.style.display = "block"; + + // 버튼 이벤트 다시 등록 + const newBtn = document.getElementById('token-check-btn'); + if (newBtn) { + newBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + // 간단한 로그 정보 표시 + const logInfo = `❌ 초기화 실패 상태\n\n오류: ${error.message}\n\n시간: ${new Date().toLocaleString()}`; + alert(logInfo); + }); + } + } + } + } + + console.log('=== DOMContentLoaded 이벤트 처리 완료 ==='); +}); + +// 전역 함수로 등록 (HTML에서 호출할 수 있도록) +window.bannedWordsManager = null; \ No newline at end of file diff --git a/manifest.json b/manifest.json index d3c8037..2f2c83f 100644 --- a/manifest.json +++ b/manifest.json @@ -27,5 +27,11 @@ "action": { "default_popup": "popup.html", "default_icon": "icon.png" - } + }, + "web_accessible_resources": [ + { + "resources": ["bannedWords.html", "bannedWords.js"], + "matches": [""] + } + ] } diff --git a/popup.html b/popup.html index 76abc19..18e501a 100644 --- a/popup.html +++ b/popup.html @@ -12,6 +12,8 @@ background: #f4f6f9; padding: 20px; width: 320px; + height: 100vh; + overflow: hidden; } h2 { text-align: center; @@ -130,41 +132,55 @@ background: #219a52; } - /* 모달 스타일 */ + /* 모달 스타일 수정 */ .modal { position: fixed; z-index: 1000; left: 0; top: 0; - width: 100%; - height: 100%; + width: 100vw; + height: 100vh; background-color: rgba(0,0,0,0.5); + display: none; } .modal-content { - background-color: white; - margin: 5% auto; - padding: 0; - border-radius: 8px; - width: 90%; - max-width: 600px; - max-height: 80%; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); + position: relative; + background-color: #fefefe; + margin: 15px auto; + padding: 20px; + border: 1px solid #888; + width: 80%; + max-width: 900px; + min-width: 400px; + min-height: 300px; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .modal-header { - background: #3498db; - color: white; - padding: 15px 20px; - display: flex; - justify-content: space-between; - align-items: center; + cursor: move; + padding: 10px; + margin: -20px -20px 20px -20px; + background: #f8f9fa; + border-bottom: 1px solid #dee2e6; + border-radius: 5px 5px 0 0; + user-select: none; } - .modal-header h3 { - margin: 0; - font-size: 18px; + .resizer { + width: 10px; + height: 10px; + background: #6c757d; + position: absolute; + right: 0; + bottom: 0; + cursor: se-resize; + border-radius: 0 0 5px 0; + } + + .resizer:hover { + background: #5a6268; } .close { @@ -180,26 +196,55 @@ .modal-body { padding: 20px; - max-height: 500px; + height: calc(100% - 56px); /* 헤더 높이를 제외한 나머지 */ overflow-y: auto; } + + /* 테이블 컨테이너 높이 조정 */ + .banned-words-table-container { + height: calc(100% - 80px); /* 통계 정보 높이를 제외한 나머지 */ + overflow: auto; + } + + #banned-words-table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; + font-size: 14px; + } + + /* 테이블 헤더 고정 */ + #banned-words-table thead { + position: sticky; + top: 0; + background: #f8f9fa; + z-index: 1; + } + + /* 통계 정보 스타일 */ + .stats-info { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 15px; + margin-bottom: 20px; + font-size: 14px; + font-weight: bold; + color: #495057; + text-align: center; + } /* 테이블 스타일 */ .banned-words-table-container { overflow-x: auto; } - #banned-words-table { - width: 100%; - border-collapse: collapse; - margin-top: 10px; - } - #banned-words-table th, #banned-words-table td { border: 1px solid #ddd; - padding: 8px; + padding: 12px 8px; text-align: left; + white-space: nowrap; } #banned-words-table th { @@ -211,14 +256,24 @@ background-color: #f9f9f9; } + /* 순번 열 스타일 */ + #banned-words-table th:first-child, + #banned-words-table td:first-child { + width: 60px; + text-align: center; + } + /* 액션 버튼 스타일 */ .action-btn { - padding: 4px 8px; + padding: 6px 10px; margin: 2px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; + white-space: nowrap; + display: inline-block; + min-width: 40px; } .view-btn { @@ -242,13 +297,13 @@ /* 키프리스 결과 스타일 */ .kipris-results-container { - max-height: 400px; + max-height: 500px; overflow-y: auto; } .kipris-item { border: 1px solid #ddd; - margin-bottom: 10px; + margin-bottom: 15px; padding: 15px; border-radius: 6px; background: #f9f9f9; @@ -275,12 +330,17 @@ border-radius: 4px; } - /* 등급 수정 입력 필드 */ - .grade-input { - width: 60px; - padding: 2px 4px; - border: 1px solid #ddd; - border-radius: 3px; + /* 등급 표시 스타일 */ + .grade-display { + display: inline-block; + padding: 4px 8px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + color: #495057; + min-width: 60px; text-align: center; } @@ -338,23 +398,31 @@
+
+ + + -