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:
1 parent
dbf5dc3611
commit
c62630e22d
19 files changed
+1362
-277
No files matched your search
+301
-80
@@ -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
@@ -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&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&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&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
@@ -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&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&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&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
@@ -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&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&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&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>
|
||||
Reference in new issue
Block a user