// 금지어 관리 모듈
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 = `
`;
// 금지어 추가 버튼 이벤트 등록
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;