546 lines
20 KiB
JavaScript
Raw Normal View History

2025-09-05 18:02:27 +09:00
/**
* =================================================================================
* 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를 호출하도록 방어 코드를 추가하는 것이 좋습니다.
// 현재는 각 페이지에서 직접 호출하므로 이 코드는 참고용입니다.
// 사이드바의 인기글/최신글 목록을 가져옵니다.
2025-08-05 11:24:23 +09:00
if (document.querySelector(".rank_of_view")) {
2025-09-05 18:02:27 +09:00
fetchRankOfViews();
2025-08-05 11:24:23 +09:00
}
if (document.querySelector(".recent_posts")) {
2025-09-05 18:02:27 +09:00
fetchRecentPosts();
2025-08-05 11:24:23 +09:00
}
2025-08-08 17:11:34 +09:00
2025-09-05 18:02:27 +09:00
// 팝업 닫기 버튼 이벤트
$('.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);
});
2025-03-21 17:15:55 +09:00
});
2025-08-05 18:01:15 +09:00
2024-10-07 16:14:03 +09:00
2025-09-05 18:02:27 +09:00
/**
* [핵심] 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;
2024-10-07 16:14:03 +09:00
}
2025-09-05 18:02:27 +09:00
getLocation();
2024-10-07 16:14:03 +09:00
2025-09-05 18:02:27 +09:00
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);
2024-10-07 16:14:03 +09:00
}
2024-12-02 18:32:14 +09:00
2025-09-05 18:02:27 +09:00
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;
2024-12-02 18:32:14 +09:00
}
}
2025-09-05 18:02:27 +09:00
} catch (e) {
console.error("Quill initialization failed:", e);
2024-12-02 18:32:14 +09:00
}
2025-09-05 18:02:27 +09:00
setupControlBox(useEditor ? 'edit' : 'view');
2024-10-07 16:14:03 +09:00
}
2024-10-23 17:06:27 +09:00
2025-09-05 18:02:27 +09:00
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);
2024-10-23 17:06:27 +09:00
} else {
2025-09-05 18:02:27 +09:00
// 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);
}
};
2024-10-23 17:06:27 +09:00
}
}
2025-09-05 18:02:27 +09:00
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');
}
});
2024-12-04 18:01:50 +09:00
}
2024-10-25 18:28:25 +09:00
2025-09-05 18:02:27 +09:00
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);
};
2024-12-04 18:01:50 +09:00
}
2025-09-05 18:02:27 +09:00
function uploadVideo(file) {
const formData = new FormData();
formData.append('video', file);
2024-12-02 18:32:14 +09:00
2025-09-05 18:02:27 +09:00
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);
});
2025-09-01 17:23:40 +09:00
}
2025-09-02 17:32:02 +09:00
2025-09-05 18:02:27 +09:00
function insertToEditor(url) {
const range = quill.getSelection(true);
quill.insertEmbed(range.index, 'image', url);
quill.setSelection(range.index + 1);
2025-09-02 17:32:02 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 에디터 모드('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 = `<span class="tag-title">카테고리: </span><span class="tag-item">${baseData.category || '지정되지 않음'}</span>`;
hashtagBox.innerHTML = '<span class="tag-title">태그: </span>';
if (baseData.tags && baseData.tags.length > 0) {
baseData.tags.split(',').forEach(tag => {
hashtagBox.innerHTML += `<span class="tag-item">#${tag.trim()}</span>`;
});
} else {
hashtagBox.innerHTML += '<span>없음</span>';
}
}
2024-12-04 18:01:50 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 백엔드 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));
}
2025-03-10 17:55:48 +09:00
2025-09-05 18:02:27 +09:00
/**
* 컨텐츠를 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);
}
2025-08-04 16:35:49 +09:00
2025-09-05 18:02:27 +09:00
/**
* 게시물 수정 페이지로 이동합니다.
*/
function loadEditor() {
if (baseData.id) {
location.href = `${getMainPath()}/blog/edit/${baseData.id}`;
}
}
2025-08-04 16:35:49 +09:00
2025-09-05 18:02:27 +09:00
/**
* 작성된 게시물을 서버에 저장합니다.
*/
function save() {
const titleField = document.getElementById('title_field');
if (titleField) {
baseData.title = encodeURIComponent(titleField.value);
}
2025-08-04 16:35:49 +09:00
2025-09-05 18:02:27 +09:00
baseData.content = encodeURIComponent(JSON.stringify(quill.getContents()));
baseData.modifyLat = currentLat;
baseData.modifyLon = currentLon;
2025-08-04 16:35:49 +09:00
2025-09-05 18:02:27 +09:00
const uploadUrl = `${getMainPath()}/blog/post.bjx`;
if (confirm("해당 내용으로 저장하시겠습니까?")) {
post(uploadUrl, serverData.enc, JSON.stringify(baseData), serverData.keyword, function(resultData) {
alert("저장되었습니다.");
});
}
2024-12-02 18:32:14 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 사용자의 현재 위치(위도, 경도) 가져옵니다.
*/
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)}`;
}
});
}
2025-08-05 11:24:23 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 팝업 레이어를 엽니다.
*/
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';
}
2024-10-25 18:28:25 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 팝업 레이어를 닫습니다.
*/
function closePopup() {
const overlay = document.querySelector('.dim_layer');
if(overlay) overlay.style.display = 'none';
document.querySelectorAll('.pop_layer').forEach(p => p.style.display = 'none');
2024-12-05 18:15:20 +09:00
}
2024-10-25 18:28:25 +09:00
2025-09-05 18:02:27 +09:00
/**
* 게시물 상세 보기 페이지로 이동합니다.
*/
function goToViewer(element) {
if (element && element.id) {
location.href = `${getMainPath()}/blog/viewer/${element.id}`;
}
2025-08-05 11:24:23 +09:00
}
2025-09-05 18:02:27 +09:00
// =================================================================================
// [복구] 이하 누락되었던 함수들
// =================================================================================
/**
* 인기글 목록을 가져와 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 += `<li><a href="${getMainPath()}/blog/viewer/${item.id}">${item.title}<br>[${formattedDate}]</a></li>`;
});
2024-12-05 18:15:20 +09:00
}
2025-09-05 18:02:27 +09:00
}).catch(error => console.error('Failed to fetch rank of views:', error));
2024-10-25 18:28:25 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 최신글 목록을 가져와 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 += `<li><a href="${getMainPath()}/blog/viewer/${item.id}">${item.title}<br>[${formattedDate}]</a></li>`;
});
}
}).catch(error => console.error('Failed to fetch recent posts:', error));
2024-10-23 17:06:27 +09:00
}
2025-09-05 18:02:27 +09:00
/**
* 로그인 데이터를 서버에 전송합니다.
*/
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}`);
}
2024-10-23 17:06:27 +09:00
});
}
2025-09-05 18:02:27 +09:00
// --- 페이지 이동(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`); }
2024-10-23 17:06:27 +09:00
2025-09-05 18:02:27 +09:00
/**
* 로그아웃을 처리합니다.
*/
function logout() {
const form = document.createElement('form');
form.method = 'POST';
form.action = `${getMainPath()}/user/logout.bs`;
2024-10-23 17:06:27 +09:00
2025-09-05 18:02:27 +09:00
// 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);
}
2024-10-23 17:06:27 +09:00
2025-09-05 18:02:27 +09:00
document.body.appendChild(form);
form.submit();
2025-03-21 17:15:55 +09:00
}
2025-09-05 18:02:27 +09:00
// =================================================================================
// 서버 통신 및 암호화 관련 유틸리티 함수들 (기존 코드 유지)
// =================================================================================
2025-03-21 17:15:55 +09:00
2025-09-05 18:02:27 +09:00
function getMainPath() {
return location.protocol + "//" + location.hostname + (location.port ? ':' + location.port : '');
2025-03-21 17:15:55 +09:00
}
2025-09-05 18:02:27 +09:00
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,
})));
2025-03-21 17:15:55 +09:00
}
2025-09-05 18:02:27 +09:00
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!'); }
2025-03-21 17:15:55 +09:00
}
2025-09-05 18:02:27 +09:00
};
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,
})));
2025-08-05 16:14:01 +09:00
}
2025-03-21 17:15:55 +09:00
2025-09-05 18:02:27 +09:00
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("");
2025-08-05 16:14:01 +09:00
}
2024-10-23 17:06:27 +09:00
}