/** * ================================================================================= * 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 += `
  • ${item.title}
    [${formattedDate}]
  • `; }); } }).catch(error => console.error('Failed to fetch rank of views:', error)); } /** * 최신글 목록을 가져와 UI에 표시합니다. */ function fetchRecentPosts() { fetch(`${getMainPath()}/blog/recentOfPost.bjx`).then(res => res.json()).then(data => { const ul = document.querySelector('.recent_posts'); 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 += `
  • ${item.title}
    [${formattedDate}]
  • `; }); } }).catch(error => console.error('Failed to fetch recent posts:', error)); } /** * 로그인 폼 데이터를 서버에 전송합니다. */ function submitLoginForm() { const data = { 'user_id': $('#loginId').val(), 'user_pw': $('#loginPassword').val(), 'rememberMe': $('#rememberMe').is(':checked'), }; postLogin(`${getMainPath()}/user/login.bjx`, serverData.enc, JSON.stringify(data), serverData.keyword, function(response) { if (response.isOk) { location.reload(); } else { alert(`로그인 실패: ${response.resultMsg}`); } }); } // --- 페이지 이동(Navigation) 함수들 --- function gotoHome() { document.location.replace(`${getMainPath()}/home.bs`); } function gotoWrite() { document.location.replace(`${getMainPath()}/blog/edit`); } // 수정된 URL function gotoModify() { document.location.replace(`${getMainPath()}/blog/posts`); } // 수정된 URL function gotoLogin() { document.location.replace(`${getMainPath()}/login.bs`); } function gotoJoin() { document.location.replace(`${getMainPath()}/user/join.bs`); } /** * 로그아웃을 처리합니다. */ function logout() { const form = document.createElement('form'); form.method = 'POST'; form.action = `${getMainPath()}/user/logout.bs`; // Spring Security CSRF 토큰 추가 const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content'); const csrfParam = document.querySelector('meta[name="_csrf_parameter"]').getAttribute('content'); if (csrfToken && csrfParam) { const csrfInput = document.createElement('input'); csrfInput.type = 'hidden'; csrfInput.name = csrfParam; csrfInput.value = csrfToken; form.appendChild(csrfInput); } document.body.appendChild(form); form.submit(); } // ================================================================================= // 서버 통신 및 암호화 관련 유틸리티 함수들 (기존 코드 유지) // ================================================================================= function getMainPath() { return location.protocol + "//" + location.hostname + (location.port ? ':' + location.port : ''); } function post(target, type, data, key, callBackResult) { const httpRequest = new XMLHttpRequest(); httpRequest.onreadystatechange = () => { if (httpRequest.readyState === XMLHttpRequest.DONE) { if (httpRequest.status === 200) callBackResult(httpRequest.response); else alert('Request Error!'); } }; httpRequest.open('POST', target, true); httpRequest.setRequestHeader("Content-Type", "text/plain"); httpRequest.send(btoa(JSON.stringify({ 'data': unformat(type, data, key), 'key': key, 'type': type, }))); } function postLogin(target, type, data, key, callBackResult) { const httpRequest = new XMLHttpRequest(); httpRequest.onreadystatechange = () => { if (httpRequest.readyState === XMLHttpRequest.DONE) { if (httpRequest.status === 200) { try { callBackResult(JSON.parse(httpRequest.response)); } catch (e) { console.error("Login response parse error:", e); } } else { alert('Request Error!'); } } }; httpRequest.withCredentials = true; httpRequest.open('POST', target, true); httpRequest.setRequestHeader("Content-Type", "text/plain"); httpRequest.send(btoa(JSON.stringify({ 'data': unformat(type, data, key), 'key': key, 'type': type, }))); } function unformat(type, data, key) { var even = [], odd = []; data.split("").forEach((v, idx) => (idx % 2 === 0 ? even.push(v) : odd.push(v))); const dividerStr = ["%7C%2A-%2A%7C", key, "%7C%2A-%2A%7C"].join(""); switch (type) { case "T0": return [odd.join(""), dividerStr, even.join("")].join(""); case "T1": return [odd.reverse().join(""), dividerStr, even.join("")].join(""); case "T2": return [odd.join(""), dividerStr, even.reverse().join("")].join(""); default: return [odd.reverse().join(""), dividerStr, even.reverse().join("")].join(""); } }