This commit is contained in:
+137
-8
@@ -6,6 +6,14 @@
|
||||
const STORAGE_KEY = 'baron.qa.posts.egbim';
|
||||
const COMMENTS_KEY = 'baron.qa.comments.egbim';
|
||||
const CATEGORY_CODES = { '오류문의': 'ERROR_QNA', '개선문의': 'IMPROVEMENT_QNA', '일반문의': 'GENERAL_QNA' };
|
||||
const STATUS_LABELS = {
|
||||
INIT: '접수',
|
||||
ON_REVIEW: '문의검토',
|
||||
DETAILED_REVIEW: '정밀검토',
|
||||
IN_PROGRESS: '처리중',
|
||||
RESOLVED: '답변완료',
|
||||
PENDING: '보류'
|
||||
};
|
||||
let authReady;
|
||||
|
||||
function qs(selector, root) { return (root || document).querySelector(selector); }
|
||||
@@ -30,6 +38,63 @@
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(posts));
|
||||
}
|
||||
|
||||
function updateStoredPost(post) {
|
||||
const posts = readJson(STORAGE_KEY, []);
|
||||
const index = posts.findIndex(function (item) { return item.id === post.id; });
|
||||
if (index < 0) return;
|
||||
posts[index] = Object.assign({}, posts[index], post);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(posts));
|
||||
}
|
||||
|
||||
function normalizeStatus(value) {
|
||||
if (value && typeof value === 'object') value = value.code || value.key || value.value || value.name || '';
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
const upper = raw.toUpperCase();
|
||||
return STATUS_LABELS[upper] || ({
|
||||
NEW: '접수', RECEIVED: '접수', 접수: '접수', 문의접수: '접수',
|
||||
REVIEW: '문의검토', 문의검토: '문의검토',
|
||||
DEEP: '정밀검토', 정밀검토: '정밀검토',
|
||||
PATCH: '처리중', 처리중: '처리중',
|
||||
DONE: '답변완료', COMPLETED: '답변완료', 답변완료: '답변완료',
|
||||
HOLD: '보류', 보류: '보류'
|
||||
}[upper] || raw);
|
||||
}
|
||||
|
||||
function extractFeedbackStatus(payload) {
|
||||
const root = payload && payload.feedback ? payload.feedback : payload;
|
||||
const candidates = [root, root && root.data, root && root.feedback, root && root.fields];
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const candidate = candidates[index];
|
||||
if (!candidate || typeof candidate !== 'object') continue;
|
||||
const value = candidate.feedback_status || candidate.feedbackStatus || candidate.status_code || candidate.status;
|
||||
if (value != null && value !== '') return normalizeStatus(value);
|
||||
}
|
||||
const fields = root && (Array.isArray(root.fields) ? root.fields : (root.data && Array.isArray(root.data.fields) ? root.data.fields : []));
|
||||
const statusField = fields.find(function (field) { return field && (field.key === 'feedback_status' || field.fieldKey === 'feedback_status' || field.name === 'feedback_status'); });
|
||||
return statusField ? normalizeStatus(statusField.value || statusField.val || statusField.data) : '';
|
||||
}
|
||||
|
||||
async function refreshRemoteStatus(post) {
|
||||
if (!post || !post.id || post.category === '공지사항') return false;
|
||||
try {
|
||||
const result = await requestJson('/api/feedbacks/' + encodeURIComponent(post.id), { method: 'GET' });
|
||||
const status = extractFeedbackStatus(result);
|
||||
if (!status || status === post.status) return false;
|
||||
post.status = status;
|
||||
updateStoredPost(post);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('[QA] feedback status sync failed:', post.id, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRemoteStatuses(posts) {
|
||||
const changed = await Promise.all((posts || []).map(refreshRemoteStatus));
|
||||
return changed.some(Boolean);
|
||||
}
|
||||
|
||||
function getComments() { return readJson(COMMENTS_KEY, {}); }
|
||||
|
||||
function parseJson(value, fallback) {
|
||||
@@ -177,7 +242,7 @@
|
||||
}
|
||||
|
||||
async function requestJson(path, options) {
|
||||
const sameOriginProxy = !config.apiBaseUrl && (path === config.createFeedbackPath || /^\/api\/feedbacks\/[^/]+\/comments$/.test(path));
|
||||
const sameOriginProxy = !config.apiBaseUrl && (path === config.createFeedbackPath || /^\/api\/feedbacks\/[^/]+(?:\/comments)?$/.test(path));
|
||||
if (!config.apiBaseUrl && !sameOriginProxy) return { local: true };
|
||||
const target = config.apiBaseUrl ? config.apiBaseUrl.replace(/\/$/, '') + path : path;
|
||||
const response = await fetch(target, Object.assign({ credentials: 'include' }, options));
|
||||
@@ -260,12 +325,13 @@
|
||||
tableBody.innerHTML = rows.map(function (post, index) {
|
||||
const isNotice = post.category === '공지사항';
|
||||
const number = isNotice ? '공지' : filtered.length - ((page - 1) * pageSize + index);
|
||||
const statusClass = post.status === '답변완료' ? 'done' : (post.status === '문의검토' || post.status === '정밀검토' ? 'review' : '');
|
||||
const status = normalizeStatus(post.status);
|
||||
const statusClass = status === '답변완료' ? 'done' : (status === '문의검토' || status === '정밀검토' ? 'review' : '');
|
||||
return '<tr class="' + (isNotice ? 'notice-row' : '') + '" data-id="' + escapeHtml(post.id) + '">' +
|
||||
'<td>' + number + '</td><td class="category-cell">' + escapeHtml(post.category.replace('문의', '')) + '</td>' +
|
||||
'<td>' + escapeHtml(post.company) + '</td><td>' + escapeHtml(post.department) + '</td><td>' + escapeHtml(post.author) + '</td>' +
|
||||
'<td class="subject"><span class="subject-link">' + (post.secret ? '<span class="lock">🔒</span>' : '') + escapeHtml(post.title) + '</span>' + (!isNotice && post.date === '2026-09-17' ? '<span class="new-mark">N</span>' : '') + '</td>' +
|
||||
'<td>' + escapeHtml(post.date) + '</td><td>' + (post.status ? '<span class="status ' + statusClass + '">' + escapeHtml(post.status) + '</span>' : '-') + '</td></tr>';
|
||||
'<td>' + escapeHtml(post.date) + '</td><td>' + (status ? '<span class="status ' + statusClass + '">' + escapeHtml(status) + '</span>' : '-') + '</td></tr>';
|
||||
}).join('');
|
||||
qsa('#qaRows tr').forEach(function (row) { row.addEventListener('click', function () { window.location.href = 'detail.html?id=' + encodeURIComponent(row.dataset.id); }); });
|
||||
pagination.innerHTML = Array.from({ length: totalPages }, function (_, i) { return '<button type="button" class="' + (i + 1 === page ? 'active' : '') + '" data-page="' + (i + 1) + '">' + (i + 1) + '</button>'; }).join('');
|
||||
@@ -275,6 +341,7 @@
|
||||
searchForm.addEventListener('submit', function (event) { event.preventDefault(); page = 1; render(); });
|
||||
qsa('input[name="category"], #onlyMine').forEach(function (input) { input.addEventListener('change', function () { page = 1; render(); }); });
|
||||
render();
|
||||
refreshRemoteStatuses(getPosts()).then(function (changed) { if (changed) render(); });
|
||||
}
|
||||
|
||||
function initWrite() {
|
||||
@@ -334,7 +401,7 @@
|
||||
}
|
||||
qs('[data-detail-category]').textContent = post.category;
|
||||
qs('[data-detail-title]').textContent = post.title;
|
||||
qs('[data-detail-status]').textContent = post.status || '공지';
|
||||
qs('[data-detail-status]').textContent = normalizeStatus(post.status) || '공지';
|
||||
qs('[data-detail-date]').textContent = post.date;
|
||||
qs('[data-detail-author]').textContent = post.author;
|
||||
qs('[data-detail-company]').textContent = post.company;
|
||||
@@ -346,14 +413,48 @@
|
||||
if (attachmentBox) attachmentBox.innerHTML = attachments.length ? attachments.map(function (attachment) {
|
||||
return '<div class="attachment-item">📎 ' + escapeHtml(attachment.originalFileName || '첨부파일') + '</div>';
|
||||
}).join('') : '<span class="help-text">첨부된 파일이 없습니다.</span>';
|
||||
function safeMediaUrl(value) {
|
||||
value = String(value || '');
|
||||
return /^(https?:\/\/|\/|blob:)/i.test(value) ? value : '';
|
||||
}
|
||||
|
||||
function normalizeAttachment(attachment) {
|
||||
if (typeof attachment === 'string') return { name: '첨부 이미지', url: safeMediaUrl(attachment), thumbnailUrl: safeMediaUrl(attachment) };
|
||||
attachment = attachment || {};
|
||||
const url = attachment.url || attachment.download_url || attachment.downloadUrl || attachment.signed_url || attachment.signedUrl || attachment.presigned_url || attachment.presignedUrl || '';
|
||||
const thumbnailUrl = attachment.thumbnail_url || attachment.thumbnailUrl || attachment.thumb_url || attachment.thumbUrl || url;
|
||||
return {
|
||||
name: attachment.original_file_name || attachment.originalFileName || attachment.file_name || attachment.fileName || attachment.name || '첨부 이미지',
|
||||
url: safeMediaUrl(url),
|
||||
thumbnailUrl: safeMediaUrl(thumbnailUrl)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeComment(comment) {
|
||||
comment = comment || {};
|
||||
return { id: comment.id || '', author: comment.author_name || comment.authorName || comment.author || comment.author?.name || '작성자', date: comment.created_at || comment.createdAt || comment.updated_at || '', content: comment.content || comment.body || '' };
|
||||
const rawAttachments = comment.attachments || comment.images || comment.comment_attachments || comment.commentAttachments || [];
|
||||
return {
|
||||
id: comment.id || '',
|
||||
author: comment.author_name || comment.authorName || comment.author?.name || comment.author || '작성자',
|
||||
date: comment.created_at || comment.createdAt || comment.updated_at || '',
|
||||
content: comment.content || comment.body || '',
|
||||
attachments: (Array.isArray(rawAttachments) ? rawAttachments : [rawAttachments]).map(normalizeAttachment)
|
||||
};
|
||||
}
|
||||
|
||||
function renderCommentAttachments(attachments) {
|
||||
return (attachments || []).map(function (attachment) {
|
||||
const imageUrl = attachment.thumbnailUrl || attachment.url;
|
||||
if (!imageUrl) return '<div class="comment-attachment-name">📎 ' + escapeHtml(attachment.name) + '</div>';
|
||||
const linkUrl = attachment.url || imageUrl;
|
||||
return '<a class="comment-image-link" href="' + escapeHtml(linkUrl) + '" target="_blank" rel="noopener noreferrer"><img class="comment-image" src="' + escapeHtml(imageUrl) + '" alt="' + escapeHtml(attachment.name) + '" loading="lazy"></a>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderComments(comments) {
|
||||
comments = (comments || []).map(normalizeComment).filter(function (comment) { return comment.content; });
|
||||
qs('#commentCount').textContent = comments.length;
|
||||
qs('#comments').innerHTML = comments.length ? comments.map(function (comment) { return '<div class="comment"><div class="comment-meta"><strong>' + escapeHtml(comment.author) + '</strong><span>' + escapeHtml(comment.date) + '</span></div><div class="comment-content">' + escapeHtml(comment.content) + '</div></div>'; }).join('') : '<div class="comment-empty">등록된 답변이 없습니다.</div>';
|
||||
qs('#comments').innerHTML = comments.length ? comments.map(function (comment) { return '<div class="comment"><div class="comment-meta"><strong>' + escapeHtml(comment.author) + '</strong><span>' + escapeHtml(comment.date) + '</span></div><div class="comment-content">' + escapeHtml(comment.content) + '</div>' + (comment.attachments.length ? '<div class="comment-attachments-list">' + renderCommentAttachments(comment.attachments) + '</div>' : '') + '</div>'; }).join('') : '<div class="comment-empty">등록된 답변이 없습니다.</div>';
|
||||
return comments;
|
||||
}
|
||||
let comments = renderComments(getComments()[post.id] || []);
|
||||
@@ -369,16 +470,32 @@
|
||||
const contentInput = qs('#commentContent');
|
||||
const errorBox = qs('#commentError');
|
||||
const submitButton = qs('#commentSubmit');
|
||||
const imageInput = qs('#commentImages');
|
||||
const files = imageInput ? Array.prototype.slice.call(imageInput.files || []) : [];
|
||||
const content = contentInput.value.trim();
|
||||
errorBox.hidden = true;
|
||||
if (!content) { errorBox.textContent = '댓글 내용을 입력해주세요.'; errorBox.hidden = false; return; }
|
||||
if (files.some(function (file) { return !/^image\//i.test(file.type); })) { errorBox.textContent = '댓글 첨부는 이미지 파일만 등록할 수 있습니다.'; errorBox.hidden = false; return; }
|
||||
if (files.some(function (file) { return file.size > 30 * 1024 * 1024; })) { errorBox.textContent = '댓글 이미지는 파일당 30MB 이하만 등록할 수 있습니다.'; errorBox.hidden = false; return; }
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = '등록 중...';
|
||||
try {
|
||||
const result = await postToApi(commentsPath, { content: content });
|
||||
comments.push(normalizeComment(result.comment || { id: result.id, content: content, author: (getAuthUser() || {}).name || '작성자', createdAt: new Date().toISOString() }));
|
||||
let payload = { content: content };
|
||||
if (files.length) {
|
||||
payload = new FormData();
|
||||
payload.append('content', content);
|
||||
files.forEach(function (file) { payload.append('attachments', file, file.name); });
|
||||
}
|
||||
const result = await postToApi(commentsPath, payload);
|
||||
const localAttachments = files.map(function (file) { const url = URL.createObjectURL(file); return { name: file.name, url: url, thumbnailUrl: url }; });
|
||||
const savedComment = normalizeComment(result.comment || { id: result.id, content: content, author: (getAuthUser() || {}).name || '작성자', createdAt: new Date().toISOString(), attachments: localAttachments });
|
||||
if (!savedComment.attachments.length) savedComment.attachments = localAttachments;
|
||||
comments.push(savedComment);
|
||||
renderComments(comments);
|
||||
contentInput.value = '';
|
||||
if (imageInput) imageInput.value = '';
|
||||
const imagePreview = qs('#commentImagePreview');
|
||||
if (imagePreview) imagePreview.innerHTML = '';
|
||||
} catch (error) {
|
||||
errorBox.textContent = error.message || '댓글 등록에 실패했습니다.';
|
||||
errorBox.hidden = false;
|
||||
@@ -387,6 +504,18 @@
|
||||
submitButton.textContent = '댓글 등록';
|
||||
}
|
||||
});
|
||||
const commentImages = qs('#commentImages');
|
||||
const commentImagePreview = qs('#commentImagePreview');
|
||||
if (commentImages && commentImagePreview) commentImages.addEventListener('change', function () {
|
||||
const files = Array.prototype.slice.call(commentImages.files || []);
|
||||
commentImagePreview.innerHTML = files.filter(function (file) { return /^image\//i.test(file.type); }).map(function (file) {
|
||||
const url = URL.createObjectURL(file);
|
||||
return '<div class="comment-image-preview-item"><img src="' + escapeHtml(url) + '" alt="' + escapeHtml(file.name) + '"><span>' + escapeHtml(file.name) + '</span></div>';
|
||||
}).join('');
|
||||
});
|
||||
refreshRemoteStatus(post).then(function (changed) {
|
||||
if (changed) qs('[data-detail-status]').textContent = normalizeStatus(post.status) || '공지';
|
||||
});
|
||||
qs('#backButton').addEventListener('click', function () { window.location.href = 'index.html'; });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user