Q&A
EG-BIM 관련 문의하기
문의 상세
문의 내용과 답변을 확인할 수 있습니다.
diff --git a/assets/styles.css b/assets/styles.css index 76b13cd..b7d46b3 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -276,6 +276,9 @@ button { cursor: pointer; } .attachments h3 { margin: 0 0 8px; font-size: 14px; } .attachment-item { padding: 7px 0; color: #555; font-size: 13px; } .attachments a { color: var(--navy); text-decoration: underline; } +.completion-notice { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 20px 22px 0; padding: 15px 17px; border: 1px solid #f0c45c; background: #fff9e8; color: #5b4500; } +.completion-notice strong { display: block; margin-bottom: 4px; } +.completion-notice p { margin: 0; font-size: 13px; } .detail-actions { display: flex; justify-content: space-between; padding: 23px 22px 0; } .egbim-page #backButton { display: none; } .egbim-page .form-footer:has(#backButton) { display: none; } diff --git a/egbim/app.js b/egbim/app.js index 31f6714..1cdcd4a 100644 --- a/egbim/app.js +++ b/egbim/app.js @@ -45,6 +45,11 @@ return ""; } + function toBoolean(value) { + if (value === true || value === 1) return true; + return ["true", "1", "yes", "y"].indexOf(String(value || "").trim().toLowerCase()) > -1; + } + function normalizeCategory(value) { const raw = String(value || "").trim(); return ({ @@ -60,13 +65,17 @@ function normalizeRemotePost(item) { item = item || {}; const requester = item.requester || item.author || item.user || {}; - const fields = item.fields && typeof item.fields === "object" && !Array.isArray(item.fields) ? item.fields : {}; + const fields = [item.fields, item.metadata, item.meta, item.extra_fields, item.data].find(function (candidate) { return candidate && typeof candidate === "object" && !Array.isArray(candidate); }) || {}; function value(keys) { return firstValue(item, keys) || firstValue(fields, keys) || firstValue(requester, keys); } const createdAt = value(["createdAt", "created_at", "date"]) || new Date().toISOString(); const secret = value(["is_secret", "isSecret", "secret"]); const requesterEmail = value(["requester_email", "requesterEmail", "email"]); + const completionRequested = value(["completion_requested", "completionRequested"]); + const completionConfirmed = value(["completion_confirmed", "completionConfirmed"]); + const completionRequestedAt = value(["completion_requested_at", "completionRequestedAt"]); + const completionConfirmedAt = value(["completion_confirmed_at", "completionConfirmedAt"]); return { id: value(["id", "feedbackId", "feedback_id"]), feedbackId: value(["id", "feedbackId", "feedback_id"]), @@ -82,7 +91,11 @@ date: String(createdAt).slice(0, 10), createdAt: createdAt, status: normalizeStatus(value(["feedback_status", "feedbackStatus", "status_code", "status"])), - secret: secret === true || String(secret).toLowerCase() === "true" || String(secret) === "1", + secret: toBoolean(secret), + completionRequested: toBoolean(completionRequested) || Boolean(completionRequestedAt), + completionRequestedAt: completionRequestedAt, + completionConfirmed: toBoolean(completionConfirmed) || Boolean(completionConfirmedAt), + completionConfirmedAt: completionConfirmedAt, attachments: value(["attachments", "images"]) || [], raw: item }; @@ -356,7 +369,7 @@ async function requestJson(path, options) { const requestPath = String(path).split("?")[0]; - const sameOriginProxy = !config.apiBaseUrl && (requestPath === config.createFeedbackPath || requestPath === "/api/feedbacks" || /^\/api\/feedbacks\/[^\/]+(?:\/comments)?$/.test(requestPath)); + const sameOriginProxy = !config.apiBaseUrl && (requestPath === config.createFeedbackPath || requestPath === "/api/feedbacks" || /^\/api\/feedbacks\/[^\/]+(?:\/comments|\/completion-confirmation)?$/.test(requestPath)); if (!config.apiBaseUrl && !sameOriginProxy) return { local: true }; const target = config.apiBaseUrl ? config.apiBaseUrl.replace(/\/$/, '') + path : path; const response = await fetch(target, Object.assign({ credentials: 'include' }, options)); @@ -529,15 +542,15 @@ if (detailListLink) detailListLink.textContent = '목록으로'; const bottomBackButton = qs('#backButton', detail); if (bottomBackButton && bottomBackButton.parentElement) bottomBackButton.parentElement.remove(); - const id = new URLSearchParams(window.location.search).get('id') || 'EG-1048'; - let post = getPosts().find(function (item) { return item.id === id; }); - if (!post) { - try { - const result = await requestJson("/api/feedbacks/" + encodeURIComponent(id), { method: "GET" }); - post = normalizeRemotePost(result.feedback || result.data || result); - } catch (error) { - console.warn("[QA] feedback detail load failed:", error.message); - } + const id = new URLSearchParams(window.location.search).get("id") || "EG-1048"; + const storedPost = getPosts().find(function (item) { return item.id === id; }); + let post = storedPost; + try { + const result = await requestJson("/api/feedbacks/" + encodeURIComponent(id), { method: "GET" }); + const remotePost = normalizeRemotePost(result.feedback || result.data || result); + if (remotePost.id) post = Object.assign({}, storedPost || {}, remotePost); + } catch (error) { + console.warn("[QA] feedback detail load failed:", error.message); } if (!post) { qs('[data-detail-title]').textContent = '문의 내용을 찾을 수 없습니다.'; @@ -546,10 +559,18 @@ } qs('[data-detail-category]').textContent = post.category; qs('[data-detail-title]').textContent = post.title; - qs('[data-detail-status]').textContent = normalizeStatus(post.status) || '공지'; + const renderDetailStatus = function () { + const statusElement = qs("[data-detail-status]"); + const status = normalizeStatus(post.status) || "공지"; + if (!statusElement) return; + statusElement.textContent = status; + statusElement.classList.toggle("done", status === "완료"); + statusElement.classList.toggle("review", status !== "완료"); + }; + renderDetailStatus(); qs('[data-detail-date]').textContent = post.date; qs('[data-detail-author]').textContent = post.author; - const detailUser = getAuthUser(); + let detailUser = getAuthUser(); qs('[data-detail-company]').textContent = getPostCompanyLabel(post, detailUser); qs('[data-detail-department]').textContent = post.department; qs('[data-detail-content]').textContent = post.content; @@ -563,13 +584,41 @@ const deleteButton = qs('#deleteButton'); const editForm = qs('#editForm'); const editError = qs('#editError'); + const completionNotice = qs("#completionNotice"); + const completionButton = qs("#completionButton"); + const renderCompletionNotice = function () { + const owner = isPostOwner(post, detailUser || getAuthUser()); + const visible = owner && post.completionRequested && !post.completionConfirmed; + if (completionNotice) completionNotice.hidden = !visible; + }; const ownerActions = function () { - const owner = isPostOwner(post, getAuthUser()); + const owner = isPostOwner(post, detailUser || getAuthUser()); if (editButton) editButton.hidden = !owner; if (deleteButton) deleteButton.hidden = !owner; + renderCompletionNotice(); }; ownerActions(); - if (authReady) authReady.then(ownerActions); + if (authReady) authReady.then(function (user) { detailUser = user || detailUser; ownerActions(); renderCompletionNotice(); }); + if (completionButton) completionButton.addEventListener("click", async function () { + if (!post.completionRequested || post.completionConfirmed) return; + completionButton.disabled = true; + completionButton.textContent = "확인 처리 중..."; + try { + await requestJson("/api/feedbacks/" + encodeURIComponent(post.id) + "/completion-confirmation", { method: "POST" }); + post.completionRequested = false; + post.completionConfirmed = true; + post.status = "완료"; + updateStoredPost(post); + renderDetailStatus(); + renderCompletionNotice(); + showToast("문의가 완료 처리되었습니다."); + } catch (error) { + showToast(error.message || "문의 완료 확인에 실패했습니다."); + } finally { + completionButton.disabled = false; + completionButton.textContent = "완료 확인"; + } + }); if (editButton && editForm) editButton.addEventListener('click', function () { qs('#editCategory').value = post.category || '일반문의'; qs('#editTitle').value = post.title || ''; @@ -741,7 +790,7 @@ }).join(''); }); refreshRemoteStatus(post).then(function (changed) { - if (changed) qs('[data-detail-status]').textContent = normalizeStatus(post.status) || '공지'; + if (changed) renderDetailStatus(); }); } diff --git a/egbim/detail.html b/egbim/detail.html index a3264ab..2f3a6a2 100644 --- a/egbim/detail.html +++ b/egbim/detail.html @@ -1,8 +1,8 @@ -
EG-BIM 관련 문의하기
문의 내용과 답변을 확인할 수 있습니다.
문의 내용과 답변을 확인할 수 있습니다.
담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.
답변 0