(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'); const privateEl = document.getElementById('djbCommentPrivate'); function getCsrfToken() { // 세션 기반 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, '''); } 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'); var cls = 'djb-comment-item'; if (c.adminYn === 'Y') cls += ' djb-comment-item--admin'; if (c.privatePlaceholder) cls += ' djb-comment-item--private-hidden'; else if (c.privateComment) cls += ' djb-comment-item--private'; li.className = cls; li.setAttribute('data-comment-id', c.id); var writerHtml = (c.adminYn === 'Y') ? '관리자' : '' + escapeHtml(c.writerName || '(알 수 없음)') + ''; // 비공개 댓글(열람 권한 있는 경우)에는 "비공개" 태그 표시 var privateTag = (c.privateComment && !c.privatePlaceholder) ? '비공개' : ''; li.innerHTML = '' + '
' + '
' + ' ' + ' ' + ' ' + writerHtml + privateTag + '
' + '
' + ' ' + escapeHtml(formatDate(c.createdDate)) + '' + (c.deletable ? ' ' : '') + '
' + '
' + '
' + escapeHtml(c.content) + '
'; 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; } var visibility = (privateEl && privateEl.checked) ? 'PRIVATE' : 'ALL'; fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments', { method: 'POST', body: JSON.stringify({ content: content, visibility: visibility }) }) .then(function (res) { if (res.status === 409) throw new Error('종료된 문의에는 댓글을 작성할 수 없습니다.'); // 작성 빈도 제한(무제한 요청 차단) — 서버가 내려준 안내 문구를 그대로 보여준다. if (res.status === 429) { return res.json() .catch(function () { return {}; }) .then(function (body) { throw new Error(body.message || '작성 빈도 제한을 초과했습니다. 잠시 후 다시 시도해 주세요.'); }); } if (!res.ok) throw new Error('댓글 등록에 실패했습니다.'); return res.json(); }) .then(function () { inputEl.value = ''; if (privateEl) privateEl.checked = false; 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(); })();