// 금지어 관리 모듈 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;