/** * ================================================================================= * common.js - 블로그 공통 스크립트 (최종 수정본) * - Quill 에디터 초기화 및 제어 (편집/읽기 모드) * - 게시물 데이터 관리 (baseData) 및 서버 통신 (save, post) * - UI 제어 (팝업, 컨트롤 박스 동적 설정) * - 페이지 이동 및 로그인/로그아웃, 유틸리티 함수 * ================================================================================= */ // 전역 변수: Quill 에디터 인스턴스와 게시물 기본 데이터를 저장합니다. var quill = null; var currentLat = 0.0; var currentLon = 0.0; var baseData = { 'id': "", 'title': "", 'content': "", 'category': "none", 'tags': "", 'firstPostLat': 0.0, 'firstPostLon': 0.0, 'modifyLat': 0.0, 'modifyLon': 0.0, 'originId': "", 'writeTime': 0, }; // jQuery를 사용하여 문서가 완전히 로드된 후에 함수를 실행합니다. $(document).ready(function() { // 뷰어/에디터 페이지가 아닐 수 있으므로, #editor 요소가 있을 때만 initEditor를 호출하도록 방어 코드를 추가하는 것이 좋습니다. // 현재는 각 페이지에서 직접 호출하므로 이 코드는 참고용입니다. // 사이드바의 인기글/최신글 목록을 가져옵니다. if (document.querySelector(".rank_of_view")) { fetchRankOfViews(); } if (document.querySelector(".recent_posts")) { fetchRecentPosts(); } // 팝업 닫기 버튼 이벤트 $('.btn_layerClose').on('click', function(e) { e.preventDefault(); closePopup(); }); // 로그인 폼 제출 이벤트 $('#loginFormElement').on('submit', function(e) { e.preventDefault(); submitLoginForm(); }); // 로그인 팝업 열기 버튼 이벤트 $('.open-login-popup').on('click', function() { openPopup(this); }); }); /** * [핵심] Quill 에디터를 초기화하는 메인 함수입니다. * useEditor 파라미터 값에 따라 '편집 모드'와 '읽기 모드'를 동적으로 전환합니다. * @param {boolean} useEditor - true: 편집기 활성화, false: 읽기 전용 뷰어 활성화 */ function initEditor(useEditor = false) { console.log("### initEditor 함수 실행됨! 편집 모드:", useEditor, "###"); // 이 줄을 추가! const editorContainer = document.querySelector('#editor'); if (!editorContainer) return; if (typeof serverData !== 'undefined') { baseData.id = serverData.id; baseData.title = decodeURIComponent(serverData.title || ''); baseData.content = decodeURIComponent(serverData.content || ''); baseData.category = serverData.category; baseData.tags = serverData.tags; baseData.firstPostLat = serverData.firstPostLat; baseData.firstPostLon = serverData.firstPostLon; baseData.writeTime = serverData.writeTime; baseData.originId = serverData.originId; } getLocation(); try { var Font = Quill.import('formats/font'); Font.whitelist = ['sans-serif', 'serif', 'monospace', 'arial', 'georgia', 'comic-sans-ms', 'courier-new', 'roboto', 'playfair-display']; Quill.register(Font, true); Quill.register({ 'modules/table-better': QuillTableBetter }, true); const quillOptions = { theme: 'snow', modules: useEditor ? { toolbar: { container: [ [{ font: Font.whitelist }], [{ 'size': ['small', false, 'large', 'huge'] }], ['bold', 'italic', 'underline', 'strike'], [{ 'color': [] }, { 'background': [] }], [{ 'header': 1 }, { 'header': 2 }, 'blockquote', 'code-block'], [{ 'script': 'sub'}, { 'script': 'super' }], [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'indent': '-1'}, { 'indent': '+1' }], ['link', 'image', 'video'], ['table-better'], [{ 'direction': 'rtl' }], [{ 'align': [] }], ['clean'] ], handlers: { image: function() { selectLocalImage(); }, video: function() { selectLocalVideo(); } } }, 'table-better': { language: 'en_US', toolbarTable: true }, keyboard: { bindings: QuillTableBetter.keyboardBindings } } : { toolbar: false }, readOnly: !useEditor }; quill = new Quill(editorContainer, quillOptions); if (baseData.content) { loadContent(baseData.content); } if (!useEditor) { editorContainer.classList.add('readonly-mode'); } else { editorContainer.classList.remove('readonly-mode'); const titleField = document.querySelector("#title_field"); if (titleField) { titleField.value = baseData.title; } } } catch (e) { console.error("Quill initialization failed:", e); } setupControlBox(useEditor ? 'edit' : 'view'); } function selectLocalImage() { // 이미지 URL 입력 받기 const url = prompt("이미지 URL을 입력하거나 빈칸으로 두시면 파일 업로드를 합니다."); if (url) { // URL이 입력된 경우 이미지 삽입 const range = quill.getSelection(true); quill.insertEmbed(range.index, 'image', url); quill.setSelection(range.index + 1); } else { // URL이 없거나 취소한 경우 파일 업로드 처리 const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.click(); input.onchange = async () => { const file = input.files[0]; if (file) { const file = input.files[0]; console.log("on selectLocalImage File", file); if (!file || !file.type.startsWith('image/')) { console.warn('이미지 파일만 업로드 가능합니다.'); return; } uploadImage(file); } }; } } function uploadImage(blob) { const formData = new FormData(); formData.append('file', blob); let uploadUrl = getMainPath() + "/blog/post/imageUpload.bjx"; let imageUrl = getMainPath() + '/blog/post/images/'; $.ajax({ type: 'POST', enctype: 'multipart/form-data', url: uploadUrl, data: formData, dataType: 'json', processData: false, contentType: false, cache: false, timeout: 600000, success: function (data) { console.log(data); imageUrl += data.fileName; insertToEditor(imageUrl); }, error: function (e) { console.error(e); // callback('image_load_fail'); } }); } function selectLocalVideo() { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'video/*'); input.click(); input.onchange = () => { const file = input.files[0]; if (!file || !file.type.startsWith('video/')) { alert('동영상 파일만 업로드할 수 있습니다.'); return; } uploadVideo(file); }; } function uploadVideo(file) { const formData = new FormData(); formData.append('video', file); fetch('/api/upload/video', { method: 'POST', body: formData }) .then(res => res.json()) .then(result => { if (result.url) { const range = quill.getSelection(true); quill.insertEmbed(range.index, 'video', result.url); quill.setSelection(range.index + 1); } else { console.error('동영상 업로드 실패', result); } }) .catch(err => { console.error('업로드 중 오류', err); }); } function insertToEditor(url) { const range = quill.getSelection(true); quill.insertEmbed(range.index, 'image', url); quill.setSelection(range.index + 1); } /** * 에디터 모드('edit' 또는 'view')에 따라 컨트롤 박스를 설정합니다. */ function setupControlBox(mode) { const categoryBox = document.querySelector('.controlbox-category'); const hashtagBox = document.querySelector('.controlbox-hashtag'); if (!categoryBox || !hashtagBox) return; if (mode === 'edit') { categoryBox.setAttribute('onclick', 'openPopup(this)'); hashtagBox.setAttribute('onclick', 'openPopup(this)'); categoryBox.innerText = '카테고리 설정'; hashtagBox.innerText = '해시태그 편집'; fetchCategoriesAndHashtags(); } else { categoryBox.removeAttribute('onclick'); hashtagBox.removeAttribute('onclick'); categoryBox.classList.remove('btn-example'); hashtagBox.classList.remove('btn-example'); categoryBox.innerHTML = `카테고리: ${baseData.category || '지정되지 않음'}`; hashtagBox.innerHTML = '태그: '; if (baseData.tags && baseData.tags.length > 0) { baseData.tags.split(',').forEach(tag => { hashtagBox.innerHTML += `#${tag.trim()}`; }); } else { hashtagBox.innerHTML += '없음'; } } } /** * 백엔드 API를 호출하여 카테고리와 해시태그 목록을 가져와 팝업을 채웁니다. */ function fetchCategoriesAndHashtags() { fetch(`${getMainPath()}/blog/categories.bjx`).then(res => res.json()).then(data => { if (data.resultCode === 0 && data.tags) { const list = document.querySelector('#category-list'); if(list) { list.innerHTML = ''; data.tags.forEach(tag => { const el = document.createElement('span'); el.className = 'tag-item'; el.innerText = tag; list.appendChild(el); }); } } }).catch(err => console.error('Error fetching categories:', err)); fetch(`${getMainPath()}/blog/hashtags.bjx`).then(res => res.json()).then(data => { if (data.resultCode === 0 && data.tags) { const list = document.querySelector('#hashtag-list'); if(list) { list.innerHTML = ''; data.tags.forEach(tag => { const el = document.createElement('span'); el.className = 'tag-item'; el.innerText = `#${tag}`; list.appendChild(el); }); } } }).catch(err => console.error('Error fetching hashtags:', err)); } /** * 컨텐츠를 Quill 에디터에 로드합니다. */ function loadContent(content) { try { const delta = JSON.parse(content); if (delta && Array.isArray(delta.ops)) { quill.setContents(delta); return; } } catch (e) { /* HTML 문자열일 경우 아래에서 처리 */ } quill.clipboard.dangerouslyPasteHTML(content); } /** * 게시물 수정 페이지로 이동합니다. */ function loadEditor() { if (baseData.id) { location.href = `${getMainPath()}/blog/edit/${baseData.id}`; } } /** * 작성된 게시물을 서버에 저장합니다. */ function save() { const titleField = document.getElementById('title_field'); if (titleField) { baseData.title = encodeURIComponent(titleField.value); } baseData.content = encodeURIComponent(JSON.stringify(quill.getContents())); baseData.modifyLat = currentLat; baseData.modifyLon = currentLon; const uploadUrl = `${getMainPath()}/blog/post.bjx`; if (confirm("해당 내용으로 저장하시겠습니까?")) { post(uploadUrl, serverData.enc, JSON.stringify(baseData), serverData.keyword, function(resultData) { alert("저장되었습니다."); }); } } /** * 사용자의 현재 위치(위도, 경도)를 가져옵니다. */ function getLocation() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(pos => { currentLat = pos.coords.latitude; currentLon = pos.coords.longitude; if (baseData.firstPostLat === 0.0) baseData.firstPostLat = currentLat; if (baseData.firstPostLon === 0.0) baseData.firstPostLon = currentLon; baseData.modifyLat = currentLat; baseData.modifyLon = currentLon; const locationField = document.getElementById('location_field'); if (locationField) { locationField.textContent = `Lat: ${currentLat.toFixed(4)}, Lon: ${currentLon.toFixed(4)}`; } }); } } /** * 팝업 레이어를 엽니다. */ function openPopup(element) { const targetId = element.getAttribute('to'); const popup = document.querySelector(targetId); const overlay = document.querySelector('.dim_layer'); if (popup && overlay) { overlay.style.display = 'block'; popup.style.display = 'block'; } } /** * 팝업 레이어를 닫습니다. */ function closePopup() { const overlay = document.querySelector('.dim_layer'); if(overlay) overlay.style.display = 'none'; document.querySelectorAll('.pop_layer').forEach(p => p.style.display = 'none'); } /** * 게시물 상세 보기 페이지로 이동합니다. */ function goToViewer(element) { if (element && element.id) { location.href = `${getMainPath()}/blog/viewer/${element.id}`; } } // ================================================================================= // [복구] 이하 누락되었던 함수들 // ================================================================================= /** * 인기글 목록을 가져와 UI에 표시합니다. */ function fetchRankOfViews() { fetch(`${getMainPath()}/blog/rankOfViews.bjx`).then(res => res.json()).then(data => { const ul = document.querySelector('.rank_of_view'); if (ul && data.posts) { ul.innerHTML = ''; data.posts.forEach(item => { const date = new Date(item.writeTime); const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; ul.innerHTML += `