2678 lines
90 KiB
JavaScript
2678 lines
90 KiB
JavaScript
// 타냐대장경 관리 모듈
|
||
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;
|
||
">×</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">×</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();
|
||
}
|
||
} |