Q&A 댓글 기능 추가 및 첨부파일 제거
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- djb 패키지 분리 - 댓글 CRUD/권한/알림 일체
- 같은 법인 공유 - 상세/댓글 조회 정책 일원화
- 작성자명 매핑 - PortalUser/TSEAIRM02 양쪽 조회
- 목록 - 댓글 수 표시 및 첨부 UI/로직 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rinjae
2026-06-15 12:09:39 +09:00
parent c77c0a2f91
commit 3f664bcd5c
29 changed files with 1423 additions and 190 deletions
+226
View File
@@ -6727,6 +6727,232 @@ select.form-control {
font-weight: bold;
}
.inquiry-comment-count {
display: inline-block;
margin-left: 6px;
color: #DC2626;
font-weight: 600;
font-size: 14px;
}
.inquiry-header-right {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #6B7280;
}
.inquiry-header-right .inquiry-detail-writer {
font-weight: 600;
color: #374151;
}
.inquiry-header-right .inquiry-detail-divider {
color: #D1D5DB;
}
.inquiry-header-right .inquiry-detail-date {
font-size: inherit;
color: inherit;
}
.djb-comment-section {
margin: 40px 0 48px 0;
}
.djb-comment-list-box {
padding: 24px 28px;
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 8px;
min-height: 120px;
margin-bottom: 28px;
}
.djb-comment-list {
list-style: none;
padding: 0;
margin: 0;
}
.djb-comment-empty {
padding: 28px 0;
text-align: center;
font-size: 14px;
color: #9CA3AF;
}
.djb-comment-item {
padding: 18px 0;
border-bottom: 1px dashed #E5E7EB;
}
.djb-comment-item:first-child {
padding-top: 4px;
}
.djb-comment-item:last-child {
border-bottom: none;
padding-bottom: 4px;
}
.djb-comment-item--admin .djb-comment-writer {
color: #1D4ED8;
}
.djb-comment-item .djb-comment-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.djb-comment-item .djb-comment-meta-left {
display: flex;
align-items: center;
gap: 8px;
}
.djb-comment-item .djb-comment-writer {
font-size: 18px;
font-weight: 700;
color: #111827;
letter-spacing: -0.2px;
}
.djb-comment-item .djb-comment-admin-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background-color: #DBEAFE;
color: #1D4ED8;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.djb-comment-item .djb-comment-meta-right {
display: flex;
align-items: center;
gap: 12px;
}
.djb-comment-item .djb-comment-date {
color: #6B7280;
font-size: 14px;
}
.djb-comment-item .djb-comment-delete-btn {
background: none;
border: 1px solid #E5E7EB;
border-radius: 4px;
padding: 3px 10px;
font-size: 12px;
color: #6B7280;
cursor: pointer;
transition: all 0.15s ease;
}
.djb-comment-item .djb-comment-delete-btn:hover {
background-color: #FEE2E2;
border-color: #FCA5A5;
color: #B91C1C;
}
.djb-comment-item .djb-comment-body {
padding: 12px 16px;
background-color: #F3F4F6;
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: #1F2937;
white-space: pre-wrap;
word-break: break-word;
}
.djb-comment-form-wrapper {
display: block;
width: 100%;
margin-top: 8px;
}
.djb-comment-form {
display: block;
width: 100%;
}
textarea.djb-comment-input {
display: block;
width: 100%;
box-sizing: border-box;
padding: 16px 18px;
border: 1px solid #D1D5DB;
border-radius: 8px;
background-color: #F9FAFB;
font-size: 14px;
line-height: 1.6;
color: #1F2937;
resize: vertical;
min-height: 120px;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}
textarea.djb-comment-input::placeholder {
color: #9CA3AF;
}
textarea.djb-comment-input:hover {
border-color: #9CA3AF;
}
textarea.djb-comment-input:focus {
border-color: #1D4ED8;
background-color: #FFFFFF;
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15);
}
.djb-comment-form-actions {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 12px;
}
.djb-comment-char-counter {
font-size: 13px;
color: #94A3B8;
}
button.djb-comment-submit {
display: inline-block;
min-width: 140px;
padding: 12px 28px;
background-color: #1D4ED8;
color: #FFFFFF;
border: 1px solid #1D4ED8;
border-radius: 8px;
font-size: 15px;
font-weight: 700;
letter-spacing: -0.2px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
line-height: 1.2;
text-align: center;
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.18);
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
}
button.djb-comment-submit:hover {
background-color: #1E40AF;
border-color: #1E40AF;
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.28);
}
button.djb-comment-submit:active {
transform: translateY(1px);
}
button.djb-comment-submit:disabled {
background-color: #94A3B8;
border-color: #94A3B8;
box-shadow: none;
cursor: not-allowed;
}
.djb-comment-readonly-msg {
margin: 0;
padding: 16px 20px;
background-color: #F3F4F6;
border-radius: 6px;
text-align: center;
color: #6B7280;
font-size: 13px;
}
.hero-carousel-section {
position: relative;
width: 100%;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,172 @@
(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() {
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
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 {
const 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();
})();
@@ -0,0 +1,256 @@
// -----------------------------------------------------------------------------
// DJBank Custom — Q&A 댓글 영역 (DJPGPT0002)
// 설계서: DJPCSQ0300P 참조
// -----------------------------------------------------------------------------
// 목록 화면 — 제목 옆 댓글 개수 표시 (설계서 ⑥)
.inquiry-comment-count {
display: inline-block;
margin-left: 6px;
color: #DC2626;
font-weight: 600;
font-size: 14px;
}
// 상세 헤더 우측 그룹 (작성자 + 날짜)
.inquiry-header-right {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #6B7280;
.inquiry-detail-writer {
font-weight: 600;
color: #374151;
}
.inquiry-detail-divider {
color: #D1D5DB;
}
.inquiry-detail-date {
font-size: inherit;
color: inherit;
}
}
.djb-comment-section {
margin: 40px 0 48px 0;
}
// ── 댓글 목록 박스 (설계서 ① 영역) ─────────────────────────
.djb-comment-list-box {
padding: 24px 28px;
background-color: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 8px;
min-height: 120px;
margin-bottom: 28px;
}
.djb-comment-list {
list-style: none;
padding: 0;
margin: 0;
}
.djb-comment-empty {
padding: 28px 0;
text-align: center;
font-size: 14px;
color: #9CA3AF;
}
// ── 댓글 한 건 (설계서 작성자 + 날짜 + Contents) ───────────
.djb-comment-item {
padding: 18px 0;
border-bottom: 1px dashed #E5E7EB;
&:first-child {
padding-top: 4px;
}
&:last-child {
border-bottom: none;
padding-bottom: 4px;
}
&--admin .djb-comment-writer {
color: #1D4ED8;
}
.djb-comment-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.djb-comment-meta-left {
display: flex;
align-items: center;
gap: 8px;
}
.djb-comment-writer {
font-size: 18px;
font-weight: 700;
color: #111827;
letter-spacing: -0.2px;
}
.djb-comment-admin-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background-color: #DBEAFE;
color: #1D4ED8;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.djb-comment-meta-right {
display: flex;
align-items: center;
gap: 12px;
}
.djb-comment-date {
color: #6B7280;
font-size: 14px;
}
.djb-comment-delete-btn {
background: none;
border: 1px solid #E5E7EB;
border-radius: 4px;
padding: 3px 10px;
font-size: 12px;
color: #6B7280;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: #FEE2E2;
border-color: #FCA5A5;
color: #B91C1C;
}
}
.djb-comment-body {
padding: 12px 16px;
background-color: #F3F4F6;
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: #1F2937;
white-space: pre-wrap;
word-break: break-word;
}
}
// ── 작성 영역 (설계서 ②, ③) ───────────────────────────────
.djb-comment-form-wrapper {
display: block;
width: 100%;
margin-top: 8px;
}
.djb-comment-form {
display: block;
width: 100%;
}
textarea.djb-comment-input {
display: block;
width: 100%;
box-sizing: border-box;
padding: 16px 18px;
border: 1px solid #D1D5DB;
border-radius: 8px;
background-color: #F9FAFB;
font-size: 14px;
line-height: 1.6;
color: #1F2937;
resize: vertical;
min-height: 120px;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
&::placeholder {
color: #9CA3AF;
}
&:hover {
border-color: #9CA3AF;
}
&:focus {
border-color: #1D4ED8;
background-color: #FFFFFF;
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15);
}
}
.djb-comment-form-actions {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 12px;
}
.djb-comment-char-counter {
font-size: 13px;
color: #94A3B8;
}
button.djb-comment-submit {
display: inline-block;
min-width: 140px;
padding: 12px 28px;
background-color: #1D4ED8;
color: #FFFFFF;
border: 1px solid #1D4ED8;
border-radius: 8px;
font-size: 15px;
font-weight: 700;
letter-spacing: -0.2px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
line-height: 1.2;
text-align: center;
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.18);
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
&:hover {
background-color: #1E40AF;
border-color: #1E40AF;
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.28);
}
&:active {
transform: translateY(1px);
}
&:disabled {
background-color: #94A3B8;
border-color: #94A3B8;
box-shadow: none;
cursor: not-allowed;
}
}
.djb-comment-readonly-msg {
margin: 0;
padding: 16px 20px;
background-color: #F3F4F6;
border-radius: 6px;
text-align: center;
color: #6B7280;
font-size: 13px;
}
+1
View File
@@ -42,6 +42,7 @@
@use 'components/pagination' as *;
@use 'components/breadcrumb' as *;
@use 'components/test-env-notice' as *;
@use 'components/djb-inquiry-comments' as *;
// 5. Page-specific styles
@use 'pages/index' as *;