1410 lines
52 KiB
JavaScript
1410 lines
52 KiB
JavaScript
// 금지어 관리 모듈
|
||
class BannedWordsManager {
|
||
constructor() {
|
||
// 초기값 설정 (chrome.storage에서 로드될 때까지 임시)
|
||
// SUPABASE 설정은 background.js에서 중앙 관리됨
|
||
this.DEBUG_MODE = true;
|
||
this.ACCESS_TOKEN = null;
|
||
this.isConfigLoaded = false;
|
||
this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지
|
||
this.userId = null; // 사용자 ID 저장
|
||
|
||
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에서 설정 로드', {
|
||
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.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']);
|
||
|
||
this.debugLog('개별 설정 조회 결과', {
|
||
hasToken: !!storageData.access_token
|
||
});
|
||
|
||
this.ACCESS_TOKEN = storageData.access_token;
|
||
}
|
||
|
||
this.isConfigLoaded = true;
|
||
|
||
this.debugLog('설정 로드 완료', {
|
||
hasToken: !!this.ACCESS_TOKEN,
|
||
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
|
||
DEBUG_MODE: this.DEBUG_MODE,
|
||
isConfigLoaded: this.isConfigLoaded
|
||
});
|
||
|
||
// 설정 검증 - SUPABASE 설정은 background.js에서 관리
|
||
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}<br>${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 호출 준비 (background.js 경유)', {
|
||
hasToken: !!this.ACCESS_TOKEN,
|
||
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
|
||
tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none'
|
||
});
|
||
|
||
// background.js를 통해 토큰 검증
|
||
const response = await chrome.runtime.sendMessage({
|
||
action: 'validateToken',
|
||
token: this.ACCESS_TOKEN
|
||
});
|
||
|
||
this.debugLog('토큰 검증 API 응답', {
|
||
success: response?.success,
|
||
hasUser: !!response?.user
|
||
});
|
||
|
||
if (!response || !response.success) {
|
||
throw new Error(response?.error || '토큰 검증 실패');
|
||
}
|
||
|
||
const userData = response.user;
|
||
this.debugLog('토큰 검증 성공', {
|
||
email: userData.email,
|
||
id: userData.id
|
||
});
|
||
|
||
return userData;
|
||
|
||
} catch (error) {
|
||
this.debugLog('토큰 검증 중 오류', {
|
||
errorName: error.name,
|
||
errorMessage: error.message
|
||
});
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 에러 렌더링 헬퍼 함수
|
||
renderError(message) {
|
||
const tbody = document.getElementById("banned-words-tbody");
|
||
const statsDiv = document.getElementById("banned-words-stats");
|
||
|
||
if (tbody) {
|
||
tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">오류: ${message}</td></tr>`;
|
||
}
|
||
|
||
if (statsDiv) {
|
||
statsDiv.innerHTML = `<div style="color: red;">오류: ${message}</div>`;
|
||
}
|
||
|
||
this.debugLog('에러 렌더링 완료', { message });
|
||
}
|
||
|
||
// 금지어 목록 로드
|
||
async loadBannedWords() {
|
||
this.debugLog('금지어 목록 로드 시작 (background.js 경유)');
|
||
|
||
const loading = document.getElementById("banned-words-loading");
|
||
const tbody = document.getElementById("banned-words-tbody");
|
||
|
||
loading.style.display = "block";
|
||
tbody.innerHTML = "";
|
||
|
||
try {
|
||
// background.js를 통해 금지어 목록 조회
|
||
const response = await chrome.runtime.sendMessage({
|
||
action: 'getBannedWords',
|
||
token: this.ACCESS_TOKEN
|
||
});
|
||
|
||
this.debugLog('금지어 목록 API 응답', {
|
||
success: response?.success,
|
||
count: response?.bannedWords?.length
|
||
});
|
||
|
||
if (!response || !response.success) {
|
||
throw new Error(response?.error || '금지어 목록 조회 실패');
|
||
}
|
||
|
||
const bannedWords = response.bannedWords;
|
||
this.userId = response.userId; // 사용자 ID 저장 (추가 시 필요)
|
||
|
||
this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length });
|
||
|
||
this.displayBannedWords(bannedWords);
|
||
} catch (error) {
|
||
this.debugLog('금지어 목록 로드 실패', {
|
||
errorName: error.name,
|
||
errorMessage: error.message
|
||
});
|
||
|
||
tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">오류: ${error.message}</td></tr>`;
|
||
} 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 = '<tr><td colspan="4" style="text-align: center; color: #666;">등록된 금지어가 없습니다.</td></tr>';
|
||
statsDiv.innerHTML = '<div>총 금지어 개수: 0개</div>';
|
||
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 = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>${statsText}</div>
|
||
<button id="add-banned-word-btn" style="
|
||
padding: 8px 16px;
|
||
background: #28a745;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
">
|
||
➕ 금지어 추가
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 금지어 추가 버튼 이벤트 등록
|
||
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 `
|
||
<tr data-word-id="${word.word_id || word.id || ''}" data-word-text="${word.banned_word}" data-word-grade="${word.grade || ''}" data-word-word-id="${word.word_id || ''}" style="background-color: ${gradeBgColor};">
|
||
<td>${index + 1}</td>
|
||
<td><strong>${word.banned_word}</strong></td>
|
||
<td>
|
||
<span class="grade-display" style="display: inline-block; padding: 6px 12px; border: 1px solid #dee2e6; border-radius: 6px; font-size: 12px; font-weight: bold; min-width: 70px; text-align: center; ${gradeStyle}">${word.grade || '값 없음'}</span>
|
||
</td>
|
||
<td>
|
||
<button class="action-btn view-btn" data-action="view" data-word-id="${word.word_id || word.id || ''}" data-word-word-id="${word.word_id || ''}" data-word-text="${word.banned_word}">보기</button>
|
||
<button class="action-btn edit-btn" data-action="edit" data-word-id="${word.word_id || word.id || ''}" data-word-grade="${word.grade || ''}">수정</button>
|
||
<button class="action-btn delete-btn" data-action="delete" data-word-id="${word.word_id || word.id || ''}" data-word-text="${word.banned_word}">삭제</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).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 = `
|
||
<div id="${modalId}" style="
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
">
|
||
<div style="
|
||
background: white;
|
||
padding: 24px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
min-width: 400px;
|
||
max-width: 500px;
|
||
">
|
||
<h3 style="margin: 0 0 16px 0; color: #333;">🔧 등급 수정</h3>
|
||
<p style="margin: 0 0 16px 0; color: #666;">현재 등급: <strong>${currentGrade || '값 없음'}</strong></p>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #555;">새로운 등급 선택:</label>
|
||
<select id="grade-select" style="
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
background: white;
|
||
">
|
||
<option value="금지" ${currentGrade === '금지' ? 'selected' : ''}>금지</option>
|
||
<option value="비허용" ${currentGrade === '비허용' ? 'selected' : ''}>비허용</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 12px; justify-content: flex-end;">
|
||
<button id="grade-cancel-btn" style="
|
||
padding: 8px 16px;
|
||
border: 2px solid #ccc;
|
||
background: white;
|
||
color: #666;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
">취소 (ESC)</button>
|
||
<button id="grade-confirm-btn" style="
|
||
padding: 8px 16px;
|
||
border: 2px solid #007bff;
|
||
background: #007bff;
|
||
color: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
">확인 (Enter)</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 모달을 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 호출 (background.js 경유)', { wordId, selectedGrade });
|
||
|
||
// background.js를 통해 등급 수정
|
||
const response = await chrome.runtime.sendMessage({
|
||
action: 'updateBannedWordGrade',
|
||
token: this.ACCESS_TOKEN,
|
||
wordId: wordId,
|
||
grade: selectedGrade
|
||
});
|
||
|
||
this.debugLog('등급 업데이트 API 응답', {
|
||
success: response?.success
|
||
});
|
||
|
||
if (!response || !response.success) {
|
||
throw new Error(response?.error || '등급 업데이트 실패');
|
||
}
|
||
|
||
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 호출 (background.js 경유)', { wordId, word });
|
||
|
||
// background.js를 통해 금지어 삭제
|
||
const response = await chrome.runtime.sendMessage({
|
||
action: 'deleteBannedWord',
|
||
token: this.ACCESS_TOKEN,
|
||
wordId: wordId
|
||
});
|
||
|
||
this.debugLog('금지어 삭제 API 응답', {
|
||
success: response?.success
|
||
});
|
||
|
||
if (!response || !response.success) {
|
||
throw new Error(response?.error || '금지어 삭제 실패');
|
||
}
|
||
|
||
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 = `<h4>🔍 "${word}" 키프리스 검색 결과</h4>`;
|
||
|
||
// 모달 스크롤 스타일 적용
|
||
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 호출 (background.js 경유)', {
|
||
wordWordId,
|
||
word
|
||
});
|
||
|
||
// background.js를 통해 키프리스 결과 조회
|
||
const response = await chrome.runtime.sendMessage({
|
||
action: 'getKiprisResults',
|
||
token: this.ACCESS_TOKEN,
|
||
wordId: wordWordId
|
||
});
|
||
|
||
this.debugLog('키프리스 데이터 API 응답', {
|
||
success: response?.success,
|
||
count: response?.kiprisData?.length
|
||
});
|
||
|
||
if (!response || !response.success) {
|
||
throw new Error(response?.error || '키프리스 결과 조회 실패');
|
||
}
|
||
|
||
const kiprisData = response.kiprisData;
|
||
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,
|
||
hasTradeMarkName: !!kiprisData[0].tradeMark_name
|
||
} : null
|
||
});
|
||
|
||
if (kiprisData.length === 0) {
|
||
results.innerHTML = `
|
||
<div style="text-align: center; color: #666; padding: 40px;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📭</div>
|
||
<div style="font-size: 18px; margin-bottom: 8px;">키프리스 검색 결과가 없습니다</div>
|
||
<div style="font-size: 14px; color: #999;">해당 금지어에 대한 상표 검색 결과가 없습니다.</div>
|
||
<div style="font-size: 12px; color: #ccc; margin-top: 8px;">검색 조건: banned_word_id = ${wordWordId}</div>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 키프리스 결과 표시 (스크롤 가능한 컨테이너 내에)
|
||
results.innerHTML = `
|
||
<div style="padding-bottom: 20px;">
|
||
<div style="background: #f0f8ff; padding: 12px; border-radius: 8px; margin-bottom: 16px; border-left: 4px solid #007bff;">
|
||
<strong>📊 검색 결과 요약:</strong> 총 ${kiprisData.length}개의 키프리스 데이터를 찾았습니다.
|
||
</div>
|
||
${kiprisData.map((item, index) => {
|
||
// 상표명 불일치 검사 (빈값이 아닐 때만)
|
||
const tradeMarkName = item.tradeMark_name;
|
||
const isNameMismatch = tradeMarkName &&
|
||
tradeMarkName.trim() !== '' &&
|
||
word &&
|
||
word.trim() !== '' &&
|
||
tradeMarkName.toLowerCase() !== word.toLowerCase() &&
|
||
!tradeMarkName.toLowerCase().includes(word.toLowerCase()) &&
|
||
!word.toLowerCase().includes(tradeMarkName.toLowerCase());
|
||
|
||
return `
|
||
<div class="kipris-item" style="border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 16px; background: #f9f9f9;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||
<h4 style="margin: 0; color: #333;">📋 검색 결과 ${index + 1}</h4>
|
||
<small style="color: #666;">등록일: ${new Date(item.created_at).toLocaleDateString()}</small>
|
||
</div>
|
||
|
||
<!-- 상표명 불일치 경고 표시 -->
|
||
${isNameMismatch ? `
|
||
<div style="background: #ffebee; border: 1px solid #f44336; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||
<span style="color: #f44336; font-size: 18px; margin-right: 8px;">⚠️</span>
|
||
<strong style="color: #f44336;">상표명 불일치 감지</strong>
|
||
</div>
|
||
<div style="font-size: 14px; color: #666;">
|
||
<div><strong>검색 키워드:</strong> "${word}"</div>
|
||
<div><strong>등록 상표명:</strong> <span style="color: red; font-weight: bold;">"${tradeMarkName}" (확인필요)</span></div>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="kipris-content" style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
||
<div class="kipris-info">
|
||
<div class="kipris-field" style="margin-bottom: 12px;">
|
||
<strong style="color: #555;">🏷️ 상표명:</strong>
|
||
<div style="padding: 4px 8px; background: ${tradeMarkName ? '#fff3e0' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||
${isNameMismatch ?
|
||
`<span style="color: red; font-weight: bold;">${tradeMarkName} 불일치(확인필요)</span>` :
|
||
(tradeMarkName || '정보 없음')
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kipris-field" style="margin-bottom: 12px;">
|
||
<strong style="color: #555;">📋 출원상태:</strong>
|
||
<div style="padding: 4px 8px; background: ${item.application_status ? '#e3f2fd' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||
${item.application_status || '정보 없음'}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kipris-field" style="margin-bottom: 12px;">
|
||
<strong style="color: #555;">📅 등록일:</strong>
|
||
<div style="padding: 4px 8px; background: ${item.registration_date ? '#e8f5e8' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||
${item.registration_date ? new Date(item.registration_date).toLocaleDateString() : '정보 없음'}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kipris-field" style="margin-bottom: 12px;">
|
||
<strong style="color: #555;">👤 출원인:</strong>
|
||
<div style="padding: 4px 8px; background: ${item.applicant_name ? '#fff3e0' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||
${item.applicant_name || '정보 없음'}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kipris-field" style="margin-bottom: 12px;">
|
||
<strong style="color: #555;">🏷️ 분류코드:</strong>
|
||
<div style="padding: 4px 8px; background: ${item.classification_code ? '#f3e5f5' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||
${item.classification_code || '정보 없음'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kipris-drawing">
|
||
${item.drawing ? `
|
||
<div class="kipris-field">
|
||
<strong style="color: #555;">🖼️ 상표 도면:</strong>
|
||
<div style="margin-top: 8px; text-align: center;">
|
||
<img src="${item.drawing}"
|
||
alt="상표 도면"
|
||
class="kipris-drawing-img"
|
||
style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; background: white;"
|
||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||
<div style="display: none; padding: 20px; background: #f5f5f5; border-radius: 4px; color: #666;">
|
||
🖼️ 이미지를 불러올 수 없습니다
|
||
</div>
|
||
</div>
|
||
</div>
|
||
` : `
|
||
<div class="kipris-field">
|
||
<strong style="color: #555;">🖼️ 상표 도면:</strong>
|
||
<div style="margin-top: 8px; padding: 40px; background: #f5f5f5; border-radius: 4px; text-align: center; color: #999;">
|
||
📷 도면 정보 없음
|
||
</div>
|
||
</div>
|
||
`}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 분류설명을 전체 폭으로 표시 -->
|
||
<div class="kipris-field" style="margin-top: 16px;">
|
||
<strong style="color: #555;">📝 분류설명:</strong>
|
||
<div style="padding: 12px; background: ${item.category_description ? '#e0f2f1' : '#f5f5f5'}; border-radius: 4px; margin-top: 8px; white-space: pre-wrap; word-break: break-word; line-height: 1.5;">
|
||
${item.category_description || '정보 없음'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
`;
|
||
|
||
this.debugLog('키프리스 결과 UI 렌더링 완료', { count: kiprisData.length });
|
||
|
||
} catch (error) {
|
||
this.debugLog('키프리스 결과 로드 실패', {
|
||
error: error.message,
|
||
wordWordId,
|
||
word
|
||
});
|
||
|
||
results.innerHTML = `
|
||
<div style="text-align: center; color: red; padding: 40px;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">❌</div>
|
||
<div style="font-size: 18px; margin-bottom: 8px;">오류가 발생했습니다</div>
|
||
<div style="font-size: 14px; background: #ffebee; padding: 12px; border-radius: 4px; word-break: break-word;">
|
||
${error.message}
|
||
</div>
|
||
<div style="font-size: 12px; color: #999; margin-top: 8px;">
|
||
디버그 정보: wordWordId=${wordWordId}, word="${word}"
|
||
</div>
|
||
</div>
|
||
`;
|
||
} finally {
|
||
loading.style.display = "none";
|
||
}
|
||
}
|
||
|
||
// 로그 확인 함수 (토큰 상태 확인 대신)
|
||
showDebugLogs() {
|
||
this.debugLog('=== 디버그 로그 확인 ===');
|
||
|
||
// 현재 상태 정보 수집
|
||
const statusInfo = {
|
||
'초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료',
|
||
'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음',
|
||
'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화',
|
||
'사용자 ID': this.userId || '❌ 없음',
|
||
'현재 시간': 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 = `
|
||
<div id="${modalId}" style="
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
">
|
||
<div style="
|
||
background: white;
|
||
padding: 24px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
min-width: 500px;
|
||
max-width: 600px;
|
||
">
|
||
<h3 style="margin: 0 0 16px 0; color: #333;">➕ 금지어 추가</h3>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #555;">금지어 입력:</label>
|
||
<input type="text" id="new-banned-word" placeholder="금지어를 입력하세요 (콤마로 구분하여 여러 단어 입력 가능)" style="
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
box-sizing: border-box;
|
||
">
|
||
<small style="color: #666; font-size: 12px; margin-top: 4px; display: block;">
|
||
💡 팁: 여러 단어를 한번에 등록하려면 콤마(,)로 구분하세요. 예: "단어1, 단어2, 단어3"
|
||
</small>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #555;">등급 선택:</label>
|
||
<select id="new-word-grade" style="
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
background: white;
|
||
">
|
||
<option value="비허용" selected>비허용 (기본값)</option>
|
||
<option value="금지">금지</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 12px; justify-content: flex-end;">
|
||
<button id="add-cancel-btn" style="
|
||
padding: 10px 20px;
|
||
border: 2px solid #ccc;
|
||
background: white;
|
||
color: #666;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
">취소 (ESC)</button>
|
||
<button id="add-confirm-btn" style="
|
||
padding: 10px 20px;
|
||
border: 2px solid #28a745;
|
||
background: #28a745;
|
||
color: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
">➕ 추가 (Enter)</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 모달을 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('추가할 금지어 목록 (background.js 경유)', { wordsToAdd, selectedGrade, count: wordsToAdd.length });
|
||
|
||
try {
|
||
confirmBtn.textContent = '🔄 추가 중...';
|
||
confirmBtn.disabled = true;
|
||
cancelBtn.disabled = true;
|
||
|
||
if (!this.ACCESS_TOKEN) {
|
||
throw new Error('로그인이 필요합니다');
|
||
}
|
||
|
||
// 사용자 ID 확인 (loadBannedWords에서 저장됨)
|
||
const userId = this.userId;
|
||
if (!userId) {
|
||
throw new Error('사용자 ID를 찾을 수 없습니다. 페이지를 새로고침해주세요.');
|
||
}
|
||
|
||
// background.js를 통해 기존 금지어 목록 조회 (중복 검사용)
|
||
const existingResponse = await chrome.runtime.sendMessage({
|
||
action: 'getExistingBannedWords',
|
||
token: this.ACCESS_TOKEN,
|
||
userId: userId
|
||
});
|
||
|
||
if (!existingResponse || !existingResponse.success) {
|
||
throw new Error(existingResponse?.error || '기존 금지어 목록 조회 실패');
|
||
}
|
||
|
||
const existingWords = existingResponse.existingWords;
|
||
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 호출 (background.js 경유)', { count: wordsData.length, grade: selectedGrade });
|
||
|
||
// background.js를 통해 금지어 추가
|
||
const addResponse = await chrome.runtime.sendMessage({
|
||
action: 'addBannedWords',
|
||
token: this.ACCESS_TOKEN,
|
||
userId: userId,
|
||
wordsData: wordsData
|
||
});
|
||
|
||
this.debugLog('금지어 추가 API 응답', {
|
||
success: addResponse?.success
|
||
});
|
||
|
||
if (!addResponse || !addResponse.success) {
|
||
throw new Error(addResponse?.error || '금지어 추가 실패');
|
||
}
|
||
|
||
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 = `
|
||
<div style="color: red; text-align: center; padding: 20px;">
|
||
❌ 초기화 실패<br>
|
||
${error.message}<br>
|
||
<small>로그인 후 다시 시도해주세요.</small>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const tbody = document.getElementById("banned-words-tbody");
|
||
if (tbody) {
|
||
tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">초기화 실패: ${error.message}</td></tr>`;
|
||
}
|
||
|
||
// 디버그 정보에도 표시 (DEBUG_MODE일 때만)
|
||
if (debugMode) {
|
||
const debugElement = document.getElementById('debug-info');
|
||
if (debugElement) {
|
||
debugElement.innerHTML = `❌ 초기화 실패: ${error.message}<br><button id="token-check-btn" style="display: inline-block;">📋 로그 확인</button>`;
|
||
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; |