Files
eapim-portal/src/main/resources/static/js/djb/inquiry-comments.js
T
Rinjae b58893d098
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
계정 보안 구현 - 연속 실패 시 계정 잠금 서비스 추가
- 비밀번호 본인확인 연속 실패 정책 및 처리 로직 추가
- 작성 요청 빈도 제한 서비스 및 메시지 구현
- 키보드 연속 문자 검증 규칙 및 테스트 추가
2026-08-25 20:24:46 +09:00

208 lines
7.9 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');
const privateEl = document.getElementById('djbCommentPrivate');
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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')
? '<span class="djb-comment-admin-badge">관리자</span>'
: '<span class="djb-comment-writer">' + escapeHtml(c.writerName || '(알 수 없음)') + '</span>';
// 비공개 댓글(열람 권한 있는 경우)에는 "비공개" 태그 표시
var privateTag = (c.privateComment && !c.privatePlaceholder)
? '<span class="djb-comment-private-tag">비공개</span>' : '';
li.innerHTML = ''
+ '<div class="djb-comment-meta">'
+ ' <div class="djb-comment-meta-left">'
+ ' <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 2px; opacity: 0.5;">'
+ ' <path d="M5 3v5a2 2 0 0 0 2 2h5 M9 7l3 3-3 3" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
+ ' </svg>'
+ writerHtml
+ privateTag
+ ' </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;
}
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();
})();