// 타냐대장경 관리 모듈 class SayingsManager { constructor() { // 기본 설정 this.config = null; this.allSayings = []; this.filteredSayings = []; this.currentUser = null; this.currentEditingSaying = null; // 현재 편집 중인 타냐대장경 this.searchTimeout = null; this.newSayingsCheckInterval = null; this.debugLogs = []; // 디버그 모드 설정 this.debugMode = false; // 백그라운드 연동 설정 this.isBackgroundConnected = false; this.lastSayingsCount = 0; this.debugLog('SayingsManager 생성자 호출'); } // 설정 로드 함수 async loadConfig() { try { this.debugLog('chrome.storage에서 설정 로드 시작'); // 1. sayings_config 우선 확인 const configData = await chrome.storage.local.get('sayings_config'); if (configData.sayings_config) { const config = configData.sayings_config; this.debugLog('sayings_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('sayings_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.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.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(`[Sayings] ${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('SayingsManager 초기화 시작'); // 1) 설정 로드 (chrome.storage에서) this.debugLog('설정 로드 중...'); await this.loadConfig(); // 2) 토큰 유효성 검증 this.debugLog('토큰 유효성 검증 중...'); await this.validateToken(); // 3) 현재 사용자 정보 로드 this.debugLog('현재 사용자 정보 로드 중...'); await this.loadCurrentUser(); // 4) 타냐대장경 등록 모달 이벤트 등록 this.debugLog('타냐대장경 등록 모달 이벤트 등록 중...'); this.attachAddSayingEvents(); // 5) 카테고리와 타겟 옵션 로드 this.debugLog('카테고리와 타겟 옵션 로드 중...'); await this.loadCategoryAndTargetOptions(); // 6) 타냐대장경 목록 로드 this.debugLog('타냐대장경 목록 로드 시작'); await this.loadSayings(); // 7) 새 타냐대장경 감지 시작 this.debugLog('새 타냐대장경 감지 기능 시작'); this.startNewSayingsMonitoring(); this.debugLog('SayingsManager 초기화 완료'); } 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 }); 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' }, mode: 'cors', credentials: 'omit' }); this.debugLog('토큰 검증 API 응답', { status: authRes.status, statusText: authRes.statusText, ok: authRes.ok }); if (!authRes.ok) { const errorText = await authRes.text(); this.debugLog('토큰 검증 실패', { status: authRes.status, error: errorText }); throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})`); } const userData = await authRes.json(); this.debugLog('토큰 검증 성공', { email: userData.email, id: userData.id }); return userData; } catch (error) { this.debugLog('토큰 검증 중 오류', { errorName: error.name, errorMessage: error.message }); if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.`); } throw error; } } // 타냐대장경 목록 로드 async loadSayings() { this.debugLog('타냐대장경 로딩 시작'); try { // 로딩 표시 document.getElementById('sayings-container').innerHTML = '
📚 타냐대장경을 불러오는 중...
'; // API 호출을 위한 헤더 설정 const headers = { 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, 'apikey': this.SUPABASE_ANON_KEY, 'Accept': 'application/json', 'Content-Type': 'application/json' }; // 승인된 타냐대장경만 가져오는 쿼리 const query = `${this.SUPABASE_URL}/rest/v1/tanya_sayings?admin_approval=eq.true&order=created_at.desc`; this.debugLog('타냐대장경 API 호출', { url: query, headers: Object.keys(headers) }); const response = await fetch(query, { method: 'GET', headers: headers }); if (!response.ok) { const errorText = await response.text(); this.debugLog('타냐대장경 로딩 실패', { status: response.status, statusText: response.statusText, error: errorText }); throw new Error(`타냐대장경 로딩 실패: ${response.status} ${response.statusText}\n${errorText}`); } const sayings = await response.json(); this.debugLog('타냐대장경 로딩 성공', { count: sayings.length, firstSaying: sayings.length > 0 ? sayings[0].saying_title : 'N/A' }); // 데이터 저장 this.allSayings = sayings; this.filteredSayings = [...sayings]; // 필터 이벤트 연결 this.attachFilterEvents(); // 타냐대장경 표시 await this.displaySayings(); } catch (error) { this.debugLog('타냐대장경 로딩 에러', { error: error.message }); this.renderError('타냐대장경을 불러오는데 실패했습니다', error.message); } } // 타냐대장경 표시 async displaySayings() { this.debugLog('타냐대장경 표시 시작'); const container = document.getElementById('sayings-container'); const stats = this.calculateStats(); // 통계 업데이트 const statsText = `총 ${stats.total}개의 타냐대장경 (${Object.entries(stats.byCategory).map(([cat, count]) => `${cat}: ${count}개`).join(', ')})`; document.getElementById('sayings-stats').textContent = statsText; this.debugLog('타냐대장경 통계 계산 완료', stats); if (this.filteredSayings.length === 0) { container.innerHTML = '
📝 표시할 타냐대장경이 없습니다.
'; this.debugLog('표시할 타냐대장경 없음'); return; } // 타냐대장경 카드 생성 (비동기 처리) const sayingCards = await Promise.all(this.filteredSayings.map(async saying => { // 콘텐츠 렌더링 (블록 데이터 우선) let contentHtml = ''; if (saying.content_blocks && Array.isArray(saying.content_blocks) && saying.content_blocks.length > 0) { // 블록 데이터가 있는 경우 contentHtml = this.renderContentBlocks(saying.content_blocks); } else { // 기존 마크다운 방식 contentHtml = await this.convertMarkdownToHtml(saying.saying); } // 날짜 포맷팅 const date = new Date(saying.created_at); const formattedDate = date.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' }); // 등록자 정보 (register 필드 사용) const authorName = saying.register || saying.user_name || '알 수 없음'; return `

${this.escapeHtml(saying.saying_title)}

