182 lines
6.5 KiB
JavaScript
182 lines
6.5 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const section = document.querySelector('.djb-comment-section');
|
|
if (!section) return;
|
|
|
|
const inquiryId = section.getAttribute('data-inquiry-id');
|
|
const closed = section.getAttribute('data-closed') === 'true';
|
|
const listEl = document.getElementById('djbCommentList');
|
|
const emptyEl = document.getElementById('djbCommentEmpty');
|
|
const countEl = document.getElementById('djbCommentCount');
|
|
const formEl = document.getElementById('djbCommentForm');
|
|
const inputEl = document.getElementById('djbCommentInput');
|
|
const counterEl = document.getElementById('djbCommentCharCounter');
|
|
|
|
function getCsrfToken() {
|
|
// 세션 기반 CSRF: 쿠키 대신 <meta name="_csrf">에서 토큰을 읽는다.
|
|
const meta = document.querySelector('meta[name="_csrf"]');
|
|
return meta ? meta.getAttribute('content') : '';
|
|
}
|
|
|
|
function fetchJson(url, options) {
|
|
options = options || {};
|
|
const headers = Object.assign({}, options.headers || {});
|
|
const method = (options.method || 'GET').toUpperCase();
|
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
headers['X-XSRF-TOKEN'] = getCsrfToken();
|
|
}
|
|
if (options.body && !headers['Content-Type']) {
|
|
headers['Content-Type'] = 'application/json;charset=UTF-8';
|
|
}
|
|
return fetch(url, Object.assign({ credentials: 'same-origin' }, options, { headers: headers }));
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (text == null) return '';
|
|
return String(text)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) return '';
|
|
try {
|
|
var d;
|
|
if (Array.isArray(value)) {
|
|
// Jackson LocalDateTime 직렬화(timestamp 배열): [year, month(1-base), day, hour, minute, second, nano]
|
|
var y = value[0], mo = value[1] || 1, da = value[2] || 1;
|
|
var h = value[3] || 0, mi = value[4] || 0, s = value[5] || 0;
|
|
d = new Date(y, mo - 1, da, h, mi, s);
|
|
} else {
|
|
d = new Date(value);
|
|
}
|
|
if (isNaN(d.getTime())) return '';
|
|
const pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
|
return d.getFullYear() + '.' + pad(d.getMonth() + 1) + '.' + pad(d.getDate())
|
|
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function render(comments) {
|
|
listEl.innerHTML = '';
|
|
if (!comments || comments.length === 0) {
|
|
const li = document.createElement('li');
|
|
li.className = 'djb-comment-empty';
|
|
li.textContent = '아직 등록된 댓글이 없습니다.';
|
|
listEl.appendChild(li);
|
|
return;
|
|
}
|
|
comments.forEach(function (c) {
|
|
const li = document.createElement('li');
|
|
li.className = 'djb-comment-item' + (c.adminYn === 'Y' ? ' djb-comment-item--admin' : '');
|
|
li.setAttribute('data-comment-id', c.id);
|
|
li.innerHTML = ''
|
|
+ '<div class="djb-comment-meta">'
|
|
+ ' <div class="djb-comment-meta-left">'
|
|
+ ' <span class="djb-comment-writer">' + escapeHtml(c.writerName || '(알 수 없음)') + '</span>'
|
|
+ (c.adminYn === 'Y' ? ' <span class="djb-comment-admin-badge">관리자</span>' : '')
|
|
+ ' </div>'
|
|
+ ' <div class="djb-comment-meta-right">'
|
|
+ ' <span class="djb-comment-date">' + escapeHtml(formatDate(c.createdDate)) + '</span>'
|
|
+ (c.deletable ? ' <button type="button" class="djb-comment-delete-btn" data-id="' + escapeHtml(c.id) + '">삭제</button>' : '')
|
|
+ ' </div>'
|
|
+ '</div>'
|
|
+ '<div class="djb-comment-body">' + escapeHtml(c.content) + '</div>';
|
|
listEl.appendChild(li);
|
|
});
|
|
}
|
|
|
|
function load() {
|
|
fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments')
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error('댓글 조회에 실패했습니다.');
|
|
return res.json();
|
|
})
|
|
.then(render)
|
|
.catch(function (err) {
|
|
console.error('[djb-comments] load error', err);
|
|
});
|
|
}
|
|
|
|
function handleSubmit() {
|
|
const content = (inputEl.value || '').trim();
|
|
if (!content) {
|
|
if (window.customPopups) window.customPopups.showAlert('댓글 내용을 입력해 주세요.');
|
|
inputEl.focus();
|
|
return;
|
|
}
|
|
fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content: content })
|
|
})
|
|
.then(function (res) {
|
|
if (res.status === 409) throw new Error('종료된 문의에는 댓글을 작성할 수 없습니다.');
|
|
if (!res.ok) throw new Error('댓글 등록에 실패했습니다.');
|
|
return res.json();
|
|
})
|
|
.then(function () {
|
|
inputEl.value = '';
|
|
updateCounter();
|
|
load();
|
|
})
|
|
.catch(function (err) {
|
|
if (window.customPopups) window.customPopups.showAlert(err.message);
|
|
else alert(err.message);
|
|
});
|
|
}
|
|
|
|
function handleDelete(commentId) {
|
|
if (!commentId) return;
|
|
function doDelete() {
|
|
fetchJson('/djb/inquiry/comments/' + encodeURIComponent(commentId), { method: 'DELETE' })
|
|
.then(function (res) {
|
|
if (res.status === 403) throw new Error('본인이 작성한 댓글만 삭제할 수 있습니다.');
|
|
if (res.status === 409) throw new Error('종료된 문의에는 댓글을 삭제할 수 없습니다.');
|
|
if (!res.ok && res.status !== 204) throw new Error('댓글 삭제에 실패했습니다.');
|
|
load();
|
|
})
|
|
.catch(function (err) {
|
|
if (window.customPopups) window.customPopups.showAlert(err.message);
|
|
else alert(err.message);
|
|
});
|
|
}
|
|
if (window.customPopups) {
|
|
window.customPopups.showConfirm('댓글을 삭제하시겠습니까?', function (ok) {
|
|
if (ok) doDelete();
|
|
});
|
|
} else if (window.confirm('댓글을 삭제하시겠습니까?')) {
|
|
doDelete();
|
|
}
|
|
}
|
|
|
|
function updateCounter() {
|
|
if (!counterEl || !inputEl) return;
|
|
const max = inputEl.getAttribute('maxlength') || '2000';
|
|
counterEl.textContent = (inputEl.value || '').length + ' / ' + max;
|
|
}
|
|
|
|
if (!closed && formEl) {
|
|
formEl.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
});
|
|
inputEl.addEventListener('input', updateCounter);
|
|
updateCounter();
|
|
}
|
|
|
|
listEl.addEventListener('click', function (e) {
|
|
const target = e.target;
|
|
if (target && target.classList.contains('djb-comment-delete-btn')) {
|
|
handleDelete(target.getAttribute('data-id'));
|
|
}
|
|
});
|
|
|
|
load();
|
|
})();
|