Q&A 댓글 기능 추가 및 첨부파일 제거
- djb 패키지 분리 - 댓글 CRUD/권한/알림 일체 - 같은 법인 공유 - 상세/댓글 조회 정책 일원화 - 작성자명 매핑 - PortalUser/TSEAIRM02 양쪽 조회 - 목록 - 댓글 수 표시 및 첨부 UI/로직 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 *;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
|
||||
<div th:fragment="commentSection(inquiry, commentWritable)" class="djb-comment-section"
|
||||
th:attr="data-inquiry-id=${inquiry.id},data-closed=${inquiry.inquiryStatus == 'CLOSED' ? 'true' : 'false'}">
|
||||
|
||||
<div class="djb-comment-list-box">
|
||||
<ul class="djb-comment-list" id="djbCommentList">
|
||||
<li class="djb-comment-empty" id="djbCommentEmpty">아직 등록된 댓글이 없습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="djb-comment-form-wrapper" th:if="${commentWritable}">
|
||||
<form id="djbCommentForm" class="djb-comment-form" onsubmit="return false;">
|
||||
<textarea id="djbCommentInput"
|
||||
class="djb-comment-input"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
placeholder="내용"
|
||||
required></textarea>
|
||||
<div class="djb-comment-form-actions">
|
||||
<span class="djb-comment-char-counter" id="djbCommentCharCounter">0 / 2000</span>
|
||||
<button type="submit" class="djb-comment-submit">댓글달기</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="djb-comment-form-wrapper djb-comment-readonly" th:unless="${commentWritable}">
|
||||
<p class="djb-comment-readonly-msg">종료된 문의에는 댓글을 작성할 수 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,28 +17,18 @@
|
||||
<div class="inquiry-detail-header">
|
||||
<div class="inquiry-header-top">
|
||||
<span class="inquiry-status-badge"
|
||||
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : 'inquiry-status-badge--pending'}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '답변대기'}">
|
||||
th:classappend="${inquiry.inquiryStatus == 'IN_PROGRESS' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'IN_PROGRESS' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
답변대기
|
||||
</span>
|
||||
<span class="inquiry-detail-date" th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
|
||||
</div>
|
||||
<h2 class="inquiry-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
|
||||
</div>
|
||||
|
||||
<!-- Attachment Section -->
|
||||
<div class="inquiry-detail-attachment" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
|
||||
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(inquiry.attachFile)}">
|
||||
<div class="attachment-item" th:each="fileDetail : ${fileInfo.getFileDetails()}">
|
||||
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'" class="inquiry-attachment-link">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="attachment-label">첨부파일</span>
|
||||
<span class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
|
||||
</a>
|
||||
<div class="inquiry-header-right">
|
||||
<span class="inquiry-detail-writer"
|
||||
th:text="${inquiry.inquirerName != null ? inquiry.inquirerName : (inquiry.inquirer != null ? inquiry.inquirer.userName : '')}">작성자</span>
|
||||
<span class="inquiry-detail-divider">|</span>
|
||||
<span class="inquiry-detail-date" th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="inquiry-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
|
||||
</div>
|
||||
|
||||
<!-- Inquiry Content Section -->
|
||||
@@ -49,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Admin Response Section -->
|
||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'RESPONDED'}">
|
||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'IN_PROGRESS' or inquiry.inquiryStatus == 'CLOSED'}">
|
||||
<div class="inquiry-response-header">
|
||||
<span class="response-label">관리자 답변</span>
|
||||
<span class="response-date" th:text="${#temporals.format(inquiry.responseDate, 'yyyy.MM.dd')}">2025.11.02</span>
|
||||
@@ -59,12 +49,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comment Section (DJBank Custom DJPGPT0002) -->
|
||||
<th:block th:replace="~{apps/community/djb/fragments-inquiry-comments :: commentSection(${inquiry}, ${commentWritable})}"></th:block>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="inquiry-detail-actions">
|
||||
<div class="actions-left">
|
||||
<button type="button" class="btn-inquiry-list" th:onclick="|location.href='@{/inquiry}'|">목록</button>
|
||||
</div>
|
||||
<div class="actions-right" th:if="${inquiry.inquiryStatus != 'RESPONDED' && inquiry.inquirer.id == #authentication.principal.id}">
|
||||
<div class="actions-right" th:if="${inquiry.inquiryStatus == 'PENDING' && inquiry.inquirer.id == #authentication.principal.id}">
|
||||
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: none;"></form>
|
||||
<button type="button" class="btn-inquiry-edit" th:onclick="|location.href='@{/inquiry/edit(id=${inquiry.id})}'|">수정</button>
|
||||
<button type="button" class="btn-inquiry-delete" onclick="confirmDelete()">삭제</button>
|
||||
@@ -87,10 +80,6 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function fn_downloadFile(fileId, fileSn) {
|
||||
window.open('[[@{/file/download}]]' + "?fileId=" + fileId + "&fileSn=" + fileSn);
|
||||
}
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.innerHTML = text;
|
||||
@@ -117,6 +106,7 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script th:src="@{/js/djb/inquiry-comments.js}"></script>
|
||||
</th:block>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- Form Content -->
|
||||
<div class="register-form-container">
|
||||
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
|
||||
th:object="${inquiry}" enctype="multipart/form-data" method="post" class="register-form">
|
||||
th:object="${inquiry}" method="post" class="register-form">
|
||||
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
|
||||
|
||||
<!-- Subject Field -->
|
||||
@@ -62,53 +62,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Field -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">첨부파일</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<div class="file-upload-inline">
|
||||
<div class="file-input-display" id="fileInputDisplay">
|
||||
<span class="file-display-text" id="fileDisplayText">선택된 파일이 없습니다</span>
|
||||
<button type="button" class="btn-remove-file-inline" id="btnRemoveFile" title="파일 삭제" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn-file-attach" onclick="document.getElementById('file').click()">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="20" height="20" fill="url(#pattern0_1161_9889)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0_1161_9889" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_1161_9889" transform="scale(0.0217391)"/>
|
||||
</pattern>
|
||||
<image id="image0_1161_9889" width="46" height="46" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAACXBIWXMAABcSAAAXEgFnn9JSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeRJREFUeNrsme1tgzAQhiHqAGxQRiATlGxAJ6i7QTpByQTtBrBBRiCdADYInaB0AvcsnaUTNR82GPzDr2Q5ODh6cO7LJgwWiHMeQZfgZReGYRO4LAEMreD/dYeWuQodQ/vh4ypcBK8JYC4eBMczXHGpzCXojICdB0xIwtcugX9IWx65h8kns8nyMAOWQRfjZUbG84Epj+QeYevfA/eJKPRpK3LU3K4KG+AV30ZG8OEAtEgq1Llu0L5m/qYwFYafS4WpvBDTkyrBbF7XWG1GVqTWnJuSuanGP6m18oeBcboi3UZBi+nAH3aOsCKqNCbwe4P/QjuZwB8MQiTDqnANPUE7o/Mbmw0FzInTVGT8OuVIC5xTJbaWqUQK59WVjrPHxinfgi69RegrGfluP3DcJT2PZWzoUtejirE8uAe3BN5ORIR+CGtcAZelbaJKDphR3yU0RJDOCXAAKckqFphhI5kxoavIil/22BQrU77cZNg8V+mVBPlqzokJ5KgojGQ6f1tlN2MjcwKYcNITHgbJ1sL4bSurWJTy8QFaH8c9uAf34B7cg3twD74xOK0/4hWP3OYonlUnDdTEYvK9V642G0FT8KP222ryhm0vVUt2QnvBX6fMM5wBHwfLDjl11WKdP6o/AQYA6KS7NOFJR28AAAAASUVORK5CYII="/>
|
||||
</defs>
|
||||
</svg>
|
||||
파일첨부
|
||||
</button>
|
||||
<input type="hidden" th:field="*{attachFile}">
|
||||
<input type="file"
|
||||
id="file"
|
||||
name="files"
|
||||
th:accept="${isInternalUser ? '*' : '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.jpeg,.png'}"
|
||||
style="display: none;">
|
||||
</div>
|
||||
<div class="form-help-text">
|
||||
<p><i class="fas fa-check-circle"></i>
|
||||
<span th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</span>
|
||||
</p>
|
||||
<p>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -134,11 +87,6 @@
|
||||
<script th:inline="javascript">
|
||||
$(function () {
|
||||
const btnSubmit = document.querySelector('.btn-submit-form');
|
||||
const fileInput = document.getElementById('file');
|
||||
const fileDisplayText = document.getElementById('fileDisplayText');
|
||||
const fileInputDisplay = document.getElementById('fileInputDisplay');
|
||||
const btnRemoveFile = document.getElementById('btnRemoveFile');
|
||||
const attachFileInput = document.querySelector('input[name="attachFile"]');
|
||||
const inquiryDetailTextarea = document.getElementById('inquiryDetail');
|
||||
const charCounter = document.querySelector('.char-counter');
|
||||
|
||||
@@ -148,13 +96,11 @@
|
||||
const maxLength = inquiryDetailTextarea.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
// Character counter for textarea
|
||||
inquiryDetailTextarea.addEventListener('input', function() {
|
||||
const currentLength = this.value.length;
|
||||
const maxLength = this.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
// Change color when near limit
|
||||
if (currentLength > maxLength * 0.9) {
|
||||
charCounter.style.color = '#FF6B6B';
|
||||
} else {
|
||||
@@ -163,15 +109,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Show existing file if present
|
||||
const existingFile = /*[[${!#strings.isEmpty(inquiry.attachFile) ? @fileService.findById(inquiry.attachFile).fileDetails[0].originalFileName + '.' + @fileService.findById(inquiry.attachFile).fileDetails[0].fileExtension : ''}]]*/ '';
|
||||
if (existingFile) {
|
||||
fileDisplayText.textContent = existingFile;
|
||||
fileDisplayText.classList.add('has-file');
|
||||
fileInputDisplay.classList.add('has-file');
|
||||
btnRemoveFile.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Form submission handler
|
||||
btnSubmit.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
@@ -180,7 +117,6 @@
|
||||
const inquirySubject = document.getElementById('inquirySubject').value.trim();
|
||||
const inquiryDetail = document.getElementById('inquiryDetail').value;
|
||||
|
||||
// Validation
|
||||
if (!inquirySubject) {
|
||||
customPopups.showAlert('제목을 작성해 주세요.');
|
||||
document.getElementById('inquirySubject').focus();
|
||||
@@ -195,47 +131,6 @@
|
||||
form.submit();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function () {
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (file) {
|
||||
/*[# th:if="${!isInternalUser}"]*/
|
||||
const allowedExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.gif', '.jpg', '.jpeg', '.png'];
|
||||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (!allowedExtensions.includes(fileExt)) {
|
||||
customPopups.showAlert("허용된 파일 형식이 아닙니다.\n문서파일(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp)과 이미지 파일(gif, jpg, png)만 등록 가능합니다.");
|
||||
fileInput.value = '';
|
||||
return;
|
||||
}
|
||||
/*[/]*/
|
||||
|
||||
fileDisplayText.textContent = file.name;
|
||||
fileDisplayText.classList.add('has-file');
|
||||
fileInputDisplay.classList.add('has-file');
|
||||
btnRemoveFile.style.display = 'flex';
|
||||
|
||||
if (attachFileInput) {
|
||||
attachFileInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// File remove handler
|
||||
btnRemoveFile.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
fileInput.value = '';
|
||||
fileDisplayText.textContent = '선택된 파일이 없습니다';
|
||||
fileDisplayText.classList.remove('has-file');
|
||||
fileInputDisplay.classList.remove('has-file');
|
||||
btnRemoveFile.style.display = 'none';
|
||||
|
||||
// Clear attachFile hidden input
|
||||
if (attachFileInput) {
|
||||
attachFileInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Focus on subject field
|
||||
document.getElementById('inquirySubject').focus();
|
||||
});
|
||||
|
||||
@@ -62,18 +62,16 @@
|
||||
<a th:href="@{/inquiry/detail(id=${inquiry.id})}" class="notice-title-link">
|
||||
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
|
||||
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
|
||||
<span class="file-icon" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="inquiry-comment-count"
|
||||
th:if="${commentCounts != null and commentCounts.get(inquiry.id) != null and commentCounts.get(inquiry.id) > 0}"
|
||||
th:text="|[${commentCounts.get(inquiry.id)}]|">[3]</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row-cell" style="width: 100px;" data-label="작성자" th:text="${inquiry.maskedInquirerName}">홍**</div>
|
||||
<div class="row-cell" style="width: 120px;" data-label="처리상태">
|
||||
<span class="inquiry-status-badge"
|
||||
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : 'inquiry-status-badge--pending'}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '답변대기'}">
|
||||
th:classappend="${inquiry.inquiryStatus == 'IN_PROGRESS' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'IN_PROGRESS' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
답변대기
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user