Files
test-mcp/js/inquiries.js

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();
});