${this.escapeHtml(saying.cat)} ${this.escapeHtml(saying.target)}
${contentHtml}
`; })); container.innerHTML = sayingCards.join(''); // 카드 클릭 이벤트 추가 const cardElements = container.querySelectorAll('.saying-card'); cardElements.forEach((cardElement, index) => { cardElement.addEventListener('click', (e) => { // 삭제 버튼이나 다른 버튼 클릭 시 모달이 열리지 않도록 함 if (e.target.closest('.delete-btn') || e.target.closest('button')) { return; } this.openDetailModal(this.filteredSayings[index]); }); }); this.debugLog('타냐대장경 표시 완료', { filteredCount: this.filteredSayings.length, cardsGenerated: sayingCards.length }); } // 블록 데이터 렌더링 함수 renderContentBlocks(blocks) { if (!blocks || !Array.isArray(blocks) || blocks.length === 0) { return ''; } return blocks.map(block => { if (block.type === 'text') { return `
${this.escapeHtml(block.content || '').replace(/\n/g, '
')}
`; } else if (block.type === 'image') { if (block.content) { return `
${this.escapeHtml(block.alt || '')} ${block.alt ? `
${this.escapeHtml(block.alt)}
` : ''}
`; } } return ''; }).join(''); } // 전체 화면 이미지 표시 함수 showFullImage(imageSrc) { let modal = document.querySelector('.full-image-modal'); if (!modal) { modal = document.createElement('div'); modal.className = 'full-image-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); display: flex; align-items: center; justify-content: center; z-index: 10000; opacity: 0; visibility: hidden; transition: all 0.3s ease; cursor: zoom-out; `; modal.innerHTML = `
× 전체 화면 이미지
`; document.body.appendChild(modal); const img = modal.querySelector('img'); const closeBtn = modal.querySelector('.close-btn'); const zoomBtns = modal.querySelectorAll('.zoom-btn'); let currentZoom = 1; let isDragging = false; let startX, startY, translateX = 0, translateY = 0; // 닫기 버튼 이벤트 closeBtn.addEventListener('click', (e) => { e.stopPropagation(); modal.style.opacity = '0'; modal.style.visibility = 'hidden'; currentZoom = 1; translateX = 0; translateY = 0; img.style.transform = 'scale(1) translate(0, 0)'; }); // 배경 클릭으로 닫기 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.style.opacity = '0'; modal.style.visibility = 'hidden'; currentZoom = 1; translateX = 0; translateY = 0; img.style.transform = 'scale(1) translate(0, 0)'; } }); // 확대/축소 버튼 이벤트 zoomBtns.forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const action = btn.dataset.action; if (action === 'zoom-in') { currentZoom = Math.min(currentZoom * 1.5, 5); } else if (action === 'zoom-out') { currentZoom = Math.max(currentZoom / 1.5, 0.5); } else if (action === 'zoom-reset') { currentZoom = 1; translateX = 0; translateY = 0; } img.style.transform = `scale(${currentZoom}) translate(${translateX}px, ${translateY}px)`; img.style.cursor = currentZoom > 1 ? 'move' : 'zoom-in'; }); // 버튼 호버 효과 btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.4)'; }); btn.addEventListener('mouseleave', () => { btn.style.background = 'rgba(255,255,255,0.2)'; }); }); // 이미지 드래그 기능 img.addEventListener('mousedown', (e) => { if (currentZoom > 1) { isDragging = true; startX = e.clientX - translateX; startY = e.clientY - translateY; img.style.cursor = 'grabbing'; e.preventDefault(); } }); document.addEventListener('mousemove', (e) => { if (isDragging && currentZoom > 1) { translateX = e.clientX - startX; translateY = e.clientY - startY; img.style.transform = `scale(${currentZoom}) translate(${translateX}px, ${translateY}px)`; } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; img.style.cursor = currentZoom > 1 ? 'move' : 'zoom-in'; } }); // 휠 확대/축소 modal.addEventListener('wheel', (e) => { e.preventDefault(); if (e.deltaY < 0) { currentZoom = Math.min(currentZoom * 1.2, 5); } else { currentZoom = Math.max(currentZoom / 1.2, 0.5); } img.style.transform = `scale(${currentZoom}) translate(${translateX}px, ${translateY}px)`; img.style.cursor = currentZoom > 1 ? 'move' : 'zoom-in'; }); // ESC 키로 닫기 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.style.visibility === 'visible') { modal.style.opacity = '0'; modal.style.visibility = 'hidden'; currentZoom = 1; translateX = 0; translateY = 0; img.style.transform = 'scale(1) translate(0, 0)'; } }); } const img = modal.querySelector('img'); img.src = imageSrc; modal.style.opacity = '1'; modal.style.visibility = 'visible'; // 이미지 로드 완료 시 최적화 적용 img.onload = () => { // 고해상도 이미지에 대한 렌더링 최적화 img.style.imageRendering = 'auto'; img.style.imageRendering = '-webkit-optimize-contrast'; // 이미지 크기에 따른 초기 스케일 조정 const containerWidth = modal.offsetWidth * 0.9; const containerHeight = modal.offsetHeight * 0.9; const imgAspectRatio = img.naturalWidth / img.naturalHeight; const containerAspectRatio = containerWidth / containerHeight; if (img.naturalWidth > containerWidth || img.naturalHeight > containerHeight) { // 이미지가 컨테이너보다 큰 경우 적절한 크기로 조정 if (imgAspectRatio > containerAspectRatio) { img.style.maxWidth = '90%'; img.style.maxHeight = 'auto'; } else { img.style.maxWidth = 'auto'; img.style.maxHeight = '90%'; } } }; } // 마크다운을 HTML로 변환하는 함수 (비동기) async convertMarkdownToHtml(content) { // 빈 내용 체크 if (!content || typeof content !== 'string') { console.warn('⚠️ 변환할 마크다운 내용이 없습니다'); return ''; } try { // 마크다운 라이브러리가 로드되었는지 확인 if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') { console.log('✅ 마크다운 라이브러리 사용 가능'); // marked로 마크다운을 HTML로 변환 const rawHtml = marked.parse(content); // DOMPurify로 HTML 정화 const cleanHtml = DOMPurify.sanitize(rawHtml); console.log('🎯 마크다운 변환 완료'); return cleanHtml; } else { console.warn('⚠️ 마크다운 라이브러리가 로드되지 않음, 기본 처리 사용'); return this.basicMarkdownToHtml(content); } } catch (error) { console.error('❌ 마크다운 변환 중 오류:', error); // 오류 발생 시 기본 마크다운 처리로 폴백 return this.basicMarkdownToHtml(content); } } // 기본 마크다운 처리 (라이브러리 없이) - 개선된 버전 basicMarkdownToHtml(markdown) { let html = this.escapeHtml(markdown); // 기본적인 마크다운 문법 처리 (순서 중요) html = html // 코드 블록 (```) .replace(/```([\s\S]*?)```/g, '
$1
') // 인라인 코드 (`) .replace(/`([^`]+)`/g, '$1') // 볼드 텍스트 (**) .replace(/\*\*([^*]+)\*\*/g, '$1') // 이탤릭 텍스트 (*) .replace(/\*([^*]+)\*/g, '$1') // 헤더 (###, ##, #) .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') // 인용문 (>) .replace(/^> (.+)$/gm, '
$1
') // 링크 [텍스트](URL) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // 리스트 (- 또는 *) .replace(/^[*-] (.+)$/gm, '
  • $1
  • ') // 줄바꿈 처리 .replace(/\n\n/g, '

    ') .replace(/\n/g, '
    '); // 리스트 태그로 감싸기 html = html.replace(/(

  • .*<\/li>)/gs, ''); // 단락 태그로 감싸기 (이미 p 태그가 있는 경우 제외) if (!html.includes('

    ') && !html.includes('

    ') && !html.includes('

    ') && !html.includes('

    ')) { html = '

    ' + html + '

    '; } this.debugLog('기본 마크다운 변환 완료', { 원본길이: markdown.length, 변환길이: html.length }); return html; } // HTML 이스케이프 escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 에러 렌더링 renderError(message) { const container = document.getElementById("sayings-container"); const statsDiv = document.getElementById("sayings-stats"); if (container) { container.innerHTML = `
    오류가 발생했습니다
    ${message}
    `; } if (statsDiv) { statsDiv.innerHTML = `
    오류: ${message}
    `; } this.debugLog('에러 렌더링 완료', { message }); } // 로그 확인 함수 showDebugLogs() { const debugElement = document.getElementById('debug-info'); if (!debugElement) return; // 현재 상태 수집 const stats = this.calculateStats(); const currentUser = this.currentUser; // 디버그 정보 생성 const debugInfo = `

    🔧 디버그 정보

    📊 타냐대장경 통계:
    • 전체 타냐대장경: ${stats.total}개
    • 필터링된 타냐대장경: ${this.filteredSayings.length}개
    • 카테고리별: ${Object.entries(stats.byCategory).map(([cat, count]) => `${cat}(${count})`).join(', ')}
    👤 사용자 정보:
    ${currentUser ? `• 이메일: ${currentUser.email}
    • 닉네임: ${currentUser.nickname}
    • 회원등급: ${currentUser.membership_level || '기본'}` : '• 로그인 필요'}
    🔔 백그라운드 새 타냐대장경 감지:
    • 상태: 활성화 (1분 간격)
    • 방식: 백그라운드 스크립트 연동
    • 마지막 확인: 백그라운드에서 관리
    • 브라우저 알림: 지원됨
    • 로컬 스토리지: 사용 중
    ⚙️ 시스템 상태:
    • 설정 로드: ${this.isConfigLoaded ? '완료' : '실패'}
    • API 연결: ${this.SUPABASE_URL ? '설정됨' : '미설정'}
    • 토큰 상태: ${this.ACCESS_TOKEN ? '있음' : '없음'}
    • 디버그 모드: ${this.DEBUG_MODE ? '활성화' : '비활성화'}
    `; debugElement.innerHTML = debugInfo; debugElement.style.display = 'block'; // 테스트 버튼 이벤트 등록 const testBtn = document.getElementById('test-new-sayings-btn'); if (testBtn) { testBtn.addEventListener('click', async (e) => { e.preventDefault(); if (confirm('새 타냐대장경 감지 기능을 테스트하시겠습니까?\n백그라운드에서 감지된 새 타냐대장경이 있으면 모달이 표시됩니다.')) { await this.checkBackgroundNewSayings(); } }); } // 스토리지 정리 버튼 이벤트 등록 const clearBtn = document.getElementById('clear-storage-btn'); if (clearBtn) { clearBtn.addEventListener('click', async (e) => { e.preventDefault(); if (confirm('모든 저장된 데이터를 삭제하시겠습니까?\n(로그인 정보, 설정, 캐시 등이 모두 삭제됩니다)')) { try { // Chrome 확장 프로그램 API 접근 가능 여부 확인 if (!chrome || !chrome.storage || !chrome.storage.local) { alert('Chrome 확장 프로그램 API에 접근할 수 없습니다.'); return; } // 안전한 스토리지 정리 await new Promise((resolve, reject) => { try { chrome.storage.local.clear(() => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(); } }); } catch (error) { reject(error); } }); // 로컬 스토리지도 정리 if (typeof localStorage !== 'undefined') { localStorage.clear(); } alert('모든 데이터가 삭제되었습니다. 페이지를 새로고침합니다.'); location.reload(); } catch (error) { console.error('스토리지 정리 오류:', error); alert('데이터 삭제 중 오류가 발생했습니다: ' + error.message); } } }); } } // 현재 사용자 정보 로드 async loadCurrentUser() { try { this.debugLog('현재 사용자 정보 로드 시작'); // 현재 사용자 정보 가져오기 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' }, mode: 'cors', credentials: 'omit' }); if (!authRes.ok) { throw new Error('사용자 인증 실패'); } const authUser = await authRes.json(); // 사용자 닉네임 가져오기 const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id,nickname&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' }, mode: 'cors', credentials: 'omit' }); if (!userRes.ok) { throw new Error('사용자 정보 조회 실패'); } const userData = await userRes.json(); if (!userData || userData.length === 0) { throw new Error('사용자 데이터를 찾을 수 없습니다'); } this.currentUser = { id: userData[0].id, nickname: userData[0].nickname || authUser.email, email: authUser.email }; this.debugLog('현재 사용자 정보 로드 완료', this.currentUser); // UI에 사용자 정보 표시 (헤더) const currentUserElement = document.getElementById('current-user'); if (currentUserElement) { currentUserElement.innerHTML = `👤 ${this.currentUser.nickname}`; } // UI에 사용자 정보 표시 (모달) const modalCurrentUserElement = document.getElementById('modal-current-user'); if (modalCurrentUserElement) { modalCurrentUserElement.innerHTML = `👤 ${this.currentUser.nickname}`; modalCurrentUserElement.style.color = '#2c3e50'; modalCurrentUserElement.style.fontWeight = 'bold'; } } catch (error) { this.debugLog('현재 사용자 정보 로드 실패', { error: error.message }); // 에러 상태 표시 const currentUserElement = document.getElementById('current-user'); if (currentUserElement) { currentUserElement.innerHTML = '👤 사용자 정보 로드 실패'; currentUserElement.style.color = '#e74c3c'; } const modalCurrentUserElement = document.getElementById('modal-current-user'); if (modalCurrentUserElement) { modalCurrentUserElement.innerHTML = '❌ 사용자 정보를 불러올 수 없습니다'; modalCurrentUserElement.style.color = '#e74c3c'; } throw error; } } // 타냐대장경 등록 모달 이벤트 등록 attachAddSayingEvents() { this.debugLog('타냐대장경 등록 모달 이벤트 등록 시작'); const addBtn = document.getElementById('add-saying-btn'); const modal = document.getElementById('add-saying-modal'); const closeBtn = document.getElementById('modal-close'); const cancelBtn = document.getElementById('cancel-btn'); const submitBtn = document.getElementById('submit-btn'); const form = document.getElementById('saying-form'); // 모달 열기 if (addBtn) { addBtn.addEventListener('click', () => { this.debugLog('타냐대장경 등록 모달 열기'); modal.style.display = 'flex'; document.getElementById('saying-title').focus(); }); } // 모달 닫기 const closeModal = () => { this.debugLog('타냐대장경 등록 모달 닫기'); modal.style.display = 'none'; form.reset(); this.resetPreviewTabs(); }; if (closeBtn) { closeBtn.addEventListener('click', closeModal); } if (cancelBtn) { cancelBtn.addEventListener('click', closeModal); } // 모달 외부 클릭 시 닫기 modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); // ESC 키로 모달 닫기 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.style.display === 'flex') { closeModal(); } }); // 마크다운 미리보기 탭 이벤트 this.attachPreviewTabEvents(); // 폼 제출 이벤트 if (form) { form.addEventListener('submit', (e) => { e.preventDefault(); this.handleSayingSubmit(); }); } if (submitBtn) { submitBtn.addEventListener('click', (e) => { e.preventDefault(); this.handleSayingSubmit(); }); } this.debugLog('타냐대장경 등록 모달 이벤트 등록 완료'); } // 마크다운 미리보기 탭 이벤트 등록 attachPreviewTabEvents() { const previewTabs = document.querySelectorAll('.preview-tab'); const contentTextarea = document.getElementById('saying-content'); const previewDiv = document.getElementById('markdown-preview'); previewTabs.forEach(tab => { tab.addEventListener('click', () => { // 모든 탭에서 active 클래스 제거 previewTabs.forEach(t => t.classList.remove('active')); // 클릭된 탭에 active 클래스 추가 tab.classList.add('active'); const tabType = tab.getAttribute('data-tab'); if (tabType === 'edit') { // 편집 모드 contentTextarea.style.display = 'block'; previewDiv.style.display = 'none'; } else if (tabType === 'preview') { // 미리보기 모드 contentTextarea.style.display = 'none'; previewDiv.style.display = 'block'; // 마크다운 렌더링 this.renderMarkdownPreview(); } }); }); // 실시간 미리보기 업데이트 contentTextarea.addEventListener('input', () => { const activeTab = document.querySelector('.preview-tab.active'); if (activeTab && activeTab.getAttribute('data-tab') === 'preview') { // 미리보기 탭이 활성화된 경우 실시간 업데이트 this.renderMarkdownPreview(); } }); } // 마크다운 미리보기 렌더링 (비동기) async renderMarkdownPreview() { const contentTextarea = document.getElementById('saying-content'); const previewDiv = document.getElementById('markdown-preview'); if (!contentTextarea || !previewDiv) { console.error('❌ 미리보기 요소를 찾을 수 없습니다'); return; } const content = contentTextarea.value.trim(); if (!content) { previewDiv.innerHTML = '
    미리보기할 내용을 입력해주세요
    '; return; } // 로딩 상태 표시 previewDiv.innerHTML = '
    미리보기 생성 중...
    '; try { // 마크다운을 HTML로 변환 const htmlContent = await this.convertMarkdownToHtml(content); // 변환된 HTML을 미리보기에 표시 previewDiv.innerHTML = htmlContent || '
    내용을 변환할 수 없습니다
    '; console.log('✅ 마크다운 미리보기 렌더링 완료'); } catch (error) { console.error('❌ 마크다운 미리보기 렌더링 실패:', error); previewDiv.innerHTML = '
    미리보기 생성 중 오류가 발생했습니다
    '; } } // 타냐대장경 제출 처리 async handleSayingSubmit() { this.debugLog('타냐대장경 제출 처리 시작'); const title = document.getElementById('saying-title').value.trim(); const category = document.getElementById('saying-category').value; const target = document.getElementById('saying-target').value; const submitBtn = document.getElementById('submit-btn'); // 블록 에디터 데이터 가져오기 let content = ''; let contentBlocks = null; if (window.blockEditor && window.blockEditor.blocks.length > 0) { // 블록 에디터 사용 시 const blockData = window.blockEditor.getContentForSave(); content = blockData.legacy_text; // 기존 텍스트 필드용 (하위 호환성) contentBlocks = blockData.blocks; // JSONB 필드용 // 블록이 비어있는지 확인 const hasContent = contentBlocks.some(block => { if (block.type === 'text') { return block.content && block.content.trim() !== ''; } else if (block.type === 'image') { return block.content !== null; } return false; }); if (!hasContent) { alert('❌ 타냐대장경 내용을 입력해주세요.'); return; } } else { // 기존 텍스트 에디터 사용 시 (하위 호환성) content = document.getElementById('saying-content').value.trim(); if (!content) { alert('❌ 타냐대장경 내용을 입력해주세요.'); document.getElementById('saying-content').focus(); return; } } // 유효성 검사 if (!title) { alert('❌ 타냐대장경 제목을 입력해주세요.'); document.getElementById('saying-title').focus(); return; } if (!category) { alert('❌ 카테고리를 선택해주세요.'); document.getElementById('saying-category').focus(); return; } if (!target) { alert('❌ 타겟을 선택해주세요.'); document.getElementById('saying-target').focus(); return; } if (!this.currentUser) { alert('❌ 사용자 정보를 불러올 수 없습니다. 페이지를 새로고침해주세요.'); return; } try { // 버튼 상태 변경 const originalText = submitBtn.textContent; submitBtn.textContent = '🔄 등록 중...'; submitBtn.disabled = true; this.debugLog('타냐대장경 등록 API 호출', { title, contentLength: content.length, hasBlocks: contentBlocks !== null, blocksCount: contentBlocks ? contentBlocks.length : 0, category, target, userId: this.currentUser.id, userNickname: this.currentUser.nickname }); // 타냐대장경 데이터 생성 (register 필드 사용) const sayingData = { saying_title: title, saying: content, // 기존 텍스트 필드 (하위 호환성) cat: category, target: target, register: this.currentUser.nickname, // 등록자 필드명 변경 user_id: this.currentUser.id, // RLS 정책을 위한 사용자 ID 추가 admin_approval: false, // 기본적으로 승인 대기 상태 created_at: new Date().toISOString() }; // 블록 데이터가 있으면 추가 if (contentBlocks) { sayingData.content_blocks = contentBlocks; } // API 호출 const response = await fetch(`${this.SUPABASE_URL}/rest/v1/tanya_sayings`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.ACCESS_TOKEN}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(sayingData) }); this.debugLog('타냐대장경 등록 API 응답', { status: response.status, statusText: response.statusText, ok: response.ok }); if (!response.ok) { const errorDetail = await response.text(); this.debugLog('타냐대장경 등록 실패', { status: response.status, error: errorDetail }); throw new Error(`타냐대장경 등록 실패 (${response.status}: ${response.statusText})\n응답: ${errorDetail}`); } this.debugLog('타냐대장경 등록 성공'); // 성공 팝업 표시 this.showSuccessPopup(title, category, target); // 모달 닫기 document.getElementById('add-saying-modal').style.display = 'none'; document.getElementById('saying-form').reset(); this.resetPreviewTabs(); // 블록 에디터 초기화 if (window.blockEditor) { window.blockEditor.loadFromData([]); } // 타냐대장경 목록 새로고침 (승인된 타냐대장경만 표시되므로 새로 등록한 타냐대장경은 보이지 않음) await this.loadSayings(); } catch (error) { this.debugLog('타냐대장경 등록 실패', { error: error.message }); alert(`❌ 타냐대장경 등록 중 오류가 발생했습니다:\n${error.message}`); } finally { // 버튼 상태 복원 submitBtn.textContent = '📝 등록하기'; submitBtn.disabled = false; } } // 성공 팝업 표시 showSuccessPopup(title, category, target) { const popup = document.createElement('div'); popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 2000; text-align: center; min-width: 400px; border: 3px solid #28a745; `; popup.innerHTML = `
    🎉

    타냐대장경 등록 완료!

    제목: ${title}
    카테고리: ${category}
    타겟: ${target}
    등록자: ${this.currentUser.nickname}
    📋 안내사항
    등록된 타냐대장경은 관리자 승인 후 목록에 표시됩니다.
    `; // 팝업을 body에 추가 document.body.appendChild(popup); // 확인 버튼 이벤트 const closeBtn = popup.querySelector('#close-popup'); closeBtn.addEventListener('click', () => { document.body.removeChild(popup); }); // 3초 후 자동으로 닫기 setTimeout(() => { if (document.body.contains(popup)) { document.body.removeChild(popup); } }, 5000); this.debugLog('성공 팝업 표시 완료', { title, category, target }); } // 카테고리와 타겟 옵션을 백엔드에서 로드 async loadCategoryAndTargetOptions() { this.debugLog('카테고리와 타겟 옵션 로드 시작'); try { // 카테고리 데이터 가져오기 this.debugLog('카테고리 데이터 요청 중...'); const categoryResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/sayings_cat?select=saying_cat&order=saying_cat.asc`, { headers: { 'apikey': this.SUPABASE_ANON_KEY, 'Authorization': `Bearer ${this.SUPABASE_ANON_KEY}`, 'Content-Type': 'application/json' } }); // 타겟 데이터 가져오기 this.debugLog('타겟 데이터 요청 중...'); const targetResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/sayings_target?select=target&order=target.asc`, { headers: { 'apikey': this.SUPABASE_ANON_KEY, 'Authorization': `Bearer ${this.SUPABASE_ANON_KEY}`, 'Content-Type': 'application/json' } }); // 카테고리 응답 처리 if (categoryResponse.ok) { const categoryData = await categoryResponse.json(); this.debugLog('카테고리 데이터 로드 성공', { count: categoryData.length }); if (categoryData.length === 0) { throw new Error('카테고리 데이터가 비어있습니다. sayings_cat 테이블을 확인해주세요.'); } // 카테고리 드롭다운 업데이트 this.updateCategoryDropdowns(categoryData); } else { const errorText = await categoryResponse.text(); this.debugLog('카테고리 데이터 로드 실패', { status: categoryResponse.status, statusText: categoryResponse.statusText, error: errorText }); throw new Error(`카테고리 데이터 로드 실패: ${categoryResponse.status} ${categoryResponse.statusText}`); } // 타겟 응답 처리 if (targetResponse.ok) { const targetData = await targetResponse.json(); this.debugLog('타겟 데이터 로드 성공', { count: targetData.length }); if (targetData.length === 0) { throw new Error('타겟 데이터가 비어있습니다. sayings_target 테이블을 확인해주세요.'); } // 타겟 드롭다운 업데이트 this.updateTargetDropdowns(targetData); } else { const errorText = await targetResponse.text(); this.debugLog('타겟 데이터 로드 실패', { status: targetResponse.status, statusText: targetResponse.statusText, error: errorText }); throw new Error(`타겟 데이터 로드 실패: ${targetResponse.status} ${targetResponse.statusText}`); } this.debugLog('카테고리와 타겟 옵션 로드 완료'); } catch (error) { this.debugLog('카테고리/타겟 옵션 로드 중 오류 발생', { error: error.message }); // 에러를 다시 던져서 앱 초기화를 중단 throw new Error(`백엔드에서 드롭다운 데이터를 가져올 수 없습니다: ${error.message}`); } } // 카테고리 드롭다운 업데이트 updateCategoryDropdowns(categoryData) { const categorySelect = document.getElementById('saying-category'); if (!categorySelect) return; // 기존 옵션 제거 (첫 번째 "선택하세요" 옵션 제외) while (categorySelect.children.length > 1) { categorySelect.removeChild(categorySelect.lastChild); } // 새 카테고리 옵션 추가 categoryData.forEach(item => { const option = document.createElement('option'); option.value = item.saying_cat; option.textContent = item.saying_cat; categorySelect.appendChild(option); }); // "아무거나" 기본값 설정 const defaultCategory = categoryData.find(item => item.saying_cat === '아무거나'); if (defaultCategory) { categorySelect.value = defaultCategory.saying_cat; this.debugLog('카테고리 기본값 설정', { 기본값: defaultCategory.saying_cat }); } // 필터 드롭다운도 업데이트 this.updateFilterCategoryOptions(categoryData); this.debugLog('카테고리 드롭다운 업데이트 완료', { 옵션개수: categorySelect.children.length - 1, 기본값: categorySelect.value || '없음' }); } // 타겟 드롭다운 업데이트 updateTargetDropdowns(targetData) { const targetSelect = document.getElementById('saying-target'); if (!targetSelect) return; // 기존 옵션 제거 (첫 번째 "선택하세요" 옵션 제외) while (targetSelect.children.length > 1) { targetSelect.removeChild(targetSelect.lastChild); } // 새 타겟 옵션 추가 targetData.forEach(item => { const option = document.createElement('option'); option.value = item.target; option.textContent = item.target; targetSelect.appendChild(option); }); // "누구나" 기본값 설정 const defaultTarget = targetData.find(item => item.target === '누구나'); if (defaultTarget) { targetSelect.value = defaultTarget.target; this.debugLog('타겟 기본값 설정', { 기본값: defaultTarget.target }); } // 필터 드롭다운도 업데이트 this.updateFilterTargetOptions(targetData); this.debugLog('타겟 드롭다운 업데이트 완료', { 옵션개수: targetSelect.children.length - 1, 기본값: targetSelect.value || '없음' }); } // 필터용 카테고리 옵션 업데이트 updateFilterCategoryOptions(categoryData) { const categoryFilter = document.getElementById('category-filter'); if (!categoryFilter) return; // 기존 옵션 제거 (첫 번째 "모든 카테고리" 옵션 제외) while (categoryFilter.children.length > 1) { categoryFilter.removeChild(categoryFilter.lastChild); } // 새 카테고리 옵션 추가 categoryData.forEach(item => { const option = document.createElement('option'); option.value = item.saying_cat; option.textContent = item.saying_cat; categoryFilter.appendChild(option); }); this.debugLog('필터용 카테고리 옵션 업데이트 완료', { 옵션개수: categoryFilter.children.length - 1 }); } // 필터용 타겟 옵션 업데이트 updateFilterTargetOptions(targetData) { const targetFilter = document.getElementById('target-filter'); if (!targetFilter) return; // 기존 옵션 제거 (첫 번째 "모든 타겟" 옵션 제외) while (targetFilter.children.length > 1) { targetFilter.removeChild(targetFilter.lastChild); } // 새 타겟 옵션 추가 targetData.forEach(item => { const option = document.createElement('option'); option.value = item.target; option.textContent = item.target; targetFilter.appendChild(option); }); this.debugLog('필터용 타겟 옵션 업데이트 완료', { 옵션개수: targetFilter.children.length - 1 }); } // 통계 계산 calculateStats() { const stats = { total: this.filteredSayings.length, byCategory: {}, byTarget: {} }; this.filteredSayings.forEach(saying => { // 카테고리별 통계 const category = saying.cat || '기타'; stats.byCategory[category] = (stats.byCategory[category] || 0) + 1; // 타겟별 통계 const target = saying.target || '기타'; stats.byTarget[target] = (stats.byTarget[target] || 0) + 1; }); return stats; } // 필터 적용 async applyFilters() { this.debugLog('필터 적용 시작'); const categoryFilter = document.getElementById('category-filter').value; const targetFilter = document.getElementById('target-filter').value; const dateFilter = document.getElementById('date-filter').value; const searchText = document.getElementById('search-input').value.toLowerCase().trim(); this.debugLog('필터 조건', { category: categoryFilter, target: targetFilter, date: dateFilter, search: searchText }); this.filteredSayings = this.allSayings.filter(saying => { // 카테고리 필터 if (categoryFilter && categoryFilter !== 'all') { if (saying.cat !== categoryFilter) return false; } // 타겟 필터 if (targetFilter && targetFilter !== 'all') { if (saying.target !== targetFilter) return false; } // 날짜 필터 if (dateFilter && dateFilter !== 'all') { const sayingDate = new Date(saying.created_at); const now = new Date(); switch (dateFilter) { case 'today': if (sayingDate.toDateString() !== now.toDateString()) return false; break; case 'week': const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); if (sayingDate < weekAgo) return false; break; case 'month': const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); if (sayingDate < monthAgo) return false; break; } } // 검색 텍스트 필터 if (searchText) { const searchableText = [ saying.saying_title || '', saying.saying || '', saying.register || saying.user_name || '', saying.cat || '', saying.target || '' ].join(' ').toLowerCase(); if (!searchableText.includes(searchText)) return false; } return true; }); this.debugLog('필터 적용 완료', { 원본개수: this.allSayings.length, 필터링된개수: this.filteredSayings.length }); await this.displaySayings(); } // 필터 이벤트 연결 attachFilterEvents() { this.debugLog('필터 이벤트 연결 시작'); // 카테고리 필터 const categoryFilter = document.getElementById('category-filter'); if (categoryFilter) { categoryFilter.addEventListener('change', async () => await this.applyFilters()); } // 타겟 필터 const targetFilter = document.getElementById('target-filter'); if (targetFilter) { targetFilter.addEventListener('change', async () => await this.applyFilters()); } // 날짜 필터 const dateFilter = document.getElementById('date-filter'); if (dateFilter) { dateFilter.addEventListener('change', async () => await this.applyFilters()); } // 검색 입력 const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', () => { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(async () => await this.applyFilters(), 300); }); } // 필터 초기화 버튼 const resetButton = document.getElementById('reset-filters'); if (resetButton) { resetButton.addEventListener('click', async () => await this.resetFilters()); } this.debugLog('필터 이벤트 연결 완료'); } // 필터 초기화 async resetFilters() { this.debugLog('필터 초기화 시작'); // 모든 필터 초기화 const categoryFilter = document.getElementById('category-filter'); const targetFilter = document.getElementById('target-filter'); const dateFilter = document.getElementById('date-filter'); const searchInput = document.getElementById('search-input'); if (categoryFilter) categoryFilter.value = 'all'; if (targetFilter) targetFilter.value = 'all'; if (dateFilter) dateFilter.value = 'all'; if (searchInput) searchInput.value = ''; // 필터 적용 await this.applyFilters(); this.debugLog('필터 초기화 완료'); } // 미리보기 탭 초기화 resetPreviewTabs() { const previewTabs = document.querySelectorAll('.preview-tab'); const contentTextarea = document.getElementById('saying-content'); const previewDiv = document.getElementById('markdown-preview'); // 모든 탭에서 active 클래스 제거 previewTabs.forEach(tab => tab.classList.remove('active')); // 편집 탭을 활성화 const editTab = document.querySelector('.preview-tab[data-tab="edit"]'); if (editTab) { editTab.classList.add('active'); } // 편집 모드로 전환 if (contentTextarea) { contentTextarea.style.display = 'block'; } if (previewDiv) { previewDiv.style.display = 'none'; previewDiv.innerHTML = ''; } this.debugLog('미리보기 탭 초기화 완료'); } // 새 타냐대장경 감지 시작 (기존 코드 제거하고 백그라운드 연동 방식으로 교체) startNewSayingsMonitoring() { this.debugLog('새 타냐대장경 감지 시작'); // 기존 인터벌이 있으면 정리 if (this.newSayingsCheckInterval) { clearInterval(this.newSayingsCheckInterval); } // 새 타냐대장경 감지 시작 (백그라운드 연동) this.startNewSayingsDetection(); } // 새 타냐대장경 감지 체크 (제거됨 - 백그라운드에서 처리) // async checkForNewSayings() { ... } - 이 함수는 제거됨 // 새 타냐대장경 모달 표시 (카운트다운 포함) showNewSayingsModal(newSayings) { console.log(`[SayingsManager] 새 타냐대장경 모달 표시: ${newSayings.length}개`); // 기존 모달이 있으면 제거 const existingModal = document.getElementById('newSayingsModal'); if (existingModal) { existingModal.remove(); } // 모달 HTML 생성 const modalHtml = ` `; // 모달을 body에 추가 document.body.insertAdjacentHTML('beforeend', modalHtml); // 카운트다운 시작 let countdown = 10; const countdownElement = document.getElementById('countdownTimer'); const countdownInterval = setInterval(() => { countdown--; if (countdown > 0) { countdownElement.textContent = `${countdown}초 후 자동 닫힘`; } else { clearInterval(countdownInterval); this.closeNewSayingsModal(); } }, 1000); // 이벤트 리스너 추가 const modal = document.getElementById('newSayingsModal'); const dismissBtn = document.getElementById('dismissNewSayings'); const viewBtn = document.getElementById('viewNewSayings'); // 오버레이 클릭으로 닫기 modal.addEventListener('click', (e) => { if (e.target === modal) { clearInterval(countdownInterval); this.closeNewSayingsModal(); } }); // 나중에 버튼 dismissBtn.addEventListener('click', () => { clearInterval(countdownInterval); this.closeNewSayingsModal(); }); // 새 타냐대장경 보기 버튼 viewBtn.addEventListener('click', () => { clearInterval(countdownInterval); this.closeNewSayingsModal(); this.loadSayings(); // 타냐대장경 목록 새로고침 }); // 모달 참조 저장 (카운트다운 정리용) this.currentModalCountdown = countdownInterval; } // 새 타냐대장경 모달 닫기 closeNewSayingsModal() { const modal = document.getElementById('newSayingsModal'); if (modal) { modal.style.animation = 'fadeOut 0.3s ease-out'; setTimeout(() => { modal.remove(); }, 5000); } // 카운트다운 정리 if (this.currentModalCountdown) { clearInterval(this.currentModalCountdown); this.currentModalCountdown = null; } } // 새 타냐대장경 감지 정리 cleanup() { console.log('[SayingsManager] 새 타냐대장경 감지 정리'); if (this.newSayingsCheckInterval) { clearInterval(this.newSayingsCheckInterval); this.newSayingsCheckInterval = null; } if (this.currentModalCountdown) { clearInterval(this.currentModalCountdown); this.currentModalCountdown = null; } // 모달 제거 const modal = document.getElementById('newSayingsModal'); if (modal) { modal.remove(); } } // 새 타냐대장경 감지 시작 startNewSayingsDetection() { console.log('[SayingsManager] 새 타냐대장경 감지 시작'); // 기존 타이머가 있으면 정리 if (this.newSayingsTimer) { clearInterval(this.newSayingsTimer); } // 페이지 로드 시 백그라운드에서 감지된 새 타냐대장경 확인 this.checkBackgroundNewSayings(); // 1분마다 새 타냐대장경 확인 (백그라운드와 동기화) this.newSayingsTimer = setInterval(() => { this.checkBackgroundNewSayings(); }, 60000); // 1분 console.log('[SayingsManager] 새 타냐대장경 감지 타이머 설정 완료 (1분 간격)'); } // 백그라운드에서 감지된 새 타냐대장경 확인 async checkBackgroundNewSayings() { try { console.log('[SayingsManager] 백그라운드 새 타냐대장경 확인 시작'); // Chrome 확장 프로그램 API 접근 가능 여부 확인 if (!chrome || !chrome.storage || !chrome.storage.local) { console.error('[SayingsManager] Chrome 확장 프로그램 API에 접근할 수 없습니다.'); return; } // Promise 기반으로 안전하게 스토리지 접근 const storageData = await new Promise((resolve, reject) => { try { chrome.storage.local.get(['hasNewSayings', 'pendingNewSayings'], (result) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(result); } }); } catch (error) { reject(error); } }); const { hasNewSayings, pendingNewSayings } = storageData; console.log('[SayingsManager] 스토리지 데이터 확인:', { hasNewSayings: !!hasNewSayings, pendingCount: pendingNewSayings ? pendingNewSayings.length : 0 }); if (hasNewSayings && pendingNewSayings && pendingNewSayings.length > 0) { console.log(`[SayingsManager] 백그라운드에서 감지된 새 타냐대장경: ${pendingNewSayings.length}개`); this.showNewSayingsModal(pendingNewSayings); // 표시 후 플래그 제거 (안전한 방식) try { await new Promise((resolve, reject) => { chrome.storage.local.remove(['hasNewSayings', 'pendingNewSayings'], () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { console.log('[SayingsManager] 새 타냐대장경 플래그 정리 완료'); resolve(); } }); }); } catch (removeError) { console.error('[SayingsManager] 새 타냐대장경 플래그 정리 실패:', removeError); } } else { console.log('[SayingsManager] 백그라운드에서 감지된 새 타냐대장경이 없습니다.'); } } catch (error) { console.error('[SayingsManager] 백그라운드 새 타냐대장경 확인 오류:', error); // 오류 유형별 처리 if (error.message.includes('Could not establish connection')) { console.error('[SayingsManager] Chrome 확장 프로그램 연결 오류 - 확장 프로그램을 다시 로드하세요.'); } else if (error.message.includes('Receiving end does not exist')) { console.error('[SayingsManager] 메시지 수신자가 존재하지 않음 - 페이지를 새로고침하세요.'); } else if (error.message.includes('Extension context invalidated')) { console.error('[SayingsManager] 확장 프로그램 컨텍스트가 무효화됨 - 확장 프로그램을 다시 로드하세요.'); } } } // 상세 보기 모달 열기 openDetailModal(saying) { this.debugLog('상세 보기 모달 열기', saying); const modal = document.getElementById('view-saying-modal'); if (!modal) { console.error('상세 보기 모달을 찾을 수 없습니다'); return; } this.currentEditingSaying = saying; // 현재 사용자와 작성자 비교 const currentUser = this.currentUser?.user?.email || this.currentUser?.email; const isAuthor = currentUser && (saying.register === currentUser || saying.user_name === currentUser); this.debugLog('권한 확인', { currentUser, sayingAuthor: saying.register || saying.user_name, isAuthor }); // 모달 데이터 설정 this.populateDetailModal(saying, isAuthor); // 모달 표시 modal.style.display = 'block'; // 모달 이벤트 바인딩 this.attachDetailModalEvents(saying, isAuthor); } // 상세 보기 모달 데이터 채우기 populateDetailModal(saying, isAuthor) { const viewMode = document.getElementById('view-mode'); const editMode = document.getElementById('edit-mode'); const editButton = document.getElementById('edit-mode-btn'); const deleteButton = document.getElementById('delete-saying-btn'); // 보기 모드 데이터 설정 document.getElementById('view-title').textContent = saying.saying_title || '제목 없음'; // 콘텐츠 렌더링 const viewContent = document.getElementById('view-content'); if (saying.content_blocks && Array.isArray(saying.content_blocks) && saying.content_blocks.length > 0) { viewContent.innerHTML = this.renderContentBlocks(saying.content_blocks); } else { viewContent.innerHTML = this.basicMarkdownToHtml(saying.saying || ''); } // 메타 정보 설정 const viewMeta = document.getElementById('view-meta'); viewMeta.innerHTML = ` ${this.escapeHtml(saying.cat || '기타')} ${this.escapeHtml(saying.target || '기타')} `; // 작성자 정보 설정 const authorName = saying.register || saying.user_name || '알 수 없음'; const createdAt = new Date(saying.created_at).toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); document.getElementById('view-author-info').innerHTML = `
    ${authorName.charAt(0).toUpperCase()}
    ${this.escapeHtml(authorName)}
    ${createdAt}
    `; // 편집 모드 데이터 설정 (작성자인 경우에만) if (isAuthor) { document.getElementById('edit-saying-title').value = saying.saying_title || ''; document.getElementById('edit-saying-content').value = saying.saying || ''; document.getElementById('edit-saying-category').value = saying.cat || ''; document.getElementById('edit-saying-target').value = saying.target || ''; } // 버튼 표시/숨김 제어 const viewModeButtons = document.getElementById('view-mode-buttons'); const editModeButtons = document.getElementById('edit-mode-buttons'); if (isAuthor) { editButton.style.display = 'inline-block'; deleteButton.style.display = 'inline-block'; viewMode.style.display = 'block'; editMode.style.display = 'none'; viewModeButtons.style.display = 'flex'; editModeButtons.style.display = 'none'; } else { editButton.style.display = 'none'; deleteButton.style.display = 'none'; viewMode.style.display = 'block'; editMode.style.display = 'none'; viewModeButtons.style.display = 'flex'; editModeButtons.style.display = 'none'; } } // 상세 보기 모달 이벤트 바인딩 attachDetailModalEvents(saying, isAuthor) { // 닫기 버튼 const closeBtn = document.getElementById('view-modal-close'); const viewCloseBtn = document.getElementById('view-close-btn'); const modal = document.getElementById('view-saying-modal'); const closeModal = () => { modal.style.display = 'none'; this.currentEditingSaying = null; }; // 기존 이벤트 리스너 제거 후 새로 추가 if (closeBtn) { closeBtn.replaceWith(closeBtn.cloneNode(true)); document.getElementById('view-modal-close').addEventListener('click', closeModal); } if (viewCloseBtn) { viewCloseBtn.replaceWith(viewCloseBtn.cloneNode(true)); document.getElementById('view-close-btn').addEventListener('click', closeModal); } // 모달 배경 클릭 시 닫기 modal.onclick = (e) => { if (e.target === modal) { closeModal(); } }; // ESC 키로 모달 닫기 const handleEsc = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleEsc); } }; document.addEventListener('keydown', handleEsc); if (isAuthor) { // 편집 버튼 const editBtn = document.getElementById('edit-mode-btn'); if (editBtn) { editBtn.replaceWith(editBtn.cloneNode(true)); document.getElementById('edit-mode-btn').addEventListener('click', () => { this.switchToEditMode(); }); } // 취소 버튼 const cancelBtn = document.getElementById('edit-cancel-btn'); if (cancelBtn) { cancelBtn.replaceWith(cancelBtn.cloneNode(true)); document.getElementById('edit-cancel-btn').addEventListener('click', () => { this.switchToViewMode(); }); } // 저장 버튼 const saveBtn = document.getElementById('update-saying-btn'); if (saveBtn) { saveBtn.replaceWith(saveBtn.cloneNode(true)); document.getElementById('update-saying-btn').addEventListener('click', () => { this.saveEditedSaying(); }); } // 삭제 버튼 const deleteBtn = document.getElementById('delete-saying-btn'); if (deleteBtn) { deleteBtn.replaceWith(deleteBtn.cloneNode(true)); document.getElementById('delete-saying-btn').addEventListener('click', () => { this.deleteSaying(saying); }); } } // 이미지 클릭 이벤트 추가 (보기 모드에서) - 원본 이미지 사용 const viewContent = document.getElementById('view-content'); const images = viewContent.querySelectorAll('.content-image'); images.forEach(img => { img.addEventListener('click', () => { // data-original 속성에서 원본 이미지 URL 가져오기 const originalSrc = img.dataset.original || img.src; this.showFullImage(originalSrc); }); }); } // 편집 모드로 전환 switchToEditMode() { const viewMode = document.getElementById('view-mode'); const editMode = document.getElementById('edit-mode'); const viewModeButtons = document.getElementById('view-mode-buttons'); const editModeButtons = document.getElementById('edit-mode-buttons'); viewMode.style.display = 'none'; editMode.style.display = 'block'; viewModeButtons.style.display = 'none'; editModeButtons.style.display = 'flex'; this.debugLog('편집 모드로 전환'); } // 보기 모드로 전환 switchToViewMode() { const viewMode = document.getElementById('view-mode'); const editMode = document.getElementById('edit-mode'); const viewModeButtons = document.getElementById('view-mode-buttons'); const editModeButtons = document.getElementById('edit-mode-buttons'); viewMode.style.display = 'block'; editMode.style.display = 'none'; viewModeButtons.style.display = 'flex'; editModeButtons.style.display = 'none'; this.debugLog('보기 모드로 전환'); } // 편집된 타냐대장경 저장 async saveEditedSaying() { if (!this.currentEditingSaying) { console.error('편집 중인 타냐대장경이 없습니다'); return; } const title = document.getElementById('edit-saying-title').value.trim(); const content = document.getElementById('edit-saying-content').value.trim(); const category = document.getElementById('edit-saying-category').value.trim(); const target = document.getElementById('edit-saying-target').value.trim(); if (!title || !content) { alert('제목과 내용을 모두 입력해주세요.'); return; } try { const response = await fetch(`${this.config.supabaseUrl}/rest/v1/tanya_sayings?id=eq.${this.currentEditingSaying.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'apikey': this.config.supabaseKey, 'Authorization': `Bearer ${this.config.supabaseKey}`, 'Prefer': 'return=representation' }, body: JSON.stringify({ saying_title: title, saying: content, cat: category, target: target }) }); if (response.ok) { alert('타냐대장경이 성공적으로 수정되었습니다.'); // 모달 닫기 document.getElementById('view-saying-modal').style.display = 'none'; this.currentEditingSaying = null; // 타냐대장경 목록 새로고침 await this.loadSayings(); } else { throw new Error('타냐대장경 수정에 실패했습니다.'); } } catch (error) { console.error('타냐대장경 수정 오류:', error); alert('타냐대장경 수정 중 오류가 발생했습니다.'); } } // 타냐대장경 삭제 async deleteSaying(saying) { if (!confirm('정말로 이 타냐대장경을 삭제하시겠습니까?')) { return; } try { const response = await fetch(`${this.config.supabaseUrl}/rest/v1/tanya_sayings?id=eq.${saying.id}`, { method: 'DELETE', headers: { 'apikey': this.config.supabaseKey, 'Authorization': `Bearer ${this.config.supabaseKey}` } }); if (response.ok) { alert('타냐대장경이 성공적으로 삭제되었습니다.'); // 모달 닫기 document.getElementById('view-saying-modal').style.display = 'none'; this.currentEditingSaying = null; // 타냐대장경 목록 새로고침 await this.loadSayings(); } else { throw new Error('타냐대장경 삭제에 실패했습니다.'); } } catch (error) { console.error('타냐대장경 삭제 오류:', error); alert('타냐대장경 삭제 중 오류가 발생했습니다.'); } } } // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', async () => { console.log('=== 타냐대장경 관리 페이지 초기화 시작 ==='); console.log('현재 시간:', new Date().toLocaleString()); // chrome.storage에서 설정 확인 let debugMode = false; // 기본값 try { const configData = await chrome.storage.local.get('sayings_config'); console.log('chrome.storage 설정 확인:', { hasSayingsConfig: !!configData.sayings_config, config: configData.sayings_config ? { hasUrl: !!configData.sayings_config.SUPABASE_URL, hasKey: !!configData.sayings_config.SUPABASE_ANON_KEY, hasToken: !!configData.sayings_config.ACCESS_TOKEN, debugMode: configData.sayings_config.DEBUG_MODE, timestamp: configData.sayings_config.timestamp } : null }); // DEBUG_MODE 설정 확인 if (configData.sayings_config && configData.sayings_config.DEBUG_MODE !== undefined) { debugMode = configData.sayings_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('log-check-btn'); if (logCheckBtn) { if (debugMode) { console.log('디버그 모드: 로그 확인 버튼 이벤트 등록'); logCheckBtn.style.display = "inline-block"; logCheckBtn.addEventListener('click', async (e) => { e.preventDefault(); console.log('로그 확인 버튼 클릭됨'); // 버튼 상태 변경 const originalText = logCheckBtn.textContent; logCheckBtn.textContent = '🔄 확인 중...'; logCheckBtn.disabled = true; try { // SayingsManager가 초기화되었는지 확인 if (window.sayingsManager && typeof window.sayingsManager.showDebugLogs === 'function') { await window.sayingsManager.showDebugLogs(); } else { console.error('SayingsManager가 초기화되지 않았습니다'); alert('❌ 관리자가 초기화되지 않았습니다. 페이지를 새로고침해주세요.'); } } finally { // 버튼 상태 복원 logCheckBtn.textContent = originalText; logCheckBtn.disabled = false; } }); console.log('로그 확인 버튼 이벤트 등록 완료'); } else { console.log('디버그 모드 비활성화: 로그 확인 버튼 숨김'); logCheckBtn.style.display = "none"; } } try { // SayingsManager 초기화 console.log('SayingsManager 인스턴스 생성 중...'); window.sayingsManager = new SayingsManager(); console.log('SayingsManager 초기화 시작...'); await window.sayingsManager.initialize(); console.log('✅ 타냐대장경 관리 페이지 초기화 완료'); } catch (error) { console.error('❌ 초기화 실패:', error); // UI에 오류 표시 const statsElement = document.getElementById('sayings-stats'); if (statsElement) { statsElement.innerHTML = `
    ❌ 초기화 실패
    ${error.message}
    로그인 후 다시 시도해주세요.
    `; } const container = document.getElementById("sayings-container"); if (container) { container.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('log-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 이벤트 처리 완료 ==='); // 블록 에디터 초기화 initializeBlockEditor(); }); // 페이지 언로드 시 정리 window.addEventListener('beforeunload', () => { console.log('페이지 언로드 - 새 타냐대장경 감지 기능 정리'); if (window.sayingsManager) { window.sayingsManager.cleanup(); } }); // 전역 함수로 등록 window.sayingsManager = null; // 블록 에디터 클래스 class BlockEditor { constructor() { this.blocks = []; this.draggedBlock = null; this.init(); } init() { this.bindEvents(); this.addInitialTextBlock(); } bindEvents() { // 툴바 버튼 이벤트 document.getElementById('add-text-block')?.addEventListener('click', () => { this.addTextBlock(); }); document.getElementById('add-image-block')?.addEventListener('click', () => { this.addImageBlock(); }); } addInitialTextBlock() { this.addTextBlock(''); } addTextBlock(content = '') { const blockId = 'block_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const block = { id: blockId, type: 'text', content: content }; this.blocks.push(block); this.renderBlock(block); this.updateContentBlocksData(); } addImageBlock() { const blockId = 'block_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const block = { id: blockId, type: 'image', content: null, alt: '', thumbnail: null }; this.blocks.push(block); this.renderBlock(block); this.updateContentBlocksData(); } renderBlock(block) { const container = document.getElementById('content-blocks'); if (!container) return; const blockElement = document.createElement('div'); blockElement.className = 'content-block'; blockElement.setAttribute('data-block-id', block.id); blockElement.draggable = true; if (block.type === 'text') { blockElement.innerHTML = this.getTextBlockHTML(block); this.bindTextBlockEvents(blockElement, block); } else if (block.type === 'image') { blockElement.innerHTML = this.getImageBlockHTML(block); this.bindImageBlockEvents(blockElement, block); } container.appendChild(blockElement); } getTextBlockHTML(block) { return `
    `; } getImageBlockHTML(block) { if (block.content) { return `
    ${block.alt || ''}
    크기: ${this.getImageSize(block.content)}
    `; } else { return `
    📷
    이미지를 드래그하거나 클릭하여 업로드
    지원 형식: JPG, PNG, GIF (최대 2MB)
    `; } } bindTextBlockEvents(blockElement, block) { const textarea = blockElement.querySelector('textarea'); const deleteBtn = blockElement.querySelector('.delete'); textarea.addEventListener('input', () => { block.content = textarea.value; this.updateContentBlocksData(); }); deleteBtn.addEventListener('click', () => { this.deleteBlock(block.id); }); } bindImageBlockEvents(blockElement, block) { const uploadArea = blockElement.querySelector('.image-upload-area'); const fileInput = blockElement.querySelector('input[type="file"]'); const deleteBtn = blockElement.querySelector('.delete'); const altInput = blockElement.querySelector('.image-alt-input'); if (uploadArea) { uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length > 0) { this.handleImageUpload(files[0], block); } }); } if (fileInput) { fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { this.handleImageUpload(e.target.files[0], block); } }); } if (altInput) { altInput.addEventListener('input', () => { block.alt = altInput.value; this.updateContentBlocksData(); }); } deleteBtn.addEventListener('click', () => { this.deleteBlock(block.id); }); // 이미지 클릭 시 전체 화면으로 보기 - 원본 이미지 사용 const uploadedImage = blockElement.querySelector('.uploaded-image'); if (uploadedImage) { uploadedImage.addEventListener('click', () => { // 원본 이미지(block.content) 사용 this.showImageModal(block.content); }); } } async handleImageUpload(file, block) { if (!file.type.startsWith('image/')) { alert('이미지 파일만 업로드 가능합니다.'); return; } if (file.size > 2 * 1024 * 1024) { alert('이미지 크기는 2MB 이하여야 합니다.'); return; } try { // 원본 이미지 Base64 변환 const originalBase64 = await this.fileToBase64(file); // 썸네일 생성 (표시용) const thumbnailBase64 = await this.createThumbnail(file, 300, 200); block.content = originalBase64; block.thumbnail = thumbnailBase64; // 블록 다시 렌더링 const blockElement = document.querySelector(`[data-block-id="${block.id}"]`); blockElement.innerHTML = this.getImageBlockHTML(block); this.bindImageBlockEvents(blockElement, block); this.updateContentBlocksData(); } catch (error) { console.error('이미지 업로드 오류:', error); alert('이미지 업로드에 실패했습니다.'); } } fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } createThumbnail(file, maxWidth, maxHeight) { return new Promise((resolve) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { // 비율 계산 const ratio = Math.min(maxWidth / img.width, maxHeight / img.height); const width = img.width * ratio; const height = img.height * ratio; canvas.width = width; canvas.height = height; // 이미지 그리기 ctx.drawImage(img, 0, 0, width, height); // Base64로 변환 (품질 0.7) resolve(canvas.toDataURL('image/jpeg', 0.7)); }; img.src = URL.createObjectURL(file); }); } getImageSize(base64) { const sizeInBytes = Math.round((base64.length * 3) / 4); if (sizeInBytes < 1024) { return sizeInBytes + ' B'; } else if (sizeInBytes < 1024 * 1024) { return Math.round(sizeInBytes / 1024) + ' KB'; } else { return Math.round(sizeInBytes / (1024 * 1024)) + ' MB'; } } changeImage(blockId) { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = (e) => { if (e.target.files.length > 0) { const block = this.blocks.find(b => b.id === blockId); if (block) { this.handleImageUpload(e.target.files[0], block); } } }; input.click(); } showImageModal(imageSrc) { let modal = document.querySelector('.image-modal'); if (!modal) { modal = document.createElement('div'); modal.className = 'image-modal'; modal.innerHTML = ` × 전체 화면 이미지 `; document.body.appendChild(modal); modal.querySelector('.image-modal-close').addEventListener('click', () => { modal.classList.remove('active'); }); modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); } modal.querySelector('img').src = imageSrc; modal.classList.add('active'); } deleteBlock(blockId) { this.blocks = this.blocks.filter(block => block.id !== blockId); const blockElement = document.querySelector(`[data-block-id="${blockId}"]`); if (blockElement) { blockElement.remove(); } this.updateContentBlocksData(); } updateContentBlocksData() { // 전역 변수에 저장 window.currentContentBlocks = this.blocks; } getContentForSave() { return { blocks: this.blocks, legacy_text: this.blocks .filter(block => block.type === 'text') .map(block => block.content) .join('\n\n') }; } loadFromData(contentBlocks) { this.blocks = contentBlocks || []; const container = document.getElementById('content-blocks'); if (container) { container.innerHTML = ''; } if (this.blocks.length === 0) { this.addInitialTextBlock(); } else { this.blocks.forEach(block => this.renderBlock(block)); } } } // 블록 에디터 초기화 함수 function initializeBlockEditor() { if (document.getElementById('content-blocks-editor')) { window.blockEditor = new BlockEditor(); } }