SearchTrademark/bannedWords.js

1410 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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