279 lines
13 KiB
JavaScript
279 lines
13 KiB
JavaScript
|
|
async function loadInquiries() {
|
|
initStickyHeader();
|
|
|
|
const pmType = document.getElementById('filterPmType').value;
|
|
const category = document.getElementById('filterCategory').value;
|
|
const status = document.getElementById('filterStatus').value;
|
|
const keyword = document.getElementById('searchKeyword').value;
|
|
|
|
const params = new URLSearchParams({
|
|
pm_type: pmType,
|
|
category: category,
|
|
keyword: keyword
|
|
});
|
|
|
|
try {
|
|
const response = await fetch(`/api/inquiries?${params}`);
|
|
const data = await response.json();
|
|
|
|
updateStats(data);
|
|
|
|
const filteredData = status ? data.filter(item => item.status === status) : data;
|
|
renderInquiryList(filteredData);
|
|
} catch (e) {
|
|
console.error("데이터 로딩 중 오류 발생:", e);
|
|
}
|
|
}
|
|
|
|
function initStickyHeader() {
|
|
const header = document.getElementById('stickyHeader');
|
|
const thead = document.querySelector('.inquiry-table thead');
|
|
if (header && thead) {
|
|
const headerHeight = header.offsetHeight;
|
|
const totalOffset = 36 + headerHeight;
|
|
document.querySelectorAll('.inquiry-table thead th').forEach(th => {
|
|
th.style.top = totalOffset + 'px';
|
|
});
|
|
}
|
|
}
|
|
|
|
function renderInquiryList(data) {
|
|
const tbody = document.getElementById('inquiryList');
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr class="inquiry-row" onclick="toggleAccordion(${item.id})">
|
|
<td title="${item.no}">${item.no}</td>
|
|
<td style="text-align:center;">
|
|
${item.image_url ? `<img src="${item.image_url}" class="img-thumbnail" alt="thumbnail">` : '<span class="no-img">없음</span>'}
|
|
</td>
|
|
<td title="${item.pm_type}">${item.pm_type}</td>
|
|
<td title="${item.browser || 'Chrome'}">${item.browser || 'Chrome'}</td>
|
|
<td title="${item.category}">${item.category}</td>
|
|
<td title="${item.project_nm}">${item.project_nm}</td>
|
|
<td class="content-preview" title="${item.content}">${item.content}</td>
|
|
<td class="content-preview" title="${item.reply || ''}" style="color: #1e5149; font-weight: 500;">${item.reply || '-'}</td>
|
|
<td title="${item.author}">${item.author}</td>
|
|
<td title="${item.reg_date}">${item.reg_date}</td>
|
|
<td><span class="status-badge ${getStatusClass(item.status)}">${item.status}</span></td>
|
|
</tr>
|
|
<tr id="detail-${item.id}" class="detail-row">
|
|
<td colspan="11">
|
|
<div class="detail-container">
|
|
<button class="btn-close-accordion" onclick="toggleAccordion(${item.id})">접기</button>
|
|
<div class="detail-content-wrapper">
|
|
<div class="detail-meta-grid">
|
|
<div><span class="detail-label">작성자:</span> ${item.author}</div>
|
|
<div><span class="detail-label">등록일:</span> ${item.reg_date}</div>
|
|
<div><span class="detail-label">시스템:</span> ${item.pm_type}</div>
|
|
<div><span class="detail-label">환경:</span> ${item.browser || 'Chrome'} / ${item.device || 'PC'}</div>
|
|
</div>
|
|
|
|
<div class="detail-q-section">
|
|
<h4 style="margin-top:0; margin-bottom:10px; color:#1e5149;">[질문 내용]</h4>
|
|
<div style="line-height:1.6; white-space: pre-wrap;">${item.content}</div>
|
|
</div>
|
|
|
|
${item.image_url ? `
|
|
<div class="detail-image-section" id="img-section-${item.id}">
|
|
<div class="image-section-header" onclick="toggleImageSection(${item.id})">
|
|
<h4>
|
|
<span>🖼️</span> [첨부 이미지]
|
|
<span style="font-size:11px; color:#888; font-weight:normal;">(클릭 시 크게 보기)</span>
|
|
</h4>
|
|
<span class="toggle-icon">▼</span>
|
|
</div>
|
|
<div class="image-section-content collapsed" id="img-content-${item.id}">
|
|
<img src="${item.image_url}" class="preview-img" alt="Inquiry Image" style="cursor: pointer;" onclick="event.stopPropagation(); openImageModal(this.src)">
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="detail-a-section">
|
|
<h4 style="margin-top:0; margin-bottom:10px; color:#1e5149;">[조치 및 답변]</h4>
|
|
<div id="reply-form-${item.id}" class="reply-edit-form readonly">
|
|
<textarea id="reply-text-${item.id}" disabled placeholder="답변 내용이 없습니다.">${item.reply || ''}</textarea>
|
|
<div style="display:flex; justify-content: space-between; align-items: center;">
|
|
<div style="display:flex; gap:15px; align-items:center;">
|
|
<div class="filter-group" style="flex-direction:row; align-items:center; gap:8px;">
|
|
<label style="margin:0;">처리상태:</label>
|
|
<select id="reply-status-${item.id}" disabled style="padding:5px 10px;">
|
|
<option value="완료" ${item.status === '완료' ? 'selected' : ''}>완료</option>
|
|
<option value="작업 중" ${item.status === '작업 중' ? 'selected' : ''}>작업 중</option>
|
|
<option value="확인 중" ${item.status === '확인 중' ? 'selected' : ''}>확인 중</option>
|
|
<option value="개발예정" ${item.status === '개발예정' ? 'selected' : ''}>개발예정</option>
|
|
<option value="미확인" ${item.status === '미확인' ? 'selected' : ''}>미확인</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group" style="flex-direction:row; align-items:center; gap:8px;">
|
|
<label style="margin:0;">처리자:</label>
|
|
<input type="text" id="reply-handler-${item.id}" disabled value="${item.handler || ''}" placeholder="이름 입력" style="padding:5px 10px; width:100px;">
|
|
</div>
|
|
</div>
|
|
<div style="display:flex; gap:8px;">
|
|
<button class="btn-edit sync-btn" onclick="enableEdit(${item.id})" style="background:#1e5149; color:#fff; border:none;">${item.reply ? '수정하기' : '답변작성'}</button>
|
|
<button class="btn-save sync-btn" onclick="saveReply(${item.id})" style="background:#1e5149; color:#fff; border:none;">저장</button>
|
|
<button class="btn-delete sync-btn" onclick="deleteReply(${item.id})" style="background:#f44336; color:#fff; border:none;">삭제</button>
|
|
<button class="btn-cancel sync-btn" onclick="cancelEdit(${item.id})" style="background:#666; color:#fff; border:none;">취소</button>
|
|
</div>
|
|
</div>
|
|
${item.handled_date ? `<div style="margin-top:10px; font-size:12px; color:#888; text-align:right;">최종 수정일: ${item.handled_date}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function enableEdit(id) {
|
|
const form = document.getElementById(`reply-form-${id}`);
|
|
form.classList.replace('readonly', 'editable');
|
|
|
|
const elements = [`reply-text-${id}`, `reply-status-${id}`, `reply-handler-${id}`];
|
|
elements.forEach(elId => document.getElementById(elId).disabled = false);
|
|
document.getElementById(`reply-text-${id}`).focus();
|
|
}
|
|
|
|
async function cancelEdit(id) {
|
|
try {
|
|
const response = await fetch(`/api/inquiries/${id}`);
|
|
const item = await response.json();
|
|
|
|
const form = document.getElementById(`reply-form-${id}`);
|
|
const txt = document.getElementById(`reply-text-${id}`);
|
|
const status = document.getElementById(`reply-status-${id}`);
|
|
const handler = document.getElementById(`reply-handler-${id}`);
|
|
|
|
txt.value = item.reply || '';
|
|
status.value = item.status;
|
|
handler.value = item.handler || '';
|
|
|
|
[txt, status, handler].forEach(el => el.disabled = true);
|
|
form.classList.replace('editable', 'readonly');
|
|
} catch (e) {
|
|
loadInquiries();
|
|
}
|
|
}
|
|
|
|
async function saveReply(id) {
|
|
const reply = document.getElementById(`reply-text-${id}`).value;
|
|
const status = document.getElementById(`reply-status-${id}`).value;
|
|
const handler = document.getElementById(`reply-handler-${id}`).value;
|
|
|
|
if (!reply.trim() || !handler.trim()) return alert("내용과 처리자를 모두 입력해 주세요.");
|
|
|
|
try {
|
|
const response = await fetch(`/api/inquiries/${id}/reply`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ reply, status, handler })
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
alert("저장되었습니다.");
|
|
loadInquiries();
|
|
}
|
|
} catch (e) {
|
|
alert("저장 중 오류가 발생했습니다.");
|
|
}
|
|
}
|
|
|
|
async function deleteReply(id) {
|
|
if (!confirm("답변을 삭제하시겠습니까?")) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/inquiries/${id}/reply`, { method: 'DELETE' });
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
alert("삭제되었습니다.");
|
|
loadInquiries();
|
|
}
|
|
} catch (e) {
|
|
alert("삭제 중 오류가 발생했습니다.");
|
|
}
|
|
}
|
|
|
|
function toggleAccordion(id) {
|
|
const detailRow = document.getElementById(`detail-${id}`);
|
|
if (!detailRow) return;
|
|
const inquiryRow = detailRow.previousElementSibling;
|
|
const isActive = detailRow.classList.contains('active');
|
|
|
|
document.querySelectorAll('.detail-row.active').forEach(row => {
|
|
if (row.id !== `detail-${id}`) {
|
|
row.classList.remove('active');
|
|
if (row.previousElementSibling) row.previousElementSibling.classList.remove('active-row');
|
|
}
|
|
});
|
|
|
|
if (isActive) {
|
|
detailRow.classList.remove('active');
|
|
inquiryRow.classList.remove('active-row');
|
|
} else {
|
|
detailRow.classList.add('active');
|
|
inquiryRow.classList.add('active-row');
|
|
scrollToRow(inquiryRow);
|
|
}
|
|
}
|
|
|
|
function scrollToRow(row) {
|
|
setTimeout(() => {
|
|
const headerHeight = document.getElementById('stickyHeader').offsetHeight;
|
|
const totalOffset = 36 + headerHeight + 40;
|
|
const offsetPosition = (row.getBoundingClientRect().top + window.pageYOffset) - totalOffset;
|
|
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
|
|
}, 100);
|
|
}
|
|
|
|
function getStatusClass(status) {
|
|
const map = { '완료': 'status-complete', '작업 중': 'status-working', '확인 중': 'status-checking' };
|
|
return map[status] || 'status-pending';
|
|
}
|
|
|
|
function updateStats(data) {
|
|
const counts = {
|
|
Total: data.length,
|
|
Complete: data.filter(i => i.status === '완료').length,
|
|
Working: data.filter(i => i.status === '작업 중').length,
|
|
Checking: data.filter(i => i.status === '확인 중').length,
|
|
Pending: data.filter(i => i.status === '개발예정').length,
|
|
Unconfirmed: data.filter(i => i.status === '미확인').length
|
|
};
|
|
Object.keys(counts).forEach(k => {
|
|
const el = document.getElementById(`count${k}`);
|
|
if (el) el.textContent = counts[k].toLocaleString();
|
|
});
|
|
}
|
|
|
|
function openImageModal(src) {
|
|
const modal = document.getElementById('imageModal');
|
|
if (modal) {
|
|
document.getElementById('modalImage').src = src;
|
|
modal.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
function closeImageModal() {
|
|
const modal = document.getElementById('imageModal');
|
|
if (modal) modal.style.display = 'none';
|
|
}
|
|
|
|
function toggleImageSection(id) {
|
|
const section = document.getElementById(`img-section-${id}`);
|
|
const content = document.getElementById(`img-content-${id}`);
|
|
const icon = section.querySelector('.toggle-icon');
|
|
const isCollapsed = content.classList.toggle('collapsed');
|
|
section.classList.toggle('active', !isCollapsed);
|
|
icon.textContent = isCollapsed ? '▼' : '▲';
|
|
}
|
|
|
|
// Global Initialization
|
|
document.addEventListener('DOMContentLoaded', loadInquiries);
|
|
window.addEventListener('resize', initStickyHeader);
|
|
|
|
// Global Key Events (ESC)
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeImageModal();
|
|
});
|