Q&A
EG-BIM 관련 문의하기
문의 상세
문의 내용과 답변을 확인할 수 있습니다.
diff --git a/assets/styles.css b/assets/styles.css index 95a76de..76b13cd 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -221,11 +221,12 @@ button { cursor: pointer; } .qa-table { width: 100%; min-width: 850px; border-collapse: collapse; table-layout: fixed; } .qa-table th { height: 48px; padding: 0 12px; border-bottom: 1px solid #252525; font-weight: 500; } .qa-table td { height: 62px; padding: 0 12px; border-bottom: 1px solid var(--line); color: #5c5c5c; text-align: center; } -.qa-table tbody tr { cursor: pointer; transition: background .15s ease; } -.qa-table tbody tr:hover { background: #fafafa; } +.qa-table tbody tr { cursor: pointer; background: #fff; transition: background .15s ease; } +.qa-table tbody tr:hover { background: #fff; } .qa-table .subject { overflow: hidden; color: #222; text-align: left; text-overflow: ellipsis; white-space: nowrap; } +.qa-table .subject-link { display: block; overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } .subject-link { font-weight: 500; } -.notice-row td { background: #fcfcf8; font-weight: 500; } +.notice-row td { background: #fff; font-weight: 500; } .category { display: inline-flex; align-items: center; justify-content: center; min-width: 66px; padding: 3px 7px; border-radius: 2px; background: #f0f4fb; color: var(--navy); font-size: 12px; } .category.notice { background: #fff6e7; color: #9b6915; } .lock { display: inline-block; margin-right: 6px; color: #888; font-size: 12px; } @@ -369,17 +370,17 @@ button { cursor: pointer; } .qa-list-page .table-wrap { width: 100%; max-width: 100%; overflow: hidden; border-top: 2px solid #111; } .qa-list-page .qa-table { display: table; width: 100%; max-width: 100%; min-width: 0 !important; table-layout: fixed; } .qa-list-page .qa-table th { height: 48px; padding: 0 10px; overflow: hidden; border-bottom: 1px solid #111; color: #111; font-size: 14px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } -.qa-list-page .qa-table td { height: 58px; padding: 0 10px; overflow: hidden; border-bottom: 1px solid #ededed; background: #f3f3f3; color: #3e4650; font-size: 14px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } +.qa-list-page .qa-table td { height: 58px; padding: 0 10px; overflow: hidden; border-bottom: 1px solid #ededed; background: #fff; color: #3e4650; font-size: 14px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } .qa-list-page .qa-table tbody tr:hover { background: transparent; } -.qa-list-page .qa-table tbody tr:hover td { background: #ededed; } +.qa-list-page .qa-table tbody tr:hover td { background: #fff; } .qa-list-page .qa-table .subject { color: #39434f; } .qa-list-page .qa-table .subject-link { font-weight: 500; } .qa-list-page .qa-table .category-cell { white-space: nowrap; } .qa-list-page .qa-table .category, .qa-list-page .qa-table .status { padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; font-size: inherit; font-weight: inherit; } .qa-list-page .qa-table .status { min-width: 0; } -.qa-list-page .qa-table .notice-row td { background: #f3f3f3; color: #3e4650; } -.qa-list-page .table-empty { background: #f3f3f3; } +.qa-list-page .qa-table .notice-row td { background: #fff; color: #3e4650; } +.qa-list-page .table-empty { background: #fff; } .qa-list-page .list-footer { position: relative; min-height: 42px; margin-top: 25px; } .qa-list-page .list-footer .pagination { margin: 0; } .qa-list-page .list-footer > .button { position: absolute; top: 0; right: 0; height: 42px; padding: 0 25px; background: #000; color: #fff; font-size: 14px; font-weight: 700; } diff --git a/egbim/app.js b/egbim/app.js index 27d111e..74cdd38 100644 --- a/egbim/app.js +++ b/egbim/app.js @@ -32,6 +32,62 @@ return readJson(STORAGE_KEY, []).filter(function (post) { return post.category !== '공지사항'; }); } + function firstValue(source, keys) { + if (!source || typeof source !== "object") return ""; + for (let index = 0; index < keys.length; index += 1) { + const value = source[keys[index]]; + if (value !== undefined && value !== null && value !== "") return value; + } + return ""; + } + + function normalizeCategory(value) { + const raw = String(value || "").trim(); + return ({ + ERROR_QNA: "오류문의", + IMPROVEMENT_QNA: "개선문의", + GENERAL_QNA: "일반문의", + 오류: "오류문의", + 개선: "개선문의", + 일반: "일반문의" + })[raw.toUpperCase()] || raw || "일반문의"; + } + + 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 : {}; + 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"]); + return { + id: value(["id", "feedbackId", "feedback_id"]), + feedbackId: value(["id", "feedbackId", "feedback_id"]), + requesterId: value(["requester_id", "requesterId", "requester_uuid"]) || firstValue(requester, ["id", "uuid", "userId"]), + requesterTenantId: value(["requester_tenant_id", "requesterTenantId", "tenant_id", "tenantId"]), + category: normalizeCategory(value(["Category", "category"])), + company: value(["requester_company", "company", "organization", "tenant_name", "tenantName", "requester_affiliation"]), + department: value(["requester_department", "department", "team"]) || "-", + author: value(["requester_name", "author_name", "authorName", "name", "author"]) || "작성자", + title: value(["title", "subject"]) || "제목 없음", + content: value(["contents", "content", "body"]) || "", + 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", + attachments: value(["attachments", "images"]) || [], + raw: item + }; + } + + async function loadRemotePosts() { + const result = await requestJson("/api/feedbacks?limit=100&page=1", { method: "GET" }); + const items = Array.isArray(result) ? result : (result.items || result.data && result.data.items || []); + return { posts: (Array.isArray(items) ? items : []).map(normalizeRemotePost).filter(function (post) { return post.id; }), meta: result.meta || result.data && result.data.meta || {} }; + } + function savePost(post) { const posts = readJson(STORAGE_KEY, []); posts.unshift(post); @@ -139,6 +195,23 @@ } catch (error) { return {}; } } + function getTenantName(tenant) { + if (!tenant || typeof tenant !== 'object') return ''; + return tenant.name || tenant.tenantName || tenant.displayName || tenant.label || tenant.nameKo || tenant.koreanName || ''; + } + + function getCompanyNameFromEmail(email) { + const domain = String(email || '').split('@').pop().trim().toLowerCase(); + return ({ + 'samaneng.com': '삼안', + 'hanmaceng.co.kr': '한맥기술', + 'brsw.kr': '바론그룹', + 'jangheon.co.kr': '장헌산업', + 'jangheon.com': '(주)장헌', + 'hallasanup.com': '한라산업개발' + })[domain] || ''; + } + function collectSsoClaims() { const storedClaims = parseJson( sessionStorage.getItem('ssoClaims') || sessionStorage.getItem('baronClaims') || sessionStorage.getItem('claims'), @@ -159,9 +232,13 @@ const tenantId = raw.tenantId || raw.tenant_id || custom.tenantId || custom.tenant_id || claims.tenantId || claims.tenant_id || (tenantList[0] && (tenantList[0].id || tenantList[0].tenantId)) || ''; const tenantIds = tenantList.map(function (tenant) { return typeof tenant === 'string' ? tenant : (tenant.id || tenant.tenantId || tenant.key || ''); }).filter(Boolean); if (tenantId && tenantIds.indexOf(tenantId) === -1) tenantIds.unshift(tenantId); + const primaryTenant = tenantList.find(function (tenant) { return typeof tenant !== 'string' && (tenant.id || tenant.tenantId) === tenantId; }) || tenantList[0] || {}; + const tenantName = raw.tenantName || raw.tenant_name || custom.tenantName || custom.tenant_name || claims.tenantName || claims.tenant_name || getTenantName(primaryTenant); const ssoSubject = raw.ssoSubject || raw.oauthSubject || raw.subject || claims.sub || claims.subject || raw.userId || sessionStorage.getItem('descopeUserId') || ''; const userUuid = raw.userUuid || raw.userId || claims.userId || claims.user_id || sessionStorage.getItem('descopeUserId') || ''; const loginId = raw.loginId || (raw.loginIds || [])[0] || raw.email || claims.email || claimProfile.email || sessionStorage.getItem('loginId') || readCookie('descope_login_id') || ''; + const email = raw.email || claims.email || claimProfile.email || sessionStorage.getItem('loginId') || ''; + const company = getCompanyNameFromEmail(email) || custom.company || raw.company || claims.company || sessionStorage.getItem('company') || ''; return { userUuid: userUuid, ssoSubject: ssoSubject, @@ -172,10 +249,11 @@ scope: raw.scope || claims.scope || sessionStorage.getItem('ssoScope') || '', roles: raw.roles || raw.roleNames || claims.roles || claims.roleNames || [], loginId: loginId, - email: raw.email || claims.email || claimProfile.email || sessionStorage.getItem('loginId') || '', + email: email, name: raw.name || claims.name || claimProfile.name || sessionStorage.getItem('userName') || readCookie('descope_user_name') || '', phone: raw.phone || raw.phoneNumber || claims.phone || claims.phone_number || sessionStorage.getItem('phone') || '', - company: custom.company || raw.company || claims.company || sessionStorage.getItem('company') || '', + tenantName: tenantName, + company: company, familyCompany: custom.familyCompany || custom.family_company || raw.familyCompany || sessionStorage.getItem('familyCompany') || '', department: custom.team || custom.department || raw.department || claims.department || sessionStorage.getItem('team') || '', rawClaims: claims @@ -260,7 +338,8 @@ } async function requestJson(path, options) { - const sameOriginProxy = !config.apiBaseUrl && (path === config.createFeedbackPath || /^\/api\/feedbacks\/[^/]+(?:\/comments)?$/.test(path)); + const requestPath = String(path).split("?")[0]; + const sameOriginProxy = !config.apiBaseUrl && (requestPath === config.createFeedbackPath || requestPath === "/api/feedbacks" || /^\/api\/feedbacks\/[^\/]+(?:\/comments)?$/.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)); @@ -313,53 +392,76 @@ } function initList() { - const tableBody = qs('#qaRows'); + const tableBody = qs("#qaRows"); if (!tableBody) return; - const searchForm = qs('#searchForm'); - const empty = qs('#tableEmpty'); - const resultCount = qs('#resultCount'); - const pagination = qs('#pagination'); + const searchForm = qs("#searchForm"); + const empty = qs("#tableEmpty"); + const resultCount = qs("#resultCount"); + const pagination = qs("#pagination"); let page = 1; + let posts = getPosts(); + let remoteLoaded = false; + + function getPostCompany(post, user) { + const company = user && (user.company || user.tenantName); + if (company && post.requesterTenantId && user.tenantId && String(post.requesterTenantId) === String(user.tenantId)) return company; + return post.company || "외부 사용자"; + } function render() { - const query = (qs('#query').value || '').trim().toLowerCase(); - const selectedCategory = qs('input[name="category"]:checked'); - const categoryFilter = selectedCategory ? selectedCategory.value : '전체'; - const onlyMine = qs('#onlyMine').checked; + const query = (qs("#query").value || "").trim().toLowerCase(); + const selectedCategory = qs("input[name=category]:checked"); + const categoryFilter = selectedCategory ? selectedCategory.value : "전체"; + const onlyMine = qs("#onlyMine").checked; const user = getAuthUser(); - const filtered = getPosts().filter(function (post) { - const matchesCategory = categoryFilter === '전체' || categoryFilter === post.category; - const haystack = [post.title, post.content, post.company, post.department, post.author].join(' ').toLowerCase(); + const filtered = posts.filter(function (post) { + const matchesCategory = categoryFilter === "전체" || categoryFilter === post.category; + const company = getPostCompany(post, user); + const haystack = [post.title, post.content, company, post.department, post.author].join(" ").toLowerCase(); const matchesQuery = !query || haystack.indexOf(query) > -1; - const matchesMine = !onlyMine || (user && (post.author === user.name || post.author === user.loginId)); + const matchesMine = !onlyMine || isPostOwner(post, user); return matchesCategory && matchesQuery && matchesMine; }); const pageSize = config.pageSize || 10; const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); page = Math.min(page, totalPages); const rows = filtered.slice((page - 1) * pageSize, page * pageSize); - if (resultCount) resultCount.textContent = '총 ' + filtered.length + '건'; - empty.style.display = rows.length ? 'none' : 'block'; + if (resultCount) resultCount.textContent = "총 " + filtered.length + "건"; + if (empty) { + empty.textContent = remoteLoaded || posts.length ? "검색 조건에 맞는 문의가 없습니다." : "문의 목록을 불러오는 중입니다."; + empty.style.display = rows.length ? "none" : "block"; + } tableBody.innerHTML = rows.map(function (post, index) { - const isNotice = post.category === '공지사항'; - const number = isNotice ? '공지' : filtered.length - ((page - 1) * pageSize + index); + 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' : ''); - return '
EG-BIM 관련 문의하기
문의 내용과 답변을 확인할 수 있습니다.
답변 0