2025-09-05 18:02:27 +09:00
|
|
|
/**
|
|
|
|
|
* =================================================================================
|
|
|
|
|
* common.js - 블로그 공통 스크립트 (최종 수정본)
|
|
|
|
|
* - Quill 에디터 초기화 및 제어 (편집/읽기 모드)
|
|
|
|
|
* - 게시물 데이터 관리 (baseData) 및 서버 통신 (save, post)
|
|
|
|
|
* - UI 제어 (팝업, 컨트롤 박스 동적 설정)
|
|
|
|
|
* - 페이지 이동 및 로그인/로그아웃, 유틸리티 함수
|
|
|
|
|
* =================================================================================
|
|
|
|
|
*/
|
|
|
|
|
|
2025-09-08 16:35:09 +09:00
|
|
|
var stagedCategory = 'none';
|
|
|
|
|
var stagedHashtags = []; // 해시태그는 배열로 관리
|
2025-09-08 18:21:57 +09:00
|
|
|
var currentReplyParentId = null;
|
2025-09-05 18:02:27 +09:00
|
|
|
// 전역 변수: Quill 에디터 인스턴스와 게시물 기본 데이터를 저장합니다.
|
|
|
|
|
var quill = null;
|
|
|
|
|
var currentLat = 0.0;
|
|
|
|
|
var currentLon = 0.0;
|
|
|
|
|
var baseData = {
|
|
|
|
|
'id': "",
|
2025-09-08 18:21:57 +09:00
|
|
|
'originId': "",
|
2025-09-05 18:02:27 +09:00
|
|
|
'title': "",
|
|
|
|
|
'content': "",
|
|
|
|
|
'category': "none",
|
|
|
|
|
'tags': "",
|
2025-09-08 18:21:57 +09:00
|
|
|
'writeTime': 0,
|
|
|
|
|
'modifyTime': 0,
|
2025-09-05 18:02:27 +09:00
|
|
|
'firstPostLat': 0.0,
|
|
|
|
|
'firstPostLon': 0.0,
|
2025-09-08 18:21:57 +09:00
|
|
|
'firstAddress': "",
|
2025-09-05 18:02:27 +09:00
|
|
|
'modifyLat': 0.0,
|
|
|
|
|
'modifyLon': 0.0,
|
2025-09-08 18:21:57 +09:00
|
|
|
'modifyAddress': "",
|
|
|
|
|
'writer': "",
|
|
|
|
|
'posting': false,
|
|
|
|
|
'readCount': 0,
|
|
|
|
|
'voteCount': 0,
|
|
|
|
|
'unlikeCount': 0
|
2025-09-05 18:02:27 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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-09-08 16:35:09 +09:00
|
|
|
/* === (대규모 수정) 팝업 입력/적용/취소 버튼 로직 === */
|
2025-09-08 18:21:57 +09:00
|
|
|
/* === (신규 추가) 댓글 등록 버튼 이벤트 리스너 === */
|
|
|
|
|
$('#submit-comment').on('click', function(e) {
|
|
|
|
|
e.preventDefault(); // 기본 버튼 동작 방지
|
|
|
|
|
submitComment(); // 아래에 정의된 새 함수 호출
|
|
|
|
|
});
|
2025-09-08 16:35:09 +09:00
|
|
|
// --- 1. Category Popup Logic ---
|
|
|
|
|
const categoryInput = document.getElementById('category-input');
|
|
|
|
|
const addCategoryBtn = document.getElementById('add-category-btn');
|
|
|
|
|
const applyCategoryBtn = document.getElementById('apply-category-btn'); // (신규) 적용 버튼 선택
|
|
|
|
|
|
|
|
|
|
// "Add" 버튼 (입력창에서 추가) 로직 (수정됨)
|
|
|
|
|
if (addCategoryBtn) {
|
|
|
|
|
addCategoryBtn.addEventListener('click', function() {
|
|
|
|
|
const newCategory = categoryInput.value.trim();
|
|
|
|
|
if (newCategory) {
|
|
|
|
|
stagedCategory = newCategory; // 1. 임시 변수(stagedCategory) 업데이트
|
|
|
|
|
renderStagedCategory(); // 2. 스테이징 UI 새로고침
|
|
|
|
|
categoryInput.value = ''; // 3. 입력창 비우기 (팝업 유지)
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// "Enter" 키 지원 (기존과 동일)
|
|
|
|
|
if (categoryInput) {
|
|
|
|
|
categoryInput.addEventListener('keyup', function(e) {
|
|
|
|
|
if (e.key === 'Enter' || e.keyCode === 13) {
|
|
|
|
|
addCategoryBtn.click();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// (신규) "Apply" 버튼 로직
|
|
|
|
|
if (applyCategoryBtn) {
|
|
|
|
|
applyCategoryBtn.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault(); // A태그 기본 동작(새로고침/이동) 방지
|
|
|
|
|
|
|
|
|
|
// "적용" 시점에만 실제 baseData를 임시 변수 값으로 덮어쓰기
|
|
|
|
|
baseData.category = stagedCategory;
|
|
|
|
|
|
|
|
|
|
updateControlBoxDisplay(); // 본문 에디터 UI 갱신
|
|
|
|
|
closePopup(); // 팝업 닫기
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- 2. Hashtag Popup Logic ---
|
|
|
|
|
const hashtagInput = document.getElementById('hashtag-input');
|
|
|
|
|
const addHashtagBtn = document.getElementById('add-hashtag-btn');
|
|
|
|
|
const applyHashtagBtn = document.getElementById('apply-hashtag-btn'); // (신규) 적용 버튼 선택
|
|
|
|
|
|
|
|
|
|
// "Add" 버튼 (입력창에서 추가) 로직 (수정됨)
|
|
|
|
|
if (addHashtagBtn) {
|
|
|
|
|
addHashtagBtn.addEventListener('click', function() {
|
|
|
|
|
// 1. 임시 배열(stagedHashtags)에 추가 (신규 헬퍼 함수 사용)
|
|
|
|
|
if (addTagToStaged(hashtagInput.value)) {
|
|
|
|
|
renderStagedHashtags(); // 2. 추가 성공 시 스테이징 UI 새로고침
|
|
|
|
|
}
|
|
|
|
|
hashtagInput.value = ''; // 3. 입력창은 항상 비움
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// "Enter" 키 지원 (기존과 동일)
|
|
|
|
|
if (hashtagInput) {
|
|
|
|
|
hashtagInput.addEventListener('keyup', function(e) {
|
|
|
|
|
if (e.key === 'Enter' || e.keyCode === 13) {
|
|
|
|
|
addHashtagBtn.click();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// (신규) "Apply" 버튼 로직
|
|
|
|
|
if (applyHashtagBtn) {
|
|
|
|
|
applyHashtagBtn.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault(); // A태그 기본 동작 방지
|
|
|
|
|
|
|
|
|
|
// "적용" 시점에 임시 배열을 쉼표(,)로 구분된 문자열로 변환하여 실제 baseData에 저장
|
|
|
|
|
baseData.tags = stagedHashtags.join(',');
|
|
|
|
|
|
|
|
|
|
updateControlBoxDisplay(); // 본문 에디터 UI 갱신
|
|
|
|
|
closePopup(); // 팝업 닫기
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/* =============================================================== */
|
|
|
|
|
|
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;
|
2025-09-08 18:21:57 +09:00
|
|
|
|
|
|
|
|
// === [버그 수정] 누락된 메타데이터 복사 로직 추가 ===
|
|
|
|
|
baseData.modifyTime = serverData.modifyTime;
|
|
|
|
|
baseData.firstAddress = serverData.firstAddress;
|
|
|
|
|
baseData.modifyLat = serverData.modifyLat;
|
|
|
|
|
baseData.modifyLon = serverData.modifyLon;
|
|
|
|
|
baseData.modifyAddress = serverData.modifyAddress;
|
|
|
|
|
baseData.writer = serverData.writer;
|
|
|
|
|
baseData.posting = serverData.posting;
|
|
|
|
|
baseData.readCount = serverData.readCount;
|
|
|
|
|
baseData.voteCount = serverData.voteCount;
|
|
|
|
|
baseData.unlikeCount = serverData.unlikeCount;
|
|
|
|
|
// ===============================================
|
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' }],
|
2025-09-08 16:35:09 +09:00
|
|
|
[{ 'indent': '-1'}, { 'indent': '+1' }], [{ 'align': [] }],
|
|
|
|
|
['table-better'], [{ 'direction': 'rtl' }], ['clean'], ['link', 'image', 'video'],
|
2025-09-05 18:02:27 +09:00
|
|
|
],
|
|
|
|
|
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)');
|
2025-09-08 16:35:09 +09:00
|
|
|
|
|
|
|
|
// === 수정된 부분 ===
|
|
|
|
|
// 기존 "innerText" 설정 줄을 삭제하고,
|
|
|
|
|
// 페이지 로드 시 현재 데이터로 UI를 업데이트하는 함수를 호출합니다.
|
|
|
|
|
updateControlBoxDisplay();
|
|
|
|
|
// ==================
|
|
|
|
|
|
|
|
|
|
fetchCategoriesAndHashtags(); // 팝업 목록 채우는 로직은 그대로 실행
|
2025-09-05 18:02:27 +09:00
|
|
|
} else {
|
2025-09-08 16:35:09 +09:00
|
|
|
// (읽기 모드 'else' 블록 수정)
|
2025-09-05 18:02:27 +09:00
|
|
|
categoryBox.removeAttribute('onclick');
|
|
|
|
|
hashtagBox.removeAttribute('onclick');
|
|
|
|
|
categoryBox.classList.remove('btn-example');
|
|
|
|
|
hashtagBox.classList.remove('btn-example');
|
|
|
|
|
|
2025-09-08 16:35:09 +09:00
|
|
|
// [수정] 카테고리를 새 구조(제목 + 래퍼)로 변경
|
|
|
|
|
const categoryContent = `<span class="tag-item">${baseData.category || '지정되지 않음'}</span>`;
|
|
|
|
|
categoryBox.innerHTML = `<span class="tag-title">CATEGORY</span><div class="tag-content-wrapper">${categoryContent}</div>`;
|
2025-09-05 18:02:27 +09:00
|
|
|
|
2025-09-08 16:35:09 +09:00
|
|
|
// [수정] 해시태그를 새 구조(제목 + 래퍼)로 변경
|
|
|
|
|
let hashtagContent = '';
|
2025-09-05 18:02:27 +09:00
|
|
|
if (baseData.tags && baseData.tags.length > 0) {
|
2025-09-08 16:35:09 +09:00
|
|
|
hashtagContent = baseData.tags.split(',').map(tag => {
|
|
|
|
|
return `<span class="tag-item">#${tag.trim()}</span>`;
|
|
|
|
|
}).join(' '); // join으로 하나의 문자열로 만듭니다.
|
2025-09-05 18:02:27 +09:00
|
|
|
} else {
|
2025-09-08 16:35:09 +09:00
|
|
|
hashtagContent = '<span>없음</span>';
|
2025-09-05 18:02:27 +09:00
|
|
|
}
|
2025-09-08 16:35:09 +09:00
|
|
|
hashtagBox.innerHTML = `<span class="tag-title">HASHTAGS</span><div class="tag-content-wrapper">${hashtagContent}</div>`;
|
2025-09-05 18:02:27 +09:00
|
|
|
}
|
2024-12-04 18:01:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
/**
|
|
|
|
|
* 백엔드 API를 호출하여 카테고리와 해시태그 목록을 가져와 팝업을 채웁니다.
|
|
|
|
|
*/
|
2025-09-08 16:35:09 +09:00
|
|
|
/**
|
|
|
|
|
* 백엔드 API를 호출하여 카테고리와 해시태그 목록을 가져와 팝업을 채웁니다.
|
|
|
|
|
* (수정됨: 클릭 시 baseData 대신 Staging 변수를 업데이트하도록 변경)
|
|
|
|
|
*/
|
2025-09-05 18:02:27 +09:00
|
|
|
function fetchCategoriesAndHashtags() {
|
2025-09-08 16:35:09 +09:00
|
|
|
// Fetch Categories
|
2025-09-05 18:02:27 +09:00
|
|
|
fetch(`${getMainPath()}/blog/categories.bjx`).then(res => res.json()).then(data => {
|
|
|
|
|
if (data.resultCode === 0 && data.tags) {
|
|
|
|
|
const list = document.querySelector('#category-list');
|
2025-09-08 16:35:09 +09:00
|
|
|
if (list) {
|
2025-09-05 18:02:27 +09:00
|
|
|
list.innerHTML = '';
|
|
|
|
|
data.tags.forEach(tag => {
|
|
|
|
|
const el = document.createElement('span');
|
|
|
|
|
el.className = 'tag-item';
|
|
|
|
|
el.innerText = tag;
|
2025-09-08 16:35:09 +09:00
|
|
|
|
|
|
|
|
// (로직 변경) 클릭 시
|
|
|
|
|
el.onclick = function() {
|
|
|
|
|
stagedCategory = tag; // 1. 임시 변수(stagedCategory) 업데이트
|
|
|
|
|
renderStagedCategory(); // 2. 스테이징 UI만 새로고침 (팝업 안 닫음)
|
|
|
|
|
};
|
2025-09-05 18:02:27 +09:00
|
|
|
list.appendChild(el);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(err => console.error('Error fetching categories:', err));
|
|
|
|
|
|
2025-09-08 16:35:09 +09:00
|
|
|
// Fetch Hashtags
|
2025-09-05 18:02:27 +09:00
|
|
|
fetch(`${getMainPath()}/blog/hashtags.bjx`).then(res => res.json()).then(data => {
|
|
|
|
|
if (data.resultCode === 0 && data.tags) {
|
|
|
|
|
const list = document.querySelector('#hashtag-list');
|
2025-09-08 16:35:09 +09:00
|
|
|
if (list) {
|
2025-09-05 18:02:27 +09:00
|
|
|
list.innerHTML = '';
|
|
|
|
|
data.tags.forEach(tag => {
|
2025-09-08 16:35:09 +09:00
|
|
|
const rawTag = tag;
|
2025-09-05 18:02:27 +09:00
|
|
|
const el = document.createElement('span');
|
|
|
|
|
el.className = 'tag-item';
|
2025-09-08 16:35:09 +09:00
|
|
|
el.innerText = `#${rawTag}`;
|
|
|
|
|
|
|
|
|
|
// (로직 변경) 클릭 시
|
|
|
|
|
el.onclick = function() {
|
|
|
|
|
// 1. 임시 배열(stagedHashtags)에 추가 (중복 방지 헬퍼 사용)
|
|
|
|
|
if (addTagToStaged(rawTag)) {
|
|
|
|
|
// 2. 추가 성공 시에만 스테이징 UI 새로고침
|
|
|
|
|
renderStagedHashtags();
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-05 18:02:27 +09:00
|
|
|
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');
|
2025-09-08 16:35:09 +09:00
|
|
|
|
|
|
|
|
// --- (수정/신규 로직) ---
|
|
|
|
|
// 1. baseData의 복사본을 만들어 전송용 임시 객체(dataToSend)를 생성합니다.
|
|
|
|
|
// (원본 baseData를 직접 수정하면 팝업 UI가 인코딩된 문자로 깨집니다)
|
|
|
|
|
let dataToSend = JSON.parse(JSON.stringify(baseData));
|
|
|
|
|
|
|
|
|
|
// 2. dataToSend 객체의 모든 텍스트 필드를 encodeURIComponent로 인코딩합니다.
|
2025-09-05 18:02:27 +09:00
|
|
|
if (titleField) {
|
2025-09-08 16:35:09 +09:00
|
|
|
dataToSend.title = encodeURIComponent(titleField.value);
|
|
|
|
|
} else {
|
|
|
|
|
dataToSend.title = encodeURIComponent(dataToSend.title || '');
|
2025-09-05 18:02:27 +09:00
|
|
|
}
|
2025-08-04 16:35:49 +09:00
|
|
|
|
2025-09-08 16:35:09 +09:00
|
|
|
dataToSend.content = encodeURIComponent(JSON.stringify(quill.getContents()));
|
|
|
|
|
// (누락되었던 필드 추가)
|
|
|
|
|
dataToSend.category = encodeURIComponent(dataToSend.category || 'none');
|
|
|
|
|
dataToSend.tags = encodeURIComponent(dataToSend.tags || '');
|
|
|
|
|
|
|
|
|
|
// 3. 좌표 데이터를 임시 객체에 업데이트합니다.
|
|
|
|
|
dataToSend.modifyLat = currentLat;
|
|
|
|
|
dataToSend.modifyLon = currentLon;
|
|
|
|
|
|
|
|
|
|
// (신규 게시물일 경우 원본 위치 좌표 설정)
|
|
|
|
|
if (dataToSend.firstPostLat === 0.0 || dataToSend.firstPostLat === null) {
|
|
|
|
|
dataToSend.firstPostLat = currentLat;
|
|
|
|
|
}
|
|
|
|
|
if (dataToSend.firstPostLon === 0.0 || dataToSend.firstPostLon === null) {
|
|
|
|
|
dataToSend.firstPostLon = 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("해당 내용으로 저장하시겠습니까?")) {
|
2025-09-08 16:35:09 +09:00
|
|
|
console.log("Data being sent to server:", dataToSend);
|
|
|
|
|
console.log("JSON string being sent:", JSON.stringify(dataToSend));
|
|
|
|
|
post(uploadUrl, serverData.enc, JSON.stringify(dataToSend), serverData.keyword, function(resultData) {
|
|
|
|
|
// --- (수정된 콜백 로직) ---
|
|
|
|
|
try {
|
|
|
|
|
// 1. 서버로부터 받은 JSON 문자열을 객체로 파싱합니다.
|
|
|
|
|
const response = JSON.parse(resultData);
|
|
|
|
|
|
|
|
|
|
// 2. 서버 응답이 성공(resultCode === 0)이고,
|
|
|
|
|
// 서버가 postId를 (예: response.data.postId) 보내줬는지 확인합니다.
|
|
|
|
|
// (참고: 'response.data.postId'는 서버 응답 구조에 따라 변경해야 할 수 있습니다.)
|
|
|
|
|
if (response.resultCode === 0 && response.data && response.data.postId) {
|
|
|
|
|
|
|
|
|
|
// 3. 알림 후, 응답받은 ID를 사용해 뷰어 페이지로 리디렉션합니다.
|
|
|
|
|
alert("저장되었습니다. 게시물 보기 페이지로 이동합니다.");
|
|
|
|
|
location.href = getMainPath() + "/blog/viewer/" + response.data.postId;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// 저장은 성공했으나 ID를 받지 못한 경우 (또는 서버가 다른 에러 코드를 보낸 경우)
|
|
|
|
|
alert("저장되었으나 페이지 이동에 실패했습니다: " + (response.resultMsg || "Unknown error"));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// JSON 파싱 실패 등 예외 처리
|
|
|
|
|
console.error("Failed to parse save response:", e, resultData);
|
|
|
|
|
alert("저장에 성공했으나 서버 응답을 처리할 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
// --- (여기까지 수정된 로직) ---
|
|
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
});
|
|
|
|
|
}
|
2024-12-02 18:32:14 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
/**
|
|
|
|
|
* 사용자의 현재 위치(위도, 경도)를 가져옵니다.
|
|
|
|
|
*/
|
|
|
|
|
function getLocation() {
|
2025-09-08 16:35:09 +09:00
|
|
|
if (baseData.firstPostLat !== 0.0 || baseData.firstPostLon !== 0.0) {
|
|
|
|
|
try {
|
|
|
|
|
var requestOptions = {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
};
|
|
|
|
|
fetch("https://api.geoapify.com/v1/geocode/reverse?lat="+baseData.firstPostLat+"&lon="+baseData.firstPostLon+"&apiKey=2b37a75bb0754086b5a1c4a7c3173ee8", requestOptions)
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
.then(function(result) {
|
|
|
|
|
const locationField = document.getElementById('location_field');
|
|
|
|
|
try {
|
|
|
|
|
var inh = `<span class="tag-title">LOCATION</span>`;
|
|
|
|
|
inh = inh + `<div class="tag-content-wrapper">`;
|
|
|
|
|
try{
|
|
|
|
|
inh = inh + `<div class="tag-item">${result.features[0].properties.formatted}</div>`;
|
|
|
|
|
}catch(err) {}
|
|
|
|
|
inh = inh + `<div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div>`;
|
|
|
|
|
inh = inh + `<div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
|
|
|
|
locationField.innerHTML = inh;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
locationField.innerHTML = `<span class="tag-title">LOCATION</span>` +
|
|
|
|
|
`<div class="tag-content-wrapper"><div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div><div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(error => console.log('error', error));
|
|
|
|
|
}catch (e) { }
|
|
|
|
|
} else if (navigator.geolocation) {
|
2025-09-05 18:02:27 +09:00
|
|
|
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) {
|
2025-09-08 16:35:09 +09:00
|
|
|
// [수정] 제목과 내용 래퍼 구조로 변경
|
|
|
|
|
locationField.innerHTML = `<span class="tag-title">LOCATION</span>` +
|
|
|
|
|
`<div class="tag-content-wrapper"><div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div><div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
2025-09-05 18:02:27 +09:00
|
|
|
}
|
|
|
|
|
});
|
2025-09-08 16:35:09 +09:00
|
|
|
} else {
|
|
|
|
|
const locationField = document.getElementById('location_field');
|
|
|
|
|
if (locationField) {
|
|
|
|
|
// [수정] 제목과 내용 래퍼 구조로 변경
|
|
|
|
|
locationField.innerHTML = `<span class="tag-title">LOCATION</span>` +
|
|
|
|
|
`<div class="tag-content-wrapper"><div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div><div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
|
|
|
|
}
|
2025-09-05 18:02:27 +09:00
|
|
|
}
|
2025-08-05 11:24:23 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
/**
|
|
|
|
|
* 팝업 레이어를 엽니다.
|
|
|
|
|
*/
|
|
|
|
|
function openPopup(element) {
|
2025-09-08 16:35:09 +09:00
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
const targetId = element.getAttribute('to');
|
|
|
|
|
const popup = document.querySelector(targetId);
|
|
|
|
|
const overlay = document.querySelector('.dim_layer');
|
2025-09-08 16:35:09 +09:00
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
if (popup && overlay) {
|
2025-09-08 16:35:09 +09:00
|
|
|
|
|
|
|
|
// === (신규) Staging 변수 초기화 로직 ===
|
|
|
|
|
if (targetId === '#popLayer1') { // 카테고리 팝업
|
|
|
|
|
// 1. 실제 데이터(baseData)에서 임시 변수(stagedCategory)로 값을 복사
|
|
|
|
|
stagedCategory = baseData.category || 'none';
|
|
|
|
|
// 2. 임시 변수 기준으로 스테이징 UI 렌더링
|
|
|
|
|
renderStagedCategory();
|
|
|
|
|
}
|
|
|
|
|
else if (targetId === '#popLayer2') { // 해시태그 팝업
|
|
|
|
|
// 1. 실제 데이터(baseData)에서 임시 배열(stagedHashtags)로 값을 복사
|
|
|
|
|
// (문자열을 배열로 변환하고, 빈 문자열 필터링)
|
|
|
|
|
stagedHashtags = baseData.tags ? baseData.tags.split(',').filter(t => t.trim() !== '') : [];
|
|
|
|
|
// 2. 임시 배열 기준으로 스테이징 UI 렌더링
|
|
|
|
|
renderStagedHashtags();
|
|
|
|
|
}
|
|
|
|
|
// ===================================
|
|
|
|
|
|
2025-09-05 18:02:27 +09:00
|
|
|
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
|
2025-09-08 16:35:09 +09:00
|
|
|
function gotoWhere() { document.location.replace(`${getMainPath()}/bums/where.bs`); }
|
2025-09-05 18:02:27 +09:00
|
|
|
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");
|
2025-09-08 18:21:57 +09:00
|
|
|
|
|
|
|
|
const csrfMeta = document.querySelector('meta[name="_csrf"]');
|
|
|
|
|
if (csrfMeta) {
|
|
|
|
|
const csrfToken = csrfMeta.getAttribute('content');
|
|
|
|
|
if (csrfToken) {
|
|
|
|
|
// Spring Security는 이 헤더를 확인합니다.
|
|
|
|
|
httpRequest.setRequestHeader("X-CSRF-TOKEN", csrfToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 1. Create the complete JSON string payload
|
|
|
|
|
const jsonPayloadString = JSON.stringify({
|
2025-09-05 18:02:27 +09:00
|
|
|
'data': unformat(type, data, key), 'key': key, 'type': type,
|
2025-09-08 18:21:57 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. [FIX] Convert the Unicode JSON string (which may contain Korean) into
|
|
|
|
|
// a UTF-8 byte stream that btoa() can safely handle.
|
|
|
|
|
const utf8SafePayload = unescape(encodeURIComponent(jsonPayloadString));
|
|
|
|
|
|
|
|
|
|
// 3. Send the result after running btoa() on the safe string.
|
|
|
|
|
httpRequest.send(btoa(utf8SafePayload));
|
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
|
|
|
}
|
2025-09-08 16:35:09 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* (신규 추가)
|
|
|
|
|
* 중복을 방지하며 baseData.tags (문자열)에 새 태그를 안전하게 추가합니다.
|
|
|
|
|
*/
|
|
|
|
|
function addTagToData(newTag) {
|
|
|
|
|
if (!newTag || newTag.trim() === '') return; // 빈 태그 방지
|
|
|
|
|
|
|
|
|
|
// 현재 태그 문자열을 배열로 변환 (태그가 없으면 빈 배열)
|
|
|
|
|
let tags = baseData.tags ? baseData.tags.split(',') : [];
|
|
|
|
|
|
|
|
|
|
// 새 태그가 이미 존재하는지 확인 (공백 제거 및 대소문자 무시)
|
|
|
|
|
const tagExists = tags.some(t => t.trim().toLowerCase() === newTag.trim().toLowerCase());
|
|
|
|
|
|
|
|
|
|
if (!tagExists) {
|
|
|
|
|
tags.push(newTag.trim()); // 새 태그 추가
|
|
|
|
|
baseData.tags = tags.join(','); // 다시 쉼표로 구분된 문자열로 저장
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* (신규 추가)
|
|
|
|
|
* 편집기 컨트롤 박스의 텍스트를 현재 baseData 기준으로 새로 고칩니다.
|
|
|
|
|
*/
|
|
|
|
|
function updateControlBoxDisplay() {
|
|
|
|
|
const categoryBox = document.querySelector('.controlbox-category');
|
|
|
|
|
const hashtagBox = document.querySelector('.controlbox-hashtag');
|
|
|
|
|
|
|
|
|
|
if (categoryBox) {
|
|
|
|
|
// [수정] 편집 모드도 읽기 모드와 동일한 HTML 구조(제목 + 래퍼)로 생성
|
|
|
|
|
const categoryContent = (baseData.category && baseData.category !== 'none')
|
|
|
|
|
? `<span class="tag-item">${baseData.category}</span>`
|
|
|
|
|
: '<i>카테고리 설정</i>';
|
|
|
|
|
categoryBox.innerHTML = `<span class="tag-title">CATEGORY</span><div class="tag-content-wrapper">${categoryContent}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hashtagBox) {
|
|
|
|
|
// [수정] 편집 모드도 읽기 모드와 동일한 HTML 구조(제목 + 래퍼)로 생성
|
|
|
|
|
let hashtagContent = '';
|
|
|
|
|
if (baseData.tags && baseData.tags.length > 0) {
|
|
|
|
|
hashtagContent = baseData.tags.split(',')
|
|
|
|
|
.map(t => `<span class="tag-item">#${t.trim()}</span>`)
|
|
|
|
|
.join(' '); // 각 태그를 span으로 감싸고 공백으로 연결
|
|
|
|
|
} else {
|
|
|
|
|
hashtagContent = '<i>해시태그 편집</i>';
|
|
|
|
|
}
|
|
|
|
|
hashtagBox.innerHTML = `<span class="tag-title">HASHTAGS</span><div class="tag-content-wrapper">${hashtagContent}</div>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* === (신규 추가) POPUP STAGING 헬퍼 함수들 === */
|
|
|
|
|
|
|
|
|
|
/** 1. Staging Category 렌더링: 선택된 카테고리(임시 변수)를 팝업 UI에 표시 */
|
|
|
|
|
function renderStagedCategory() {
|
|
|
|
|
const area = document.getElementById('selected-category-area');
|
|
|
|
|
if (area) {
|
|
|
|
|
if (stagedCategory && stagedCategory !== 'none') {
|
|
|
|
|
// 선택된 아이템에 삭제(X) 버튼을 포함하여 렌더링
|
|
|
|
|
area.innerHTML = `<span class="tag-item">${stagedCategory}
|
|
|
|
|
<span class="remove-tag" onclick="removeStagedCategory()">X</span>
|
|
|
|
|
</span>`;
|
|
|
|
|
} else {
|
|
|
|
|
area.innerHTML = '<i>No category selected.</i>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 2. Staging Hashtags 렌더링: 선택된 해시태그 목록(임시 배열)을 팝업 UI에 표시 */
|
|
|
|
|
function renderStagedHashtags() {
|
|
|
|
|
const area = document.getElementById('selected-hashtags-area');
|
|
|
|
|
if (area) {
|
|
|
|
|
area.innerHTML = ''; // 영역 초기화
|
|
|
|
|
if (stagedHashtags.length > 0) {
|
|
|
|
|
stagedHashtags.forEach((tag, index) => {
|
|
|
|
|
// 각 아이템에 삭제(X) 버튼과 올바른 index를 전달하는 onclick 이벤트 추가
|
|
|
|
|
area.innerHTML += `<span class="tag-item">#${tag}
|
|
|
|
|
<span class="remove-tag" onclick="removeStagedHashtag(${index})">X</span>
|
|
|
|
|
</span>`;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
area.innerHTML = '<i>No tags selected.</i>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 3. Staged Category 삭제: (X) 버튼 클릭 시 호출 */
|
|
|
|
|
function removeStagedCategory() {
|
|
|
|
|
stagedCategory = 'none';
|
|
|
|
|
renderStagedCategory(); // UI 새로고침
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 4. Staged Hashtag 삭제: (X) 버튼 클릭 시 호출 */
|
|
|
|
|
function removeStagedHashtag(index) {
|
|
|
|
|
stagedHashtags.splice(index, 1); // 배열에서 해당 인덱스의 아이템 1개 제거
|
|
|
|
|
renderStagedHashtags(); // UI 새로고침
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 5. Staged Hashtag 추가 (중복 방지 헬퍼): 임시 배열에 태그 추가 */
|
|
|
|
|
function addTagToStaged(newTag) {
|
|
|
|
|
if (!newTag || newTag.trim() === '') return false; // 빈 값 방지
|
|
|
|
|
|
|
|
|
|
const tagToAdd = newTag.trim().replace(/#/g, ''); // # 제거 및 공백 제거
|
|
|
|
|
// 임시 배열에 이미 존재하는지 확인 (대소문자 무시)
|
|
|
|
|
const tagExists = stagedHashtags.some(t => t.toLowerCase() === tagToAdd.toLowerCase());
|
|
|
|
|
|
|
|
|
|
if (!tagExists) {
|
|
|
|
|
stagedHashtags.push(tagToAdd); // 임시 배열에 추가
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false; // 중복이면 false 반환
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleVote(buttonElement, voteType) {
|
|
|
|
|
// 1. 가장 가까운 .vote-controls 컨테이너를 찾음
|
|
|
|
|
const controls = buttonElement.closest('.vote-controls');
|
|
|
|
|
|
|
|
|
|
// 2. 컨테이너의 data-post-id 속성에서 postId를 가져옴
|
|
|
|
|
const postId = controls.dataset.postId; // (data-post-id="...") 값을 읽음
|
|
|
|
|
|
|
|
|
|
// 3. 모든 버튼 비활성화 (중복 클릭 방지)
|
|
|
|
|
controls.querySelectorAll('button').forEach(btn => btn.disabled = true);
|
|
|
|
|
|
|
|
|
|
// 4. 요청할 URL 생성
|
|
|
|
|
let url = `${getMainPath()}/blog/post/${postId}/${voteType === 'like' ? 'like' : 'unlike'}.bjx`;
|
|
|
|
|
|
|
|
|
|
// 5. CSRF 토큰 및 헤더 준비 (기본 헤더)
|
|
|
|
|
let headers = {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// [수정] CSRF 메타 태그가 존재하는지 (즉, 사용자가 로그인했는지) 확인
|
|
|
|
|
const csrfMeta = document.querySelector('meta[name="_csrf"]');
|
|
|
|
|
if (csrfMeta) {
|
|
|
|
|
const csrfToken = csrfMeta.getAttribute('content');
|
|
|
|
|
if (csrfToken) {
|
|
|
|
|
// 토큰이 존재할 경우에만 헤더에 추가
|
|
|
|
|
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6. Fetch API를 사용하여 POST 요청 전송
|
|
|
|
|
// (익명 사용자는 CSRF 헤더 없이 요청하고, 인증 사용자는 헤더와 함께 요청)
|
|
|
|
|
fetch(url, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: headers
|
|
|
|
|
})
|
|
|
|
|
.then(response => {
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Network response was not ok');
|
|
|
|
|
}
|
|
|
|
|
return response.json();
|
|
|
|
|
})
|
|
|
|
|
.then(data => {
|
|
|
|
|
// 7. 성공 시: UI 업데이트
|
|
|
|
|
const likeSpan = controls.querySelector('.like-count');
|
|
|
|
|
const unlikeSpan = controls.querySelector('.unlike-count');
|
|
|
|
|
|
|
|
|
|
if (likeSpan) likeSpan.innerText = data.voteCount;
|
|
|
|
|
if (unlikeSpan) unlikeSpan.innerText = data.unlikeCount;
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
// 8. 실패 시: 버튼 다시 활성화
|
|
|
|
|
console.error('Error handling vote:', error);
|
|
|
|
|
alert('투표 중 오류가 발생했습니다. 나중에 다시 시도해주세요.');
|
|
|
|
|
controls.querySelectorAll('button').forEach(btn => btn.disabled = false);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-08 18:21:57 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* [수정됨] 댓글 등록 처리 함수 (대댓글 지원)
|
|
|
|
|
*/
|
|
|
|
|
function submitComment() {
|
|
|
|
|
|
|
|
|
|
const commentInput = document.getElementById('comment-input');
|
|
|
|
|
if (!commentInput) return;
|
|
|
|
|
|
|
|
|
|
const content = commentInput.value.trim();
|
|
|
|
|
|
|
|
|
|
if (content.length === 0) {
|
|
|
|
|
alert('댓글 내용을 입력하세요.');
|
|
|
|
|
commentInput.focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// [수정] parentId를 하드코딩(null)하는 대신 전역 변수에서 읽어옴
|
|
|
|
|
const commentData = {
|
|
|
|
|
content: content,
|
|
|
|
|
writer: null,
|
|
|
|
|
parentId: currentReplyParentId, // currentReplyParentId 값 (null 또는 댓글ID) 사용
|
|
|
|
|
postId: null,
|
|
|
|
|
id: null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const postId = serverData.id;
|
|
|
|
|
if (!postId) {
|
|
|
|
|
alert("게시물 ID를 찾을 수 없어 댓글을 등록할 수 없습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uploadUrl = `${getMainPath()}/blog/posts/${postId}/comments.bjx`;
|
|
|
|
|
const encType = serverData.enc;
|
|
|
|
|
const keyword = serverData.keyword;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
post(uploadUrl, encType, JSON.stringify(commentData), keyword, function(resultData) {
|
|
|
|
|
try {
|
|
|
|
|
const response = JSON.parse(resultData);
|
|
|
|
|
|
|
|
|
|
if (response.resultCode === 0) {
|
|
|
|
|
alert('댓글이 성공적으로 등록되었습니다.');
|
|
|
|
|
commentInput.value = ''; // 입력창 초기화
|
|
|
|
|
|
|
|
|
|
cancelReply(); // [신규 추가] 답글 상태 초기화
|
|
|
|
|
|
|
|
|
|
fetchComments(postId); // 목록 새로고침
|
|
|
|
|
} else {
|
|
|
|
|
alert('댓글 등록 실패: ' + (response.resultMsg || '알 수 없는 오류'));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to parse comment submission response:', e, resultData);
|
|
|
|
|
alert('댓글 등록 후 서버 응답을 처리하는 중 오류가 발생했습니다.');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error during comment submission:', err);
|
|
|
|
|
alert('댓글 전송 중 예외가 발생했습니다.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchComments(postId) {
|
|
|
|
|
if (!postId) return;
|
|
|
|
|
|
|
|
|
|
const commentsListContainer = document.getElementById('comments-list');
|
|
|
|
|
if (!commentsListContainer) return;
|
|
|
|
|
|
|
|
|
|
commentsListContainer.innerHTML = '<p style="text-align: center; color: #888;">댓글 목록을 불러오는 중...</p>';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. 최상위 댓글 목록 (ParentId = null)을 먼저 가져옵니다.
|
|
|
|
|
const parentResponse = await fetch(`${getMainPath()}/blog/posts/${postId}/comments.bjx`);
|
|
|
|
|
const parentData = await parentResponse.json();
|
|
|
|
|
|
|
|
|
|
if (parentData.resultCode !== 0 || !parentData.comments) {
|
|
|
|
|
throw new Error(parentData.resultMsg || 'Failed to load parent comments');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parentData.comments.length === 0) {
|
|
|
|
|
commentsListContainer.innerHTML = '<p style="text-align: center; color: #888;">아직 댓글이 없습니다. 첫 댓글을 작성해보세요.</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 컨테이너 비우기
|
|
|
|
|
commentsListContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// 2. 각 최상위 댓글을 순회합니다 (for...of 루프 사용 필수)
|
|
|
|
|
for (const parentComment of parentData.comments) {
|
|
|
|
|
|
|
|
|
|
// 2-1. 최상위 댓글 HTML 생성 및 DOM에 추가
|
|
|
|
|
const parentElement = document.createElement('article');
|
|
|
|
|
parentElement.className = 'comment-item';
|
|
|
|
|
parentElement.id = `comment-${parentComment.id}`;
|
|
|
|
|
parentElement.innerHTML = createCommentHTML(parentComment); // 헬퍼 함수 사용
|
|
|
|
|
commentsListContainer.appendChild(parentElement);
|
|
|
|
|
|
|
|
|
|
// 2-2. 해당 댓글의 "답글 API"를 호출합니다.
|
|
|
|
|
const replyResponse = await fetch(`${getMainPath()}/blog/comments/${parentComment.id}/replies.bjx`);
|
|
|
|
|
const replyData = await replyResponse.json();
|
|
|
|
|
|
|
|
|
|
if (replyData.resultCode === 0 && replyData.comments && replyData.comments.length > 0) {
|
|
|
|
|
// 2-3. 답글 컨테이너(div) 생성
|
|
|
|
|
const replyListContainer = document.createElement('div');
|
|
|
|
|
replyListContainer.className = 'reply-list'; // CSS 들여쓰기 적용
|
|
|
|
|
|
|
|
|
|
// 2-4. 모든 답글(대댓글)을 순회하며 HTML 추가
|
|
|
|
|
replyData.comments.forEach(childComment => {
|
|
|
|
|
replyListContainer.innerHTML += createCommentHTML(childComment, true); // 헬퍼 함수 사용 (isReply=true)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2-5. 답글 컨테이너를 부모 댓글(parentElement)의 자식으로 삽입
|
|
|
|
|
parentElement.appendChild(replyListContainer);
|
|
|
|
|
}
|
|
|
|
|
} // end for loop
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to fetch comments hierarchicaly:', err);
|
|
|
|
|
commentsListContainer.innerHTML = '<p>댓글 로딩 중 오류가 발생했습니다.</p>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* [신규 추가] 댓글 객체를 받아 HTML 문자열을 생성하는 헬퍼 함수
|
|
|
|
|
* @param {object} comment - 댓글 객체
|
|
|
|
|
* @param {boolean} isReply - 대댓글 여부 (대댓글에는 "답글달기" 버튼 숨김. 원한다면 true로 변경)
|
|
|
|
|
* @returns {string} - 완성된 HTML 문자열
|
|
|
|
|
*/
|
|
|
|
|
function createCommentHTML(comment, isReply = false) {
|
|
|
|
|
const writerName = comment.writer || 'Anonymous';
|
|
|
|
|
const date = new Date(comment.writeTime);
|
|
|
|
|
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
|
|
|
|
|
|
|
|
// JS에서 XSS를 방지하기 위해 특수문자를 HTML 엔티티로 치환 (간단 버전)
|
|
|
|
|
const safeContent = String(comment.content).replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>");
|
|
|
|
|
const writerNameForReply = String(writerName).replace(/'/g, "\\'"); // ' 문자가 JS를 깨트리는 것을 방지
|
|
|
|
|
|
|
|
|
|
// 참고: 현재 로직은 대댓글의 대댓글(3단계)은 지원하지 않습니다. (isReply = true이면 답글 버튼 생성 안 함)
|
|
|
|
|
// 3단계 이상을 지원하려면 isReply 체크를 제거하고, 답글 API가 대댓글도 정상적으로 가져오는지 확인해야 합니다.
|
|
|
|
|
const replyButtonHTML = !isReply
|
|
|
|
|
? `<button class="btn-reply" onclick="setReplyTarget('${comment.id}', '${writerNameForReply}')">답글</button>`
|
|
|
|
|
: ''; // 대댓글에는 "답글" 버튼 표시 안 함
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="comment-header">
|
|
|
|
|
<div class="comment-author-info">
|
|
|
|
|
<strong>${writerName}</strong>
|
|
|
|
|
<span class="comment-date">${formattedDate}</span>
|
|
|
|
|
</div>
|
|
|
|
|
${replyButtonHTML}
|
|
|
|
|
</div>
|
|
|
|
|
<p style="padding: 5px 0 15px 5px; min-height: 2em;">${safeContent}</p>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* [신규 추가] "답글 달기" 버튼 클릭 시 호출되는 헬퍼 함수
|
|
|
|
|
* @param {string} commentId - 부모가 될 댓글의 ID
|
|
|
|
|
* @param {string} writerName - 부모 댓글 작성자명
|
|
|
|
|
*/
|
|
|
|
|
function setReplyTarget(commentId, writerName) {
|
|
|
|
|
currentReplyParentId = commentId; // 전역 변수(상태) 설정
|
|
|
|
|
|
|
|
|
|
// UI 업데이트
|
|
|
|
|
const statusBar = document.getElementById('reply-status-bar');
|
|
|
|
|
const statusText = document.getElementById('reply-status-text');
|
|
|
|
|
const commentInput = document.getElementById('comment-input');
|
|
|
|
|
|
|
|
|
|
if (statusBar && statusText) {
|
|
|
|
|
statusText.innerText = `@${writerName} 님에게 답글 다는 중...`;
|
|
|
|
|
statusBar.style.display = 'flex'; // 숨겨둔 상태바 표시
|
|
|
|
|
}
|
|
|
|
|
commentInput.focus(); // 입력창으로 포커스 이동
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* [신규 추가] 답글 달기 "취소" 시 호출되는 헬퍼 함수
|
|
|
|
|
*/
|
|
|
|
|
function cancelReply() {
|
|
|
|
|
currentReplyParentId = null; // 상태 초기화
|
|
|
|
|
const statusBar = document.getElementById('reply-status-bar');
|
|
|
|
|
if (statusBar) {
|
|
|
|
|
statusBar.style.display = 'none'; // 상태바 숨기기
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 16:35:09 +09:00
|
|
|
/* ============================================= */
|