315 lines
15 KiB
JavaScript
315 lines
15 KiB
JavaScript
/**
|
|
* Project Master Overseas Inquiries JS
|
|
* 기능: 문의사항 로드, 필터링, 답변 관리, 아코디언 및 이미지 모달
|
|
*/
|
|
|
|
// --- 초기화 ---
|
|
let allInquiries = [];
|
|
let currentSort = { field: 'no', direction: 'desc' };
|
|
|
|
async function loadInquiries() {
|
|
initStickyHeader();
|
|
|
|
const pmType = document.getElementById('filterPmType').value;
|
|
const category = document.getElementById('filterCategory').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}`);
|
|
allInquiries = await response.json();
|
|
|
|
refreshInquiryBoard();
|
|
} catch (e) {
|
|
console.error("데이터 로딩 중 오류 발생:", e);
|
|
}
|
|
}
|
|
|
|
function refreshInquiryBoard() {
|
|
const status = document.getElementById('filterStatus').value;
|
|
|
|
// 1. 상태 필터링
|
|
let filteredData = status ? allInquiries.filter(item => item.status === status) : [...allInquiries];
|
|
|
|
// 2. 정렬 적용
|
|
filteredData = sortData(filteredData);
|
|
|
|
// 3. 통계 및 리스트 렌더링
|
|
updateStats(allInquiries);
|
|
updateSortUI();
|
|
renderInquiryList(filteredData);
|
|
}
|
|
|
|
function handleSort(field) {
|
|
if (currentSort.field === field) {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.field = field;
|
|
currentSort.direction = 'asc';
|
|
}
|
|
refreshInquiryBoard();
|
|
}
|
|
|
|
function sortData(data) {
|
|
const { field, direction } = currentSort;
|
|
const modifier = direction === 'asc' ? 1 : -1;
|
|
|
|
return data.sort((a, b) => {
|
|
let valA = a[field];
|
|
let valB = b[field];
|
|
|
|
// 숫자형 변환 시도 (No 필드 등)
|
|
if (field === 'no' || !isNaN(valA)) {
|
|
valA = Number(valA);
|
|
valB = Number(valB);
|
|
}
|
|
|
|
// null/undefined 처리
|
|
if (valA === null || valA === undefined) valA = "";
|
|
if (valB === null || valB === undefined) valB = "";
|
|
|
|
if (valA < valB) return -1 * modifier;
|
|
if (valA > valB) return 1 * modifier;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
function updateSortUI() {
|
|
// 모든 헤더 클래스 및 아이콘 초기화
|
|
document.querySelectorAll('.inquiry-table thead th.sortable').forEach(th => {
|
|
th.classList.remove('active-sort');
|
|
const icon = th.querySelector('.sort-icon');
|
|
if (icon) {
|
|
// 레이아웃 시프트 방지를 위해 투명한 기본 아이콘(또는 공백) 유지
|
|
icon.textContent = "▲";
|
|
icon.style.opacity = "0";
|
|
}
|
|
});
|
|
|
|
// 현재 정렬된 헤더 강조 및 아이콘 표시
|
|
const activeTh = document.querySelector(`.inquiry-table thead th[onclick*="'${currentSort.field}'"]`);
|
|
if (activeTh) {
|
|
activeTh.classList.add('active-sort');
|
|
const icon = activeTh.querySelector('.sort-icon');
|
|
if (icon) {
|
|
icon.textContent = currentSort.direction === 'asc' ? "▲" : "▼";
|
|
icon.style.opacity = "1";
|
|
}
|
|
}
|
|
}
|
|
|
|
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 title="${item.author}">${item.author}</td>
|
|
<td title="${item.reg_date}">${item.reg_date}</td>
|
|
<td class="content-preview" title="${item.reply || ''}" style="color: #1e5149; font-weight: 500;">${item.reply || '-'}</td>
|
|
<td><span class="status-badge ${Utils.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 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);
|
|
document.getElementById(`reply-form-${id}`).classList.replace('editable', 'readonly');
|
|
} catch { 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 })
|
|
});
|
|
if ((await response.json()).success) { alert("저장되었습니다."); loadInquiries(); }
|
|
} catch { alert("저장 중 오류가 발생했습니다."); }
|
|
}
|
|
|
|
async function deleteReply(id) {
|
|
if (!confirm("답변을 삭제하시겠습니까?")) return;
|
|
try {
|
|
const response = await fetch(`${API.INQUIRIES}/${id}/reply`, { method: 'DELETE' });
|
|
if ((await response.json()).success) { alert("삭제되었습니다."); loadInquiries(); }
|
|
} catch { 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 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) {
|
|
document.getElementById('modalImage').src = src;
|
|
ModalManager.open('imageModal');
|
|
}
|
|
|
|
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 ? '▼' : '▲';
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadInquiries);
|
|
window.addEventListener('resize', initStickyHeader);
|