feat(ui): apply inquiry list and modal design

Apply the supplied inquiry layout to the list and native write/detail dialogs while preserving existing API requests, author controls, attachments, comments and search. Keep legacy entry links and confirm before discarding drafts.

Validation: 98 tests and syntax checks pass; mocked Chromium desktop/mobile and six author/session visibility scenarios pass.
This commit is contained in:
root committed 2026-11-02 13:11:47 +09:00
1 parent dbf5dc3611
commit c62630e22d
19 files changed
+1362 -277

No files matched your search

+301 -80
View File
@@ -20,6 +20,7 @@
};
let authReady;
let resolvedAuthUser = null;
let inquiryUI = null;
function qs(selector, root) { return (root || document).querySelector(selector); }
function qsa(selector, root) { return Array.prototype.slice.call((root || document).querySelectorAll(selector)); }
@@ -590,6 +591,14 @@
let loadFailed = false;
let receivedRemotePage = false;
const searchStatus = qs('#searchStatus');
const changes = new Map();
const deleted = new Set();
function mergeChanges(remotePosts) {
const result = remotePosts.filter(function (post) { return !deleted.has(post.id); }).map(function (post) { return changes.get(post.id) || post; });
for (const post of changes.values()) if (!deleted.has(post.id) && !result.some(function (item) { return item.id === post.id; })) result.unshift(post);
return result;
}
function getPostCompany(post, user) { return getPostCompanyLabel(post, user); }
@@ -621,15 +630,19 @@
const isNotice = post.category === "공지사항";
const number = isNotice ? "공지" : filtered.length - ((page - 1) * pageSize + index);
const status = normalizeStatus(post.status);
const statusClass = status === "답변완료" ? "done" : (status === "문의검토" || status === "정밀검토" ? "review" : "");
const statusClass = status === '완료' ? 'done' : status === '신규' ? 'new' : status === '보류' ? 'hold' : status === '진행중' ? 'patch' : 'review';
const company = getPostCompany(post, user);
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(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.dateTime || post.date) + "</td><td>" + (status ? "<span class=\"status " + statusClass + "\">" + escapeHtml(status) + "</span>" : "-") + "</td></tr>";
'<td title="' + escapeHtml(company) + '">' + escapeHtml(company) + '</td><td title="' + escapeHtml(post.department) + '">' + escapeHtml(post.department) + '</td><td>' + escapeHtml(post.author) + '</td>' +
'<td class="inquiry-subject"><a class="subject-link" title="' + escapeHtml(post.title) + '" href="detail.html?id=' + encodeURIComponent(post.id) + '">' + (post.secret ? '<span class="lock" aria-label="비밀글">🔒</span>' : '') + '<span>' + escapeHtml(post.title) + '</span></a></td>' +
'<td title="' + escapeHtml(post.dateTime || post.date) + '">' + escapeHtml(post.dateTime || post.date) + '</td><td>' + (status ? '<span class="inquiry-status inquiry-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); }); });
qsa("#qaRows tr").forEach(function (row) { row.addEventListener("click", function (event) {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
if (inquiryUI) { event.preventDefault(); inquiryUI.open('detail', row.dataset.id, row.querySelector('a')); }
else window.location.href = 'detail.html?id=' + encodeURIComponent(row.dataset.id);
}); });
const visiblePages = Array.from(new Set([1, totalPages, page - 2, page - 1, page, page + 1, page + 2])).filter(function (value) { return value >= 1 && value <= totalPages; }).sort(function (a, b) { return a - b; });
pagination.innerHTML = '<button type="button" data-page="' + Math.max(1, page - 1) + '" aria-label="이전 페이지"' + (page === 1 ? ' disabled' : '') + '>‹</button>' + visiblePages.map(function (value, index) {
const gap = index > 0 && value - visiblePages[index - 1] > 1 ? '<span aria-hidden="true">…</span>' : '';
@@ -642,8 +655,8 @@
qsa("input[name=category], #onlyMine").forEach(function (input) { input.addEventListener("change", function () { page = 1; render(); }); });
render();
if (authReady) authReady.then(render);
loadRemotePosts(function (result) { receivedRemotePage = true; posts = result.posts; render(); }).then(function (result) {
posts = result.posts;
loadRemotePosts(function (result) { receivedRemotePage = true; posts = mergeChanges(result.posts); render(); }).then(function (result) {
posts = mergeChanges(result.posts);
remoteLoaded = true;
render();
}).catch(function (error) {
@@ -653,16 +666,26 @@
render();
if (!receivedRemotePage) refreshRemoteStatuses(posts).then(function (changed) { if (changed) render(); });
});
return {
upsert: function (post) { changes.set(post.id, post); posts = mergeChanges(posts); render(); },
remove: function (id) { changes.delete(id); deleted.add(id); posts = posts.filter(function (post) { return post.id !== id; }); render(); }
};
}
function initWrite() {
const form = qs('#qaForm');
function initWrite(root) {
const find = function (selector, container) { return qs(selector, container || root); };
const form = find('#qaForm');
if (!form) return;
const user = getAuthUser();
const errorBox = qs('#formError');
if (!user) {
qs('#writeFields').setAttribute('aria-disabled', 'true');
}
const errorBox = find('#formError');
Promise.resolve(authReady || user).then(function (profile) {
for (const [selector, value] of [['#writeCompany', profile && profile.company], ['#writeDepartment', profile && profile.department], ['#writeAuthor', profile && (profile.name || profile.loginId)], ['#writeEmail', profile && profile.email]]) {
const field = find(selector);
if (field) field.value = value || '-';
}
const notice = find('[data-auth-state]');
if (notice) notice.textContent = profile ? '로그인한 사용자 정보로 문의가 등록됩니다.' : '로그인 상태를 확인하지 못했습니다. 새로고침 후 다시 시도해주세요.';
});
form.addEventListener('submit', async function (event) {
event.preventDefault();
console.info('[QA] feedback submit started');
@@ -671,17 +694,17 @@
console.info('[QA] SSO user resolved:', Boolean(currentUser), currentUser ? { hasRequesterId: Boolean(currentUser.requesterId), hasTenantId: Boolean(currentUser.requesterTenantId), hasPhone: Boolean(currentUser.phone) } : null);
if (!currentUser) { errorBox.textContent = '로그인 후 문의를 등록할 수 있습니다.'; errorBox.style.display = 'block'; return; }
if (!currentUser.requesterId || !currentUser.requesterTenantId) { errorBox.textContent = 'SSO에서 작성자 UUID와 테넌트 정보를 확인하지 못했습니다. 다시 로그인한 뒤 시도해주세요.'; errorBox.style.display = 'block'; return; }
const title = qs('#title').value.trim();
const content = qs('#content').value.trim();
if (!qs('#category').value || !title || !content) { errorBox.textContent = '구분, 제목, 내용을 모두 입력해주세요.'; errorBox.style.display = 'block'; return; }
const title = find('#title').value.trim();
const content = find('#content').value.trim();
if (!find('#category').value || !title || !content) { errorBox.textContent = '구분, 제목, 내용을 모두 입력해주세요.'; errorBox.style.display = 'block'; return; }
const feedbackId = makeUuid();
const createdAt = new Date().toISOString();
const post = { id: feedbackId, feedbackId: feedbackId, requesterId: currentUser.requesterId, requesterTenantId: currentUser.requesterTenantId, category: qs('#category').value, company: currentUser.company || currentUser.tenantName || currentUser.familyCompany || '외부 사용자', department: currentUser.department || '-', author: currentUser.name || currentUser.loginId, title: title, date: formatKoreanDateTime(createdAt, false), dateTime: formatKoreanDateTime(createdAt, true), createdAt: createdAt, status: '신규', secret: qs('#secret').checked, content: content };
const submitButton = qs('#submitButton');
const post = { id: feedbackId, feedbackId: feedbackId, requesterId: currentUser.requesterId, requesterTenantId: currentUser.requesterTenantId, category: find('#category').value, company: currentUser.company || currentUser.tenantName || currentUser.familyCompany || '외부 사용자', department: currentUser.department || '-', author: currentUser.name || currentUser.loginId, title: title, date: formatKoreanDateTime(createdAt, false), dateTime: formatKoreanDateTime(createdAt, true), createdAt: createdAt, status: '신규', secret: find('#secret').checked, content: content };
const submitButton = find('#submitButton');
submitButton.disabled = true;
submitButton.textContent = '등록 중...';
try {
const submission = await buildFeedbackSubmission(post, feedbackId, qs('#attachment').files);
const submission = await buildFeedbackSubmission(post, feedbackId, find('#attachment').files);
console.info('[QA] sending feedback to Worker:', config.createFeedbackPath);
const result = await postToApi(config.createFeedbackPath, submission.body);
console.info('[QA] Worker feedback response:', result);
@@ -689,7 +712,8 @@
post.attachments = submission.attachments;
savePost(post);
showToast(result.local ? '테스트 글로 저장했습니다. UUID가 생성되었습니다.' : '문의가 등록되었습니다.');
window.setTimeout(function () { window.location.href = 'detail.html?id=' + encodeURIComponent(post.id); }, 500);
if (inquiryUI) inquiryUI.saved(post);
else window.setTimeout(function () { window.location.href = 'detail.html?id=' + encodeURIComponent(post.id); }, 500);
} catch (error) {
console.error('[QA] feedback submit failed:', error);
errorBox.textContent = error.message || '등록 중 오류가 발생했습니다.';
@@ -700,14 +724,15 @@
});
}
async function initDetail() {
const detail = qs('#detail');
async function initDetail(root, requestedId) {
const find = function (selector, container) { return qs(selector, container || root); };
const detail = find('#detail');
if (!detail) return;
const detailListLink = qs('.detail-actions > a', detail);
const detailListLink = find('.detail-actions > a', detail);
if (detailListLink) detailListLink.textContent = '목록으로';
const bottomBackButton = qs('#backButton', detail);
const bottomBackButton = find('#backButton', detail);
if (bottomBackButton && bottomBackButton.parentElement) bottomBackButton.parentElement.remove();
const id = new URLSearchParams(window.location.search).get("id") || "EG-1048";
const id = requestedId || new URLSearchParams(window.location.search).get("id") || "EG-1048";
const storedPost = getPosts().find(function (item) { return item.id === id; });
let post = storedPost;
try {
@@ -718,42 +743,45 @@
console.warn("[QA] feedback detail load failed:", error.message);
}
if (!post) {
qs('[data-detail-title]').textContent = '문의 내용을 찾을 수 없습니다.';
qs('[data-detail-content]').textContent = '목록에 등록된 문의가 없습니다.';
find('[data-detail-title]').textContent = '문의 내용을 찾을 수 없습니다.';
find('[data-detail-content]').textContent = '목록에 등록된 문의가 없습니다.';
const commentBox = find('.comment-box');
if (commentBox) commentBox.hidden = true;
return;
}
qs('[data-detail-category]').textContent = post.category;
qs('[data-detail-title]').textContent = post.title;
find('[data-detail-category]').textContent = post.category;
find('[data-detail-title]').textContent = post.title;
const renderDetailStatus = function () {
const statusElement = qs("[data-detail-status]");
const statusElement = find("[data-detail-status]");
const status = normalizeStatus(post.status) || "공지";
if (!statusElement) return;
statusElement.textContent = status;
statusElement.classList.toggle("done", status === "완료");
statusElement.classList.toggle("review", status !== "완료");
if (statusElement.setAttribute) statusElement.setAttribute('data-status', status);
};
renderDetailStatus();
qs('[data-detail-date]').textContent = post.dateTime || post.date;
qs('[data-detail-author]').textContent = post.author;
find('[data-detail-date]').textContent = post.dateTime || post.date;
find('[data-detail-author]').textContent = post.author;
const detailUser = await (authReady || Promise.resolve(getAuthUser()));
qs('[data-detail-company]').textContent = getPostCompanyLabel(post, detailUser);
qs('[data-detail-department]').textContent = post.department;
qs('[data-detail-content]').textContent = post.content;
qs('[data-detail-secret]').hidden = !post.secret;
const attachmentBox = qs('[data-detail-attachments]');
find('[data-detail-company]').textContent = getPostCompanyLabel(post, detailUser);
find('[data-detail-department]').textContent = post.department;
find('[data-detail-content]').textContent = post.content;
find('[data-detail-secret]').hidden = !post.secret;
const attachmentBox = find('[data-detail-attachments]');
const attachments = Array.isArray(post.attachments) ? post.attachments : [];
if (attachmentBox) attachmentBox.innerHTML = attachments.length ? attachments.map(function (attachment) {
const file = normalizeFeedbackAttachment(attachment);
if (!file.url) return '<div class="attachment-item">📎 ' + escapeHtml(file.name) + '</div>';
return '<div class="attachment-item"><a href="' + escapeHtml(file.url) + '" target="_blank" rel="noopener noreferrer">📎 ' + escapeHtml(file.name) + '</a></div>';
}).join('') : '<span class="help-text">첨부된 파일이 없습니다.</span>';
const editButton = qs('#editButton');
const deleteButton = qs('#deleteButton');
const editForm = qs('#editForm');
const editError = qs('#editError');
const completionNotice = qs("#completionNotice");
const completionDoneNotice = qs("#completionDoneNotice");
const completionButton = qs("#completionButton");
const editButton = find('#editButton');
const deleteButton = find('#deleteButton');
const editForm = find('#editForm');
const editError = find('#editError');
const completionNotice = find("#completionNotice");
const completionDoneNotice = find("#completionDoneNotice");
const completionButton = find("#completionButton");
const renderCompletionNotice = function () {
const owner = isPostOwner(post, detailUser);
const completed = normalizeStatus(post.status) === "완료" || post.completionConfirmed;
@@ -778,6 +806,7 @@
post.completionConfirmed = true;
post.status = "완료";
updateStoredPost(post);
if (inquiryUI) inquiryUI.changed(post);
renderDetailStatus();
renderCompletionNotice();
showToast("문의가 완료 처리되었습니다.");
@@ -789,49 +818,55 @@
}
});
if (editButton && editForm) editButton.addEventListener('click', function () {
qs('#editCategory').value = post.category || '일반문의';
qs('#editTitle').value = post.title || '';
qs('#editContent').value = post.content || '';
qs('#editSecret').checked = Boolean(post.secret);
find('#editCategory').value = post.category || '일반문의';
find('#editTitle').value = post.title || '';
find('#editContent').value = post.content || '';
find('#editSecret').checked = Boolean(post.secret);
editError.hidden = true;
editError.style.display = 'none';
editForm.hidden = false;
detail.classList.add('is-editing');
editForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (inquiryUI) inquiryUI.markClean(editForm);
});
const editCancel = find('#editCancel');
if (editCancel && editForm) editCancel.addEventListener('click', function () {
if (inquiryUI && !inquiryUI.discardForm(editForm)) return;
editForm.reset(); editForm.hidden = true; detail.classList.remove('is-editing');
if (inquiryUI) inquiryUI.markClean(editForm);
});
const editCancel = qs('#editCancel');
if (editCancel && editForm) editCancel.addEventListener('click', function () { editForm.hidden = true; detail.classList.remove('is-editing'); });
if (editForm) editForm.addEventListener('submit', async function (event) {
event.preventDefault();
editError.hidden = true;
editError.style.display = 'none';
const title = qs('#editTitle').value.trim();
const content = qs('#editContent').value.trim();
const category = qs('#editCategory').value;
const title = find('#editTitle').value.trim();
const content = find('#editContent').value.trim();
const category = find('#editCategory').value;
if (!title || !content || !category) {
editError.textContent = '구분, 제목, 내용을 모두 입력해주세요.';
editError.hidden = false;
editError.style.display = 'block';
return;
}
const saveButton = qs('button[type="submit"]', editForm);
const saveButton = find('button[type="submit"]', editForm);
saveButton.disabled = true;
saveButton.textContent = '저장 중...';
try {
await requestJson('/api/feedbacks/' + encodeURIComponent(post.id), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: title, contents: content, Category: CATEGORY_CODES[category], is_secret: qs('#editSecret').checked ? 'true' : 'false' })
body: JSON.stringify({ title: title, contents: content, Category: CATEGORY_CODES[category], is_secret: find('#editSecret').checked ? 'true' : 'false' })
});
post.category = category;
post.title = title;
post.content = content;
post.secret = qs('#editSecret').checked;
post.secret = find('#editSecret').checked;
updateStoredPost(post);
qs('[data-detail-category]').textContent = post.category;
qs('[data-detail-title]').textContent = post.title;
qs('[data-detail-content]').textContent = post.content;
qs('[data-detail-secret]').hidden = !post.secret;
if (inquiryUI) { inquiryUI.changed(post); inquiryUI.markClean(editForm); }
find('[data-detail-category]').textContent = post.category;
find('[data-detail-title]').textContent = post.title;
find('[data-detail-content]').textContent = post.content;
find('[data-detail-secret]').hidden = !post.secret;
editForm.hidden = true;
detail.classList.remove('is-editing');
showToast('문의가 수정되었습니다.');
@@ -850,7 +885,8 @@
try {
await requestJson('/api/feedbacks/' + encodeURIComponent(post.id), { method: 'DELETE' });
removeStoredPost(post.id);
window.location.href = 'index.html';
if (inquiryUI) inquiryUI.deleted(post.id);
else window.location.href = 'index.html';
} catch (error) {
deleteButton.disabled = false;
showToast(error.message || '문의 삭제에 실패했습니다.');
@@ -896,15 +932,34 @@
}
let editingCommentId = null;
const objectUrls = new Set();
const previewUrls = new Set();
function imageObjectUrl(file, preview) {
const url = URL.createObjectURL(file);
objectUrls.add(url);
if (preview) previewUrls.add(url);
return url;
}
function clearImagePreview() {
previewUrls.forEach(function (url) { URL.revokeObjectURL(url); objectUrls.delete(url); });
previewUrls.clear();
}
if (inquiryUI) inquiryUI.onCleanup(root, function () { objectUrls.forEach(function (url) { URL.revokeObjectURL(url); }); });
function isCommentOwner(comment, user) {
return matchesAuthorId(comment.authorId, user);
}
function renderComments(comments) {
comments = (comments || []).map(normalizeComment).filter(function (comment) { return comment.content; });
qs('#commentCount').textContent = comments.length;
const usedUrls = new Set(comments.flatMap(function (comment) {
return comment.attachments.flatMap(function (attachment) { return [attachment.url, attachment.thumbnailUrl]; });
}));
objectUrls.forEach(function (url) {
if (!usedUrls.has(url) && !previewUrls.has(url)) { URL.revokeObjectURL(url); objectUrls.delete(url); }
});
find('#commentCount').textContent = comments.length;
const user = detailUser;
qs('#comments').innerHTML = comments.length ? comments.map(function (comment) {
find('#comments').innerHTML = comments.length ? comments.map(function (comment) {
const owner = Boolean(comment.id) && isCommentOwner(comment, user);
const editing = Boolean(editingCommentId) && editingCommentId === comment.id && owner;
const actions = owner ? '<div class="comment-actions"><button class="button button-outline" type="button" data-comment-edit="' + escapeHtml(comment.id) + '">수정</button><button class="button button-danger" type="button" data-comment-delete="' + escapeHtml(comment.id) + '">삭제</button></div>' : '';
@@ -923,7 +978,7 @@
}).catch(function (error) {
console.warn('[QA] comments load failed:', error.message);
});
qs('#comments').addEventListener('click', async function (event) {
find('#comments').addEventListener('click', async function (event) {
const editButton = event.target.closest('[data-comment-edit]');
const cancelButton = event.target.closest('[data-comment-cancel]');
const saveButton = event.target.closest('[data-comment-save]');
@@ -931,15 +986,19 @@
if (editButton) {
editingCommentId = editButton.getAttribute('data-comment-edit');
comments = renderComments(comments);
const input = qs('[data-comment-edit-input]');
const input = find('[data-comment-edit-input]');
if (input) { input.focus(); input.setSelectionRange(input.value.length, input.value.length); }
return;
}
if (cancelButton) { editingCommentId = null; comments = renderComments(comments); return; }
if (cancelButton) {
const input = find('[data-comment-edit-input]');
if (inquiryUI && input && !inquiryUI.discardEditor(input)) return;
editingCommentId = null; comments = renderComments(comments); return;
}
if (saveButton) {
const commentId = saveButton.getAttribute('data-comment-save');
const comment = comments.find(function (item) { return item.id === commentId; });
const input = qs('[data-comment-edit-input]');
const input = find('[data-comment-edit-input]');
const content = input ? input.value.trim() : '';
if (!content) { showToast('댓글 내용을 입력해주세요.'); return; }
saveButton.disabled = true;
@@ -965,13 +1024,13 @@
} catch (error) { showToast(error.message || '댓글 삭제에 실패했습니다.'); deleteButton.disabled = false; }
}
});
const commentForm = qs('#commentForm');
const commentForm = find('#commentForm');
if (commentForm) commentForm.addEventListener('submit', async function (event) {
event.preventDefault();
const contentInput = qs('#commentContent');
const errorBox = qs('#commentError');
const submitButton = qs('#commentSubmit');
const imageInput = qs('#commentImages');
const contentInput = find('#commentContent');
const errorBox = find('#commentError');
const submitButton = find('#commentSubmit');
const imageInput = find('#commentImages');
const files = imageInput ? Array.prototype.slice.call(imageInput.files || []) : [];
const content = contentInput.value.trim();
errorBox.hidden = true;
@@ -988,7 +1047,7 @@
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 localAttachments = files.map(function (file) { const url = imageObjectUrl(file); return { name: file.name, url: url, thumbnailUrl: url }; });
const savedComment = normalizeComment(Object.assign({
content: content,
author: (getAuthUser() || {}).name || '작성자',
@@ -1002,8 +1061,10 @@
renderComments(comments);
contentInput.value = '';
if (imageInput) imageInput.value = '';
const imagePreview = qs('#commentImagePreview');
const imagePreview = find('#commentImagePreview');
if (imagePreview) imagePreview.innerHTML = '';
clearImagePreview();
if (inquiryUI) inquiryUI.markClean(commentForm);
} catch (error) {
errorBox.textContent = error.message || '댓글 등록에 실패했습니다.';
errorBox.hidden = false;
@@ -1012,21 +1073,181 @@
submitButton.textContent = '댓글 등록';
}
});
const commentImages = qs('#commentImages');
const commentImagePreview = qs('#commentImagePreview');
const commentImageLabel = qs('label[for="commentImages"]');
const commentImages = find('#commentImages');
const commentImagePreview = find('#commentImagePreview');
const commentImageLabel = find('label[for="commentImages"]');
if (commentImageLabel) commentImageLabel.textContent = '댓글 이미지 첨부';
if (commentImages && commentImagePreview) commentImages.addEventListener('change', function () {
clearImagePreview();
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);
const url = imageObjectUrl(file, true);
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) renderDetailStatus();
if (changed && (!inquiryUI || inquiryUI.isCurrent(root))) {
renderDetailStatus();
if (inquiryUI) inquiryUI.changed(post);
}
});
}
document.addEventListener('DOMContentLoaded', function () { normalizePageHeading(); bindGlobal(); initList(); initWrite(); initDetail(); });
function initInquiryUI(list) {
const dialogs = { write: qs('#composeModal'), detail: qs('#detailModal') };
if (!dialogs.write || !dialogs.detail) return;
const baselines = new WeakMap();
let active = null;
let restoringHistory = false;
let depth = 0;
function snapshot(form) {
return JSON.stringify(Array.from(form.elements).filter(function (input) {
return !input.readOnly && !input.disabled && !['submit', 'button'].includes(input.type);
}).map(function (input) {
return [input.id, input.type === 'file' ? Array.from(input.files).map(function (file) { return [file.name, file.size, file.lastModified]; }) : input.type === 'checkbox' ? input.checked : input.value];
}));
}
function dirty(form) { return !form.hidden && snapshot(form) !== baselines.get(form); }
function dirtyEditor(input) { return input.value !== input.defaultValue; }
function busy() { return active && Boolean(active.root.querySelector('button:disabled')); }
function hasChanges() {
return active && (qsa('form', active.root).some(dirty) || qsa('[data-comment-edit-input]', active.root).some(dirtyEditor));
}
function confirmDiscard() { return window.confirm('작성 중인 내용이 있습니다. 닫으면 입력한 내용이 사라집니다. 닫으시겠습니까?'); }
function canLeave(force) {
if (force || !active) return true;
if (busy()) { showToast('처리가 끝난 후 닫을 수 있습니다.'); return false; }
return !hasChanges() || confirmDiscard();
}
function clear(restoreFocus) {
if (!active) return;
const previous = active;
active = null;
previous.dialog.close();
previous.cleanups.forEach(function (cleanup) { cleanup(); });
const toast = qs('#toast');
if (toast) document.body.append(toast);
previous.dialog.querySelector('[data-modal-content]').replaceChildren();
document.documentElement.classList.remove('inquiry-modal-open');
if (restoreFocus) {
window.scrollTo({ top: previous.scroll, behavior: 'instant' });
const opener = previous.opener && previous.opener.isConnected ? previous.opener : qs('#writeButton') || qs('#boardHeading');
if (opener) opener.focus({ preventScroll: true });
}
}
function routeURL(view, id) {
const url = new URL(window.location.href);
url.searchParams.delete('view'); url.searchParams.delete('id');
if (view) url.searchParams.set('view', view);
if (view === 'detail' && id) url.searchParams.set('id', id);
return url;
}
async function mount(view, id, opener) {
const scroll = active ? active.scroll : window.scrollY;
const previousOpener = active && active.opener;
clear(false);
const dialog = dialogs[view];
if (!dialog) return;
const root = document.createElement('div');
root.setAttribute('aria-busy', view === 'detail' ? 'true' : 'false');
root.append(qs(view === 'write' ? '#writeTemplate' : '#detailTemplate').content.cloneNode(true));
dialog.querySelector('[data-modal-content]').replaceChildren(root);
const toast = qs('#toast');
if (toast) dialog.append(toast);
active = { view: view, id: id, root: root, dialog: dialog, opener: opener || previousOpener, scroll: scroll, cleanups: [] };
qsa('form', root).forEach(function (form) { baselines.set(form, snapshot(form)); });
document.documentElement.classList.add('inquiry-modal-open');
dialog.showModal();
const heading = root.querySelector('h2');
if (heading) heading.focus({ preventScroll: true });
try {
if (view === 'write') initWrite(root);
else if (id) await initDetail(root, id);
else {
root.querySelector('[data-detail-title]').textContent = '문의 내용을 찾을 수 없습니다.';
root.querySelector('.comment-box').hidden = true;
}
} catch (error) {
if (active && active.root === root) showToast('화면을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.');
console.warn('[QA] dialog initialization failed:', error.message);
} finally {
root.setAttribute('aria-busy', 'false');
}
}
function open(view, id, opener, options) {
options = options || {};
if (!canLeave(options.force)) return;
const url = routeURL(view, id);
if (active || options.replace) history.replaceState({ qaModal: true }, '', url);
else history.pushState({ qaModal: true }, '', url);
depth = 1;
mount(view, id, opener);
}
function close(force) {
if (!canLeave(force)) return;
clear(true);
if (depth === 1) history.back();
else history.replaceState({ qaModal: false }, '', routeURL());
}
inquiryUI = {
open: open,
isCurrent: function (root) { return Boolean(active && active.root === root); },
markClean: function (form) { baselines.set(form, snapshot(form)); },
discardForm: function (form) { return !dirty(form) || confirmDiscard(); },
discardEditor: function (input) { return !dirtyEditor(input) || confirmDiscard(); },
onCleanup: function (root, callback) { if (active && active.root === root) active.cleanups.push(callback); else callback(); },
changed: function (post) { if (list) list.upsert(post); },
saved: function (post) { if (list) list.upsert(post); open('detail', post.id, null, { force: true, replace: true }); },
deleted: function (id) { if (list) list.remove(id); close(true); }
};
qsa('[data-open-write]').forEach(function (button) {
button.addEventListener('click', function (event) { event.preventDefault(); open('write', null, button); });
});
Object.values(dialogs).forEach(function (dialog) {
dialog.addEventListener('cancel', function (event) { event.preventDefault(); close(false); });
dialog.addEventListener('click', function (event) {
if (event.target.closest('[data-modal-close]')) { close(false); return; }
const rect = dialog.getBoundingClientRect();
if (event.target === dialog && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) close(false);
});
});
window.addEventListener('beforeunload', function (event) {
if (hasChanges() || busy()) { event.preventDefault(); event.returnValue = ''; }
});
window.addEventListener('popstate', function (event) {
if (restoringHistory) { restoringHistory = false; return; }
const nextDepth = event.state && event.state.qaModal ? 1 : 0;
if (!canLeave(false)) {
restoringHistory = true;
history.go(depth > nextDepth ? 1 : -1);
return;
}
depth = nextDepth;
const url = new URL(window.location.href);
const view = url.searchParams.get('view');
if (view === 'write' || view === 'detail') mount(view, url.searchParams.get('id'));
else clear(true);
});
const initial = new URL(window.location.href);
const view = initial.searchParams.get('view');
history.replaceState({ qaModal: false }, '', routeURL());
if (view === 'write' || view === 'detail') open(view, initial.searchParams.get('id'));
}
document.addEventListener('DOMContentLoaded', function () {
normalizePageHeading(); bindGlobal();
const list = initList();
if (qs('#composeModal')) initInquiryUI(list);
else { initWrite(); initDetail(); }
});
})();
+1 -8
View File
@@ -1,8 +1 @@
<!doctype html>
<html lang="ko">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>문의 상세 | EG-BIM Q&amp;A</title><link rel="stylesheet" href="../assets/styles.css?v=20261030-owner-1"></head>
<body>
<main class="egbim-page"><section class="page-hero"><div class="hero-inner"><span class="hero-kicker">EG-BIM SUPPORT</span><h1>Q&amp;A</h1><p>EG-BIM 관련 문의하기</p></div><nav class="sub-nav" aria-label="EG-BIM 지원 메뉴"><div class="sub-nav-inner"><a href="index.html" aria-current="page">문의하기(Q&amp;A)</a><a href="#" aria-disabled="true" class="disabled-tab">원격지원</a></div></nav></section>
<section class="page-shell" id="detail"><div class="section-heading"><div><h2>문의 상세</h2><p>문의 내용과 답변을 확인할 수 있습니다.</p></div></div><article class="detail-card"><div class="detail-head"><div class="detail-title-line"><h2><span class="category" data-detail-category>문의</span> <span data-detail-title>문의 제목</span></h2><span class="status review" data-detail-status>문의접수</span></div><div class="detail-meta"><span data-detail-date>2026-09-17</span><span data-detail-author>작성자</span><span data-detail-company>회사</span><span data-detail-department>부서</span><span data-detail-secret hidden>🔒 비밀글</span></div></div><div class="detail-body" data-detail-content></div><div class="attachments"><h3>첨부파일</h3><div data-detail-attachments><span class="help-text">첨부된 파일이 없습니다.</span></div></div><div id="completionNotice" class="completion-notice" hidden><div><strong>처리 완료 안내</strong><p>담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.</p></div><button class="button button-primary" id="completionButton" type="button">완료 확인</button></div><div id="completionDoneNotice" class="completion-done-notice" hidden>완료 처리 된 요청 건입니다.</div><div class="detail-actions"><div class="owner-actions"><button class="button button-outline" id="editButton" type="button" hidden>수정</button><button class="button button-danger" id="deleteButton" type="button" hidden>삭제</button></div><a class="button button-outline" href="index.html">목록</a></div></article><form id="editForm" class="edit-form" hidden><h3>문의 수정</h3><div class="form-row"><label class="form-label" for="editCategory">구분</label><div class="form-control"><select id="editCategory" class="select"><option value="오류문의">오류문의</option><option value="개선문의">개선문의</option><option value="일반문의">일반문의</option></select></div></div><div class="form-row"><label class="form-label" for="editTitle">제목</label><div class="form-control"><input id="editTitle" class="input" maxlength="200" required></div></div><div class="form-row"><label class="form-label" for="editContent">내용</label><div class="form-control column"><textarea id="editContent" class="textarea" maxlength="10000" required></textarea></div></div><div class="form-row"><div class="form-label">설정</div><div class="form-control"><label class="check"><input id="editSecret" type="checkbox"> 비밀글</label></div></div><div class="form-footer"><button class="button button-outline" id="editCancel" type="button">취소</button><button class="button button-primary" type="submit">수정 저장</button></div><div id="editError" class="form-error" role="alert" hidden></div></form><section class="comment-box"><h3>답변 <span id="commentCount">0</span></h3><div id="comments"></div><form id="commentForm" class="comment-form"><label class="sr-only" for="commentContent">댓글 내용</label><textarea id="commentContent" class="textarea" maxlength="5000" placeholder="답변 또는 추가 문의를 입력해주세요" required></textarea><div class="comment-attachment-input"><label class="button button-primary comment-image-button" for="commentImages">이미지 첨부</label><input id="commentImages" type="file" accept="image/*" multiple><div id="commentImagePreview" class="comment-image-preview"></div></div><div class="comment-form-actions"><span id="commentError" class="form-error" role="alert" hidden></span><button class="button button-primary" id="commentSubmit" type="submit">댓글 등록</button></div></form></section><div class="form-footer"><button class="button button-outline" id="backButton" type="button">목록으로</button></div></section>
</main><script src="config.js?v=20260921-3"></script><script src="app.js?v=20261030-owner-1"></script>
</body></html>
<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>S/W 문의 | 바론컨설턴트</title><script src="../assets/inquiry/entry.js?v=20261102-ui-1" defer></script></head><body><p>문의 화면으로 이동합니다. <a href="./">문의 목록</a></p></body></html>
+207 -28
View File
@@ -1,29 +1,208 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="EG-BIM Q&A 문의 게시판">
<title>EG-BIM Q&A | 바론컨설턴트</title>
<link rel="stylesheet" href="../assets/styles.css?v=20261030-owner-1">
</head>
<body>
<main class="qa-list-page">
<section class="page-hero">
<div class="hero-inner"><span class="hero-kicker">EG-BIM SUPPORT</span><h1>Q&amp;A</h1><p>EG-BIM 관련 문의하기</p></div>
<nav class="sub-nav" aria-label="EG-BIM 지원 메뉴"><div class="sub-nav-inner"><a href="index.html" aria-current="page">문의하기(Q&amp;A)</a><a href="#" aria-disabled="true" class="disabled-tab">원격지원</a></div></nav>
</section>
<section class="page-shell">
<div class="qa-list-heading"><h1>문의하기(Q&amp;A)</h1></div>
<form class="filter-panel" id="searchForm">
<div class="filter-row"><div class="chip-group"><label class="chip"><input type="radio" name="category" value="전체" checked>전체</label><label class="chip"><input type="radio" name="category" value="일반문의">일반</label><label class="chip"><input type="radio" name="category" value="개선문의">개선</label><label class="chip"><input type="radio" name="category" value="오류문의">오류</label></div><label class="check"><input type="checkbox" id="onlyMine">내가 작성한 글</label><span class="filter-spacer"></span><div class="search-form"><input id="query" type="search" maxlength="120" placeholder="검색어 입력 (오타·초성 지원)" aria-label="제목·내용·회사·부서·작성자 검색 (오타·초성 지원)"><button class="button search-button" type="submit"><span aria-hidden="true">⌕</span> 검색</button></div></div>
</form>
<p id="searchStatus" class="help-text" role="status" aria-live="polite" hidden></p>
<div class="table-wrap"><table class="qa-table"><caption class="sr-only">EG-BIM 문의 목록</caption><colgroup><col style="width:6%"><col style="width:8%"><col style="width:13%"><col style="width:11%"><col style="width:7%"><col style="width:38%"><col style="width:11%"><col style="width:6%"></colgroup><thead><tr><th>번호</th><th>구분</th><th>회사</th><th>부서</th><th>작성자</th><th>제목</th><th>등록일</th><th>상태</th></tr></thead><tbody id="qaRows"></tbody></table><div class="table-empty" id="tableEmpty">검색 조건에 맞는 문의가 없습니다.</div></div>
<div class="list-footer"><div class="pagination" id="pagination" aria-label="페이지 이동"></div><a class="button button-primary" href="write.html">문의등록</a></div>
</section>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="config.js?v=20260921-3"></script><script src="app.js?v=20261030-owner-1"></script>
</body>
</html>
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="EG-BIM 소프트웨어 문의"><title>S/W 문의 | 바론컨설턴트</title><link rel="stylesheet" href="../assets/styles.css?v=20261102-ui-1"><link rel="stylesheet" href="../assets/inquiry/inquiry.css?v=20261102-ui-1"></head><body>
<main class="inquiry-page">
<section class="inquiry-hero" aria-labelledby="inquiry-title">
<div class="inquiry-hero__copy">
<h1 id="inquiry-title">S/W 문의</h1>
<p>소프트웨어 관련 문의하기</p>
</div>
</section>
<div class="inquiry-workspace">
<div class="inquiry-tabs-shell">
<div class="inquiry-tabs" role="tablist" aria-label="문의할 소프트웨어">
<button type="button" role="tab" id="tab-egbim" aria-controls="inquiry-panel" aria-selected="true" data-service="egbim" aria-label="EG-BIM 문의">
<span class="inquiry-tab-description">BIM 모델</span>
<picture class="inquiry-tab-logo"><source media="(max-width: 600px)" srcset="../assets/inquiry/egbim-symbol.svg" width="49" height="25" /><img src="../assets/inquiry/egbim.svg" alt="EG-BIM" width="150" height="26" /></picture>
</button>
<button type="button" role="tab" id="tab-tova" aria-controls="inquiry-panel" aria-selected="false" aria-disabled="true" disabled tabindex="-1" data-service="tova" aria-label="TOVA 문의">
<span class="inquiry-tab-description">통합 교통 수요분석 <span class="inquiry-coming-soon">준비중</span></span>
<span class="inquiry-tab-logo"><img src="../assets/inquiry/tova.svg" alt="TOVA" width="104" height="26" /></span>
</button>
<button type="button" role="tab" id="tab-gaia" aria-controls="inquiry-panel" aria-selected="false" aria-disabled="true" disabled tabindex="-1" data-service="gaia" aria-label="GAIA 문의">
<span class="inquiry-tab-description">지반조사 <span class="inquiry-coming-soon">준비중</span></span>
<span class="inquiry-tab-logo"><img src="../assets/inquiry/gaia.svg" alt="GAIA" width="82" height="33" /></span>
</button>
</div>
</div>
<section class="inquiry-board" id="inquiry-panel" role="tabpanel" aria-labelledby="tab-egbim">
<div class="inquiry-board__heading">
<h2 id="boardHeading" tabindex="-1">문의 내역</h2>
<a class="inquiry-remote" href="https://939.co.kr/saman/" target="_blank" rel="noopener noreferrer" aria-label="원격지원 (새 창)">
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="13" rx="2" /><path d="M8 21h8M12 17v4m-1-13 4 3-4 3" /></svg>
<span>원격지원</span>
</a>
</div>
<form class="inquiry-controls" role="search" id="searchForm">
<div class="inquiry-filters">
<fieldset class="inquiry-categories">
<legend class="inquiry-sr-only">문의 구분</legend>
<label><input type="radio" name="category" value="전체" checked /><span>전체</span></label>
<label><input type="radio" name="category" value="일반문의" /><span>일반</span></label>
<label><input type="radio" name="category" value="개선문의" /><span>개선</span></label>
<label><input type="radio" name="category" value="오류문의" /><span>오류</span></label>
</fieldset>
<label class="inquiry-mine"><input type="checkbox" name="mine" id="onlyMine" /> 내가 작성한 글</label>
</div>
<div class="inquiry-search">
<label class="inquiry-sr-only" for="query">제목 및 내용 검색</label>
<input type="search" id="query" name="keyword" placeholder="제목 또는 내용 검색" autocomplete="off" maxlength="120" aria-label="제목·내용·회사·부서·작성자 검색 (오타·초성 지원)" />
<button type="submit" class="inquiry-search-button" aria-label="검색"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10" cy="10" r="6.5" /><path d="m15 15 6 6" /></svg></button>
</div>
</form>
<p class="inquiry-scroll-hint">좌우로 밀어 문의 목록을 확인하세요.</p>
<div class="inquiry-table-scroll" tabindex="0" role="region" aria-label="문의 목록. 작은 화면에서는 좌우로 스크롤할 수 있습니다.">
<table class="inquiry-table">
<caption class="inquiry-sr-only"><span data-service-name>EG-BIM</span> 문의 목록</caption>
<colgroup><col class="col-number" /><col class="col-category" /><col class="col-company" /><col class="col-team" /><col class="col-author" /><col /><col class="col-date" /><col class="col-status" /></colgroup>
<thead><tr><th scope="col">번호</th><th scope="col">구분</th><th scope="col">회사</th><th scope="col">부서</th><th scope="col">작성자</th><th scope="col">제목</th><th scope="col">등록일</th><th scope="col">상태</th></tr></thead>
<tbody id="qaRows"></tbody>
</table><div class="table-empty" id="tableEmpty">문의 목록을 불러오는 중입니다.</div>
</div>
<p class="help-text" id="searchStatus" role="status" aria-live="polite" hidden></p>
<nav class="inquiry-pagination" aria-label="문의 목록 페이지" id="pagination"></nav>
<div class="inquiry-board__bottom">
<span id="resultCount" class="help-text" aria-live="polite"></span>
<button type="button" class="inquiry-button inquiry-write" id="writeButton" data-open-write><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m4 15 12-12 5 5L9 20l-6 1 1-6Zm10-10 5 5M4 15l5 5" /></svg>문의등록</button>
</div>
</section>
</div>
</main>
<dialog class="inquiry-dialog" id="composeModal" aria-labelledby="composeHeading"><div data-modal-content></div></dialog>
<dialog class="inquiry-dialog" id="detailModal" aria-labelledby="detailHeading"><div data-modal-content></div></dialog>
<template id="writeTemplate">
<div class="inquiry-dialog__heading"><h2 id="composeHeading" tabindex="-1">문의등록</h2><button type="button" class="inquiry-close" data-modal-close aria-label="문의등록 닫기">×</button></div>
<p class="help-text" data-auth-state>로그인 상태를 확인하고 있습니다.</p><div class="form-error" id="formError" role="alert"></div>
<form id="qaForm" novalidate><div id="writeFields">
<div class="inquiry-form-grid">
<label>서비스<select disabled aria-label="서비스"><option>EG-BIM</option></select></label>
<label for="category">문의 구분<select id="category" required><option value="">선택해주세요</option><option>일반문의</option><option>개선문의</option><option>오류문의</option></select></label>
<label>회사<input id="writeCompany" type="text" readonly placeholder="확인 중"></label>
<label>부서<input id="writeDepartment" type="text" readonly placeholder="확인 중"></label>
<label>작성자<input id="writeAuthor" type="text" readonly placeholder="확인 중"></label>
<label>이메일<input id="writeEmail" type="email" readonly placeholder="확인 중"></label>
</div>
<label class="inquiry-form-wide" for="title">제목<input id="title" type="text" maxlength="100" placeholder="문의 제목을 입력하세요" required></label>
<label class="inquiry-secret"><input id="secret" type="checkbox">비밀글로 등록</label>
<label class="inquiry-form-wide" for="content">내용<textarea id="content" rows="6" placeholder="문의 내용을 입력하세요" required></textarea></label>
<label class="inquiry-form-wide" for="attachment">첨부파일<input id="attachment" type="file" multiple></label>
<p class="help-text">파일당 최대 30MB. 개인정보, 비밀번호, 인증번호 등 민감한 정보는 입력하지 마세요.</p>
</div><div class="inquiry-dialog__actions"><button type="button" class="inquiry-button inquiry-button--light" data-modal-close>취소</button><button type="submit" class="inquiry-button" id="submitButton">등록</button></div></form>
</template>
<template id="detailTemplate">
<section id="detail">
<div class="inquiry-dialog__heading">
<h2 id="detailHeading" data-detail-title tabindex="-1">문의 내용을 불러오는 중입니다.</h2>
<button type="button" class="inquiry-close" data-modal-close aria-label="문의 내용 닫기">×</button>
</div>
<article class="detail-card">
<div class="detail-head">
<div class="inquiry-detail__summary">
<span class="category" data-detail-category>
</span>
<span class="status review" data-detail-status>
</span>
</div>
<div class="detail-meta">
<span data-detail-date>
</span>
<span data-detail-author>
</span>
<span data-detail-company>
</span>
<span data-detail-department>
</span>
<span data-detail-secret hidden>🔒 비밀글</span>
</div>
</div>
<div class="detail-body" data-detail-content>
</div>
<div class="attachments">
<h3>첨부파일</h3>
<div data-detail-attachments>
<span class="help-text">첨부된 파일이 없습니다.</span>
</div>
</div>
<div id="completionNotice" class="completion-notice" hidden>
<div>
<strong>처리 완료 안내</strong>
<p>담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.</p>
</div>
<button class="button button-primary" id="completionButton" type="button">완료 확인</button>
</div>
<div id="completionDoneNotice" class="completion-done-notice" hidden>완료 처리 된 요청 건입니다.</div>
<div class="detail-actions">
<div class="owner-actions">
<button class="button button-outline" id="editButton" type="button" hidden>수정</button>
<button class="button button-danger" id="deleteButton" type="button" hidden>삭제</button>
</div>
</div>
</article>
<form id="editForm" class="edit-form" hidden>
<h3>문의 수정</h3>
<div class="form-row">
<label class="form-label" for="editCategory">구분</label>
<div class="form-control">
<select id="editCategory" class="select">
<option value="오류문의">오류문의</option>
<option value="개선문의">개선문의</option>
<option value="일반문의">일반문의</option>
</select>
</div>
</div>
<div class="form-row">
<label class="form-label" for="editTitle">제목</label>
<div class="form-control">
<input id="editTitle" class="input" maxlength="200" required>
</div>
</div>
<div class="form-row">
<label class="form-label" for="editContent">내용</label>
<div class="form-control column">
<textarea id="editContent" class="textarea" maxlength="10000" required>
</textarea>
</div>
</div>
<div class="form-row">
<div class="form-label">설정</div>
<div class="form-control">
<label class="check">
<input id="editSecret" type="checkbox"> 비밀글</label>
</div>
</div>
<div class="form-footer">
<button class="button button-outline" id="editCancel" type="button">취소</button>
<button class="button button-primary" type="submit">수정 저장</button>
</div>
<div id="editError" class="form-error" role="alert" hidden>
</div>
</form>
<section class="comment-box">
<h3>답변 <span id="commentCount">0</span>
</h3>
<div id="comments">
</div>
<form id="commentForm" class="comment-form">
<label class="sr-only" for="commentContent">댓글 내용</label>
<textarea id="commentContent" class="textarea" maxlength="5000" placeholder="답변 또는 추가 문의를 입력해주세요" required>
</textarea>
<div class="comment-attachment-input">
<label class="button button-primary comment-image-button" for="commentImages">이미지 첨부</label>
<input id="commentImages" type="file" accept="image/*" multiple>
<div id="commentImagePreview" class="comment-image-preview">
</div>
</div>
<div class="comment-form-actions">
<span id="commentError" class="form-error" role="alert" hidden>
</span>
<button class="button button-primary" id="commentSubmit" type="submit">댓글 등록</button>
</div>
</form>
</section>
<div class="inquiry-dialog__actions">
<button class="inquiry-button" type="button" data-modal-close>목록으로</button>
</div>
</section>
</template>
<div id="toast" class="toast" role="status" aria-live="polite"></div><script src="config.js?v=20260921-3"></script><script src="app.js?v=20261102-ui-1"></script>
</body></html>
+1 -8
View File
@@ -1,8 +1 @@
<!doctype html>
<html lang="ko">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>문의 등록 | EG-BIM Q&amp;A</title><link rel="stylesheet" href="../assets/styles.css?v=20261030-owner-1"></head>
<body>
<main class="egbim-page"><section class="page-hero"><div class="hero-inner"><span class="hero-kicker">EG-BIM SUPPORT</span><h1>Q&amp;A</h1><p>EG-BIM 관련 문의하기</p></div><nav class="sub-nav" aria-label="EG-BIM 지원 메뉴"><div class="sub-nav-inner"><a href="index.html" aria-current="page">문의하기(Q&amp;A)</a><a href="#" aria-disabled="true" class="disabled-tab">원격지원</a></div></nav></section>
<section class="page-shell"><div class="section-heading"><div><h2>문의 등록</h2><p>로그인한 사용자 정보로 문의가 등록됩니다.</p></div></div><div class="form-notice" data-auth-state>로그인 상태를 확인하고 있습니다.</div><div class="form-error" id="formError" role="alert"></div><form id="qaForm" novalidate><div class="form-card" id="writeFields"><div class="form-row"><label class="form-label" for="category">구분 <span class="required">*</span></label><div class="form-control"><select class="select" id="category" required><option value="">선택해주세요</option><option>오류문의</option><option>개선문의</option><option>일반문의</option></select></div></div><div class="form-row"><label class="form-label" for="title">제목 <span class="required">*</span></label><div class="form-control"><input class="input" id="title" type="text" maxlength="100" placeholder="문의 제목을 입력해주세요" required></div></div><div class="form-row"><span class="form-label">공개 설정</span><div class="form-control"><label class="check"><input id="secret" type="checkbox"> 비밀글로 등록</label></div></div><div class="form-row"><label class="form-label" for="content">내용 <span class="required">*</span></label><div class="form-control column"><textarea class="textarea" id="content" placeholder="문의 내용을 작성해주세요" required></textarea><span class="help-text">개인정보, 비밀번호, 인증번호 등 민감한 정보는 입력하지 마세요.</span></div></div><div class="form-row"><label class="form-label" for="attachment">첨부파일</label><div class="form-control column"><input class="file-input" id="attachment" type="file" multiple><span class="help-text">파일은 문의와 함께 안전하게 업로드됩니다. 파일당 최대 30MB</span></div></div></div><div class="form-footer"><a class="button button-outline" href="index.html">취소</a><button class="button button-primary" id="submitButton" type="submit">문의 등록</button></div></form></section>
</main><div id="toast" class="toast" role="status" aria-live="polite"></div><script src="config.js?v=20260921-3"></script><script src="app.js?v=20261030-owner-1"></script>
</body></html>
<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>S/W 문의 | 바론컨설턴트</title><script src="../assets/inquiry/entry.js?v=20261102-ui-1" defer></script></head><body><p>문의 화면으로 이동합니다. <a href="./">문의 목록</a></p></body></html>