SearchTrademark/sayings.js

2678 lines
90 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 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 생성자 호출');
}
// 설정 로드 함수 (Background.js 통합 버전)
async loadConfig() {
try {
this.debugLog('chrome.storage에서 설정 로드 시작');
// access_token만 로드 (SUPABASE 설정은 background.js에서 관리)
const storageData = await chrome.storage.local.get(['access_token', 'sayings_config']);
// DEBUG_MODE 설정 확인
if (storageData.sayings_config && storageData.sayings_config.DEBUG_MODE !== undefined) {
this.DEBUG_MODE = storageData.sayings_config.DEBUG_MODE;
} else {
this.DEBUG_MODE = false; // 기본값: 비활성화
}
this.ACCESS_TOKEN = storageData.access_token;
this.debugLog('설정 로드 결과', {
hasToken: !!this.ACCESS_TOKEN,
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
DEBUG_MODE: this.DEBUG_MODE
});
// 토큰 검증
if (!this.ACCESS_TOKEN) {
throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다');
}
this.isConfigLoaded = true;
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}<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('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);
}
}
// 토큰 유효성 검증 (Background.js 통합 버전)
async validateToken() {
try {
this.debugLog('토큰 검증 시작 (Background 요청)');
const response = await chrome.runtime.sendMessage({
action: 'getUserInfo',
token: this.ACCESS_TOKEN
});
if (!response || !response.success) {
throw new Error(response?.error || '토큰이 유효하지 않습니다');
}
this.debugLog('토큰 검증 성공', {
email: response.user.email,
id: response.user.id
});
return response.user;
} catch (error) {
this.debugLog('토큰 검증 중 오류', {
errorName: error.name,
errorMessage: error.message
});
throw error;
}
}
// 타냐대장경 목록 로드 (Background.js 통합 버전)
async loadSayings() {
this.debugLog('타냐대장경 로딩 시작 (Background 요청)');
try {
// 로딩 표시
document.getElementById('sayings-container').innerHTML = '<div class="loading">📚 타냐대장경을 불러오는 중...</div>';
const response = await chrome.runtime.sendMessage({
action: 'getSayings',
token: this.ACCESS_TOKEN,
adminApproval: true
});
if (!response || !response.success) {
throw new Error(response?.error || '타냐대장경 로딩 실패');
}
const sayings = response.sayings || [];
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 = '<div class="empty-state">📝 표시할 타냐대장경이 없습니다.</div>';
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 `
<div class="saying-card" data-category="${saying.cat}" data-target="${saying.target}">
<div class="saying-header">
<h3 class="saying-title">${this.escapeHtml(saying.saying_title)}</h3>
<div class="saying-meta">
<span class="saying-category">${this.escapeHtml(saying.cat)}</span>
<span class="saying-target">${this.escapeHtml(saying.target)}</span>
</div>
</div>
<div class="saying-content">
${contentHtml}
</div>
<div class="saying-footer">
<span class="saying-author">👤 ${this.escapeHtml(authorName)}</span>
<span class="saying-date">📅 ${formattedDate}</span>
</div>
</div>
`;
}));
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 `<div class="content-text-block">${this.escapeHtml(block.content || '').replace(/\n/g, '<br>')}</div>`;
} else if (block.type === 'image') {
if (block.content) {
return `
<div class="content-image-block">
<img src="${block.thumbnail || block.content}"
alt="${this.escapeHtml(block.alt || '')}"
class="content-image"
data-original="${block.content}"
onclick="window.sayingsManager.showFullImage(this.dataset.original)"
style="max-width: 100%; height: auto; border-radius: 8px; cursor: pointer; transition: transform 0.3s ease; margin: 10px 0;"
onmouseover="this.style.transform='scale(1.02)'"
onmouseout="this.style.transform='scale(1)'" />
${block.alt ? `<div class="image-caption" style="font-size: 12px; color: #666; margin-top: 5px; text-align: center;">${this.escapeHtml(block.alt)}</div>` : ''}
</div>
`;
}
}
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 = `
<div style="position: relative; max-width: 95%; max-height: 95%; display: flex; align-items: center; justify-content: center;">
<span class="close-btn" style="
position: absolute;
top: -40px;
right: -40px;
color: white;
font-size: 32px;
cursor: pointer;
z-index: 10001;
background: rgba(0,0,0,0.7);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
">&times;</span>
<img src="" alt="전체 화면 이미지" style="
max-width: 100%;
max-height: 100%;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.8);
cursor: zoom-in;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
" />
<div class="image-controls" style="
position: absolute;
bottom: -50px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 25px;
">
<button class="zoom-btn" data-action="zoom-in" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
">+</button>
<button class="zoom-btn" data-action="zoom-out" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
">-</button>
<button class="zoom-btn" data-action="zoom-reset" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
">1:1</button>
</div>
</div>
`;
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, '<pre><code>$1</code></pre>')
// 인라인 코드 (`)
.replace(/`([^`]+)`/g, '<code>$1</code>')
// 볼드 텍스트 (**)
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
// 이탤릭 텍스트 (*)
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
// 헤더 (###, ##, #)
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
// 인용문 (>)
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
// 링크 [텍스트](URL)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
// 리스트 (- 또는 *)
.replace(/^[*-] (.+)$/gm, '<li>$1</li>')
// 줄바꿈 처리
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>');
// 리스트 태그로 감싸기
html = html.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>');
// 단락 태그로 감싸기 (이미 p 태그가 있는 경우 제외)
if (!html.includes('<p>') && !html.includes('<h1>') && !html.includes('<h2>') && !html.includes('<h3>')) {
html = '<p>' + html + '</p>';
}
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 = `
<div class="empty-state">
<div class="icon">❌</div>
<div class="title">오류가 발생했습니다</div>
<div class="description">${message}</div>
</div>
`;
}
if (statsDiv) {
statsDiv.innerHTML = `<div style="color: red;">오류: ${message}</div>`;
}
this.debugLog('에러 렌더링 완료', { message });
}
// 로그 확인 함수
showDebugLogs() {
const debugElement = document.getElementById('debug-info');
if (!debugElement) return;
// 현재 상태 수집
const stats = this.calculateStats();
const currentUser = this.currentUser;
// 디버그 정보 생성
const debugInfo = `
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0; font-family: monospace; font-size: 12px; line-height: 1.4;">
<h4 style="margin: 0 0 10px 0; color: #495057;">🔧 디버그 정보</h4>
<div style="margin-bottom: 8px;"><strong>📊 타냐대장경 통계:</strong></div>
<div style="margin-left: 15px; margin-bottom: 10px;">
• 전체 타냐대장경: ${stats.total}개<br>
• 필터링된 타냐대장경: ${this.filteredSayings.length}개<br>
• 카테고리별: ${Object.entries(stats.byCategory).map(([cat, count]) => `${cat}(${count})`).join(', ')}
</div>
<div style="margin-bottom: 8px;"><strong>👤 사용자 정보:</strong></div>
<div style="margin-left: 15px; margin-bottom: 10px;">
${currentUser ? `• 이메일: ${currentUser.email}<br>• 닉네임: ${currentUser.nickname}<br>• 회원등급: ${currentUser.membership_level || '기본'}` : '• 로그인 필요'}
</div>
<div style="margin-bottom: 8px;"><strong>🔔 백그라운드 새 타냐대장경 감지:</strong></div>
<div style="margin-left: 15px; margin-bottom: 10px;">
• 상태: 활성화 (1분 간격)<br>
• 방식: 백그라운드 스크립트 연동<br>
• 마지막 확인: 백그라운드에서 관리<br>
• 브라우저 알림: 지원됨<br>
• 로컬 스토리지: 사용 중
</div>
<div style="margin-bottom: 8px;"><strong>⚙️ 시스템 상태:</strong></div>
<div style="margin-left: 15px; margin-bottom: 10px;">
• 설정 로드: ${this.isConfigLoaded ? '완료' : '실패'}<br>
• API 연결: Background.js 통합<br>
• 토큰 상태: ${this.ACCESS_TOKEN ? '있음' : '없음'}<br>
• 디버그 모드: ${this.DEBUG_MODE ? '활성화' : '비활성화'}
</div>
<div style="margin-top: 15px;">
<button id="test-new-sayings-btn" style="
background: #28a745;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-right: 8px;
">🧪 새 타냐대장경 감지 테스트</button>
<button id="clear-storage-btn" style="
background: #dc3545;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">🗑️ 스토리지 정리</button>
</div>
</div>
`;
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('현재 사용자 정보 로드 시작 (Background 요청)');
if (!this.ACCESS_TOKEN) {
throw new Error('로그인이 필요합니다.');
}
const response = await chrome.runtime.sendMessage({
action: 'getUserInfo',
token: this.ACCESS_TOKEN
});
if (!response || !response.success) {
throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
}
const user = response.user;
this.currentUser = {
id: user.id,
nickname: user.nickname || user.email,
email: user.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 = '<div style="padding: 20px; text-align: center; color: #999;">미리보기할 내용을 입력해주세요</div>';
return;
}
// 로딩 상태 표시
previewDiv.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">미리보기 생성 중...</div>';
try {
// 마크다운을 HTML로 변환
const htmlContent = await this.convertMarkdownToHtml(content);
// 변환된 HTML을 미리보기에 표시
previewDiv.innerHTML = htmlContent || '<div style="padding: 20px; text-align: center; color: #999;">내용을 변환할 수 없습니다</div>';
console.log('✅ 마크다운 미리보기 렌더링 완료');
} catch (error) {
console.error('❌ 마크다운 미리보기 렌더링 실패:', error);
previewDiv.innerHTML = '<div style="padding: 20px; text-align: center; color: #f44336;">미리보기 생성 중 오류가 발생했습니다</div>';
}
}
// 타냐대장경 제출 처리
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 호출 (Background.js 통합 버전)
const response = await chrome.runtime.sendMessage({
action: 'createSaying',
token: this.ACCESS_TOKEN,
sayingData: sayingData
});
if (!response || !response.success) {
throw new Error(response?.error || '타냐대장경 등록 실패');
}
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 = `
<div style="font-size: 48px; margin-bottom: 16px;">🎉</div>
<h3 style="margin: 0 0 16px 0; color: #28a745;">타냐대장경 등록 완료!</h3>
<div style="margin-bottom: 20px; color: #666;">
<div style="margin-bottom: 8px;"><strong>제목:</strong> ${title}</div>
<div style="margin-bottom: 8px;"><strong>카테고리:</strong> ${category}</div>
<div style="margin-bottom: 8px;"><strong>타겟:</strong> ${target}</div>
<div style="margin-bottom: 8px;"><strong>등록자:</strong> ${this.currentUser.nickname}</div>
</div>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 12px; margin-bottom: 20px; color: #856404;">
<strong>📋 안내사항</strong><br>
등록된 타냐대장경은 관리자 승인 후 목록에 표시됩니다.
</div>
<button id="close-popup" style="
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
">확인</button>
`;
// 팝업을 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 });
}
// 카테고리와 타겟 옵션을 백엔드에서 로드 (Background.js 통합 버전)
async loadCategoryAndTargetOptions() {
this.debugLog('카테고리와 타겟 옵션 로드 시작 (Background 요청)');
try {
// 카테고리와 타겟 데이터를 병렬로 가져오기
const [categoryResponse, targetResponse] = await Promise.all([
chrome.runtime.sendMessage({
action: 'getSayingsCategories',
token: this.ACCESS_TOKEN
}),
chrome.runtime.sendMessage({
action: 'getSayingsTargets',
token: this.ACCESS_TOKEN
})
]);
// 카테고리 응답 처리
if (categoryResponse && categoryResponse.success) {
const categoryData = categoryResponse.categories || [];
this.debugLog('카테고리 데이터 로드 성공', { count: categoryData.length });
if (categoryData.length === 0) {
throw new Error('카테고리 데이터가 비어있습니다. sayings_cat 테이블을 확인해주세요.');
}
// 카테고리 드롭다운 업데이트
this.updateCategoryDropdowns(categoryData);
} else {
throw new Error(categoryResponse?.error || '카테고리 데이터 로드 실패');
}
// 타겟 응답 처리
if (targetResponse && targetResponse.success) {
const targetData = targetResponse.targets || [];
this.debugLog('타겟 데이터 로드 성공', { count: targetData.length });
if (targetData.length === 0) {
throw new Error('타겟 데이터가 비어있습니다. sayings_target 테이블을 확인해주세요.');
}
// 타겟 드롭다운 업데이트
this.updateTargetDropdowns(targetData);
} else {
throw new Error(targetResponse?.error || '타겟 데이터 로드 실패');
}
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 = `
<div id="newSayingsModal" class="modal-overlay" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
animation: fadeIn 0.3s ease-out;
">
<div class="modal-content" style="
background: white;
border-radius: 12px;
padding: 24px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
animation: slideIn 0.3s ease-out;
position: relative;
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0; color: #2c3e50; font-size: 24px;">🎉 새 타냐대장경 알림</h2>
<div id="countdownTimer" style="
background: #e74c3c;
color: white;
padding: 8px 12px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
">10초 후 자동 닫힘</div>
</div>
<p style="margin-bottom: 20px; color: #34495e; font-size: 16px;">
<strong>${newSayings.length}개</strong>의 새로운 타냐대장경이 등록되었습니다!
</p>
<div id="newSayingsList" style="margin-bottom: 20px;">
${newSayings.map(saying => `
<div style="
border: 1px solid #ecf0f1;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
background: #f8f9fa;
">
<div style="font-weight: bold; color: #2c3e50; margin-bottom: 8px;">
"${saying.title || saying.saying_title}"
</div>
<div style="color: #7f8c8d; font-size: 14px; margin-bottom: 4px;">
👤 ${saying.author || saying.register || '알 수 없음'} | 📂 ${saying.sayings_cat?.name || saying.cat || '미분류'} | 🎯 ${saying.sayings_target?.name || saying.target || '전체'}
</div>
<div style="color: #95a5a6; font-size: 12px;">
📅 ${new Date(saying.created_at).toLocaleString('ko-KR')}
</div>
</div>
`).join('')}
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button id="dismissNewSayings" style="
padding: 10px 20px;
border: 1px solid #bdc3c7;
background: white;
color: #7f8c8d;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
">나중에</button>
<button id="viewNewSayings" style="
padding: 10px 20px;
border: none;
background: #3498db;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
">새 타냐대장경 보기</button>
</div>
</div>
</div>
<style>
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: translateY(-30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
#newSayingsModal button:hover {
opacity: 0.8;
transform: translateY(-1px);
}
</style>
`;
// 모달을 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 = `
<span class="meta-badge category-badge">${this.escapeHtml(saying.cat || '기타')}</span>
<span class="meta-badge target-badge">${this.escapeHtml(saying.target || '기타')}</span>
`;
// 작성자 정보 설정
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 = `
<div class="author-details">
<div class="author-avatar">${authorName.charAt(0).toUpperCase()}</div>
<div>
<div class="author-name">${this.escapeHtml(authorName)}</div>
<div class="creation-date">${createdAt}</div>
</div>
</div>
`;
// 편집 모드 데이터 설정 (작성자인 경우에만)
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('보기 모드로 전환');
}
// 편집된 타냐대장경 저장 (Background.js 통합 버전)
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 chrome.runtime.sendMessage({
action: 'updateSaying',
token: this.ACCESS_TOKEN,
sayingId: this.currentEditingSaying.id,
sayingData: {
saying_title: title,
saying: content,
cat: category,
target: target
}
});
if (response && response.success) {
alert('타냐대장경이 성공적으로 수정되었습니다.');
// 모달 닫기
document.getElementById('view-saying-modal').style.display = 'none';
this.currentEditingSaying = null;
// 타냐대장경 목록 새로고침
await this.loadSayings();
} else {
throw new Error(response?.error || '타냐대장경 수정에 실패했습니다.');
}
} catch (error) {
console.error('타냐대장경 수정 오류:', error);
alert('타냐대장경 수정 중 오류가 발생했습니다: ' + error.message);
}
}
// 타냐대장경 삭제 (Background.js 통합 버전)
async deleteSaying(saying) {
if (!confirm('정말로 이 타냐대장경을 삭제하시겠습니까?')) {
return;
}
try {
const response = await chrome.runtime.sendMessage({
action: 'deleteSaying',
token: this.ACCESS_TOKEN,
sayingId: saying.id
});
if (response && response.success) {
alert('타냐대장경이 성공적으로 삭제되었습니다.');
// 모달 닫기
document.getElementById('view-saying-modal').style.display = 'none';
this.currentEditingSaying = null;
// 타냐대장경 목록 새로고침
await this.loadSayings();
} else {
throw new Error(response?.error || '타냐대장경 삭제에 실패했습니다.');
}
} catch (error) {
console.error('타냐대장경 삭제 오류:', error);
alert('타냐대장경 삭제 중 오류가 발생했습니다: ' + error.message);
}
}
}
// 페이지 로드 시 초기화
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');
// DEBUG_MODE 설정 확인
if (configData.sayings_config && configData.sayings_config.DEBUG_MODE !== undefined) {
debugMode = configData.sayings_config.DEBUG_MODE;
}
console.log('디버그 모드:', debugMode);
} 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 = `
<div style="color: red; text-align: center; padding: 20px;">
❌ 초기화 실패<br>
${error.message}<br>
<small>로그인 후 다시 시도해주세요.</small>
</div>
`;
}
const container = document.getElementById("sayings-container");
if (container) {
container.innerHTML = `
<div class="empty-state">
<div class="icon">❌</div>
<div class="title">초기화 실패</div>
<div class="description">${error.message}</div>
</div>
`;
}
// 디버그 정보에도 표시 (DEBUG_MODE일 때만)
if (debugMode) {
const debugElement = document.getElementById('debug-info');
if (debugElement) {
debugElement.innerHTML = `❌ 초기화 실패: ${error.message}<br><button id="log-check-btn" style="display: inline-block;">📋 로그 확인</button>`;
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 `
<div class="block-controls">
<button class="block-control-btn move" title="이동">⬍</button>
<button class="block-control-btn delete" title="삭제">×</button>
</div>
<div class="text-block">
<textarea placeholder="텍스트를 입력하세요...">${block.content || ''}</textarea>
</div>
`;
}
getImageBlockHTML(block) {
if (block.content) {
return `
<div class="block-controls">
<button class="block-control-btn move" title="이동">⬍</button>
<button class="block-control-btn delete" title="삭제">×</button>
</div>
<div class="image-block">
<img src="${block.thumbnail || block.content}" class="uploaded-image" alt="${block.alt || ''}" />
<div class="image-info">
<span>크기: ${this.getImageSize(block.content)}</span>
<button class="btn-small" onclick="window.blockEditor.changeImage('${block.id}')">이미지 변경</button>
</div>
<input type="text" class="image-alt-input" placeholder="이미지 설명 (선택사항)" value="${block.alt || ''}" />
</div>
`;
} else {
return `
<div class="block-controls">
<button class="block-control-btn move" title="이동">⬍</button>
<button class="block-control-btn delete" title="삭제">×</button>
</div>
<div class="image-block">
<div class="image-upload-area">
<div style="font-size: 48px; margin-bottom: 10px;">📷</div>
<div>이미지를 드래그하거나 클릭하여 업로드</div>
<div style="font-size: 12px; color: #666; margin-top: 5px;">지원 형식: JPG, PNG, GIF (최대 2MB)</div>
</div>
<input type="file" accept="image/*" style="display: none;" />
</div>
`;
}
}
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 = `
<span class="image-modal-close">&times;</span>
<img src="" alt="전체 화면 이미지" />
`;
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();
}
}