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 @@
+
+
+
+
-
+
+
+
+
+ | 순번 |
금지어 |
등급 |
작업 |
@@ -373,7 +441,7 @@
-