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 '' + - '' + number + '' + escapeHtml(post.category.replace('문의', '')) + '' + - '' + escapeHtml(post.company) + '' + escapeHtml(post.department) + '' + escapeHtml(post.author) + '' + - '' + (post.secret ? '🔒' : '') + escapeHtml(post.title) + '' + (!isNotice && post.date === '2026-09-17' ? 'N' : '') + '' + - '' + escapeHtml(post.date) + '' + (status ? '' + escapeHtml(status) + '' : '-') + ''; - }).join(''); - qsa('#qaRows tr').forEach(function (row) { row.addEventListener('click', function () { window.location.href = 'detail.html?id=' + encodeURIComponent(row.dataset.id); }); }); - pagination.innerHTML = Array.from({ length: totalPages }, function (_, i) { return ''; }).join(''); - qsa('#pagination button').forEach(function (button) { button.addEventListener('click', function () { page = Number(button.dataset.page); render(); window.scrollTo({ top: 360, behavior: 'smooth' }); }); }); + const statusClass = status === "답변완료" ? "done" : (status === "문의검토" || status === "정밀검토" ? "review" : ""); + const company = getPostCompany(post, user); + return "" + + "" + number + "" + escapeHtml(post.category.replace("문의", "")) + "" + + "" + escapeHtml(company) + "" + escapeHtml(post.department) + "" + escapeHtml(post.author) + "" + + "" + (post.secret ? "🔒" : "") + escapeHtml(post.title) + "" + (!isNotice && post.date === "2026-09-17" ? "N" : "") + "" + + "" + escapeHtml(post.date) + "" + (status ? "" + escapeHtml(status) + "" : "-") + ""; + }).join(""); + qsa("#qaRows tr").forEach(function (row) { row.addEventListener("click", function () { window.location.href = "detail.html?id=" + encodeURIComponent(row.dataset.id); }); }); + pagination.innerHTML = Array.from({ length: totalPages }, function (_, i) { return ""; }).join(""); + qsa("#pagination button").forEach(function (button) { button.addEventListener("click", function () { page = Number(button.dataset.page); render(); window.scrollTo({ top: 360, behavior: "smooth" }); }); }); } - searchForm.addEventListener('submit', function (event) { event.preventDefault(); page = 1; render(); }); - qsa('input[name="category"], #onlyMine').forEach(function (input) { input.addEventListener('change', function () { page = 1; render(); }); }); + searchForm.addEventListener("submit", function (event) { event.preventDefault(); page = 1; render(); }); + qsa("input[name=category], #onlyMine").forEach(function (input) { input.addEventListener("change", function () { page = 1; render(); }); }); render(); - refreshRemoteStatuses(getPosts()).then(function (changed) { if (changed) render(); }); + if (authReady) authReady.then(render); + loadRemotePosts().then(function (result) { + posts = result.posts; + remoteLoaded = true; + render(); + }).catch(function (error) { + console.warn("[QA] feedback list load failed:", error.message); + remoteLoaded = true; + render(); + refreshRemoteStatuses(posts).then(function (changed) { if (changed) render(); }); + }); } function initWrite() { @@ -383,7 +485,7 @@ if (!qs('#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.familyCompany || currentUser.company || '외부 사용자', department: currentUser.department || '-', author: currentUser.name || currentUser.loginId, title: title, date: createdAt.slice(0, 10), createdAt: createdAt, status: '접수', secret: qs('#secret').checked, content: content }; + 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: createdAt.slice(0, 10), createdAt: createdAt, status: '접수', secret: qs('#secret').checked, content: content }; const submitButton = qs('#submitButton'); submitButton.disabled = true; submitButton.textContent = '등록 중...'; @@ -407,7 +509,7 @@ }); } - function initDetail() { + async function initDetail() { const detail = qs('#detail'); if (!detail) return; const detailListLink = qs('.detail-actions > a', detail); @@ -415,7 +517,15 @@ const bottomBackButton = qs('#backButton', detail); if (bottomBackButton && bottomBackButton.parentElement) bottomBackButton.parentElement.remove(); const id = new URLSearchParams(window.location.search).get('id') || 'EG-1048'; - const post = getPosts().find(function (item) { return item.id === id; }); + 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); + } + } if (!post) { qs('[data-detail-title]').textContent = '문의 내용을 찾을 수 없습니다.'; qs('[data-detail-content]').textContent = '목록에 등록된 문의가 없습니다.'; @@ -426,7 +536,8 @@ qs('[data-detail-status]').textContent = normalizeStatus(post.status) || '공지'; qs('[data-detail-date]').textContent = post.date; qs('[data-detail-author]').textContent = post.author; - qs('[data-detail-company]').textContent = post.company; + const detailUser = getAuthUser(); + qs('[data-detail-company]').textContent = detailUser && (detailUser.company || detailUser.tenantName) && post.requesterTenantId && String(detailUser.tenantId) === String(post.requesterTenantId) ? (detailUser.company || detailUser.tenantName) : (post.company || '외부 사용자'); qs('[data-detail-department]').textContent = post.department; qs('[data-detail-content]').textContent = post.content; qs('[data-detail-secret]').hidden = !post.secret; diff --git a/egbim/detail.html b/egbim/detail.html index 11b9602..94d80e3 100644 --- a/egbim/detail.html +++ b/egbim/detail.html @@ -4,5 +4,5 @@
EG-BIM SUPPORT

Q&A

EG-BIM 관련 문의하기

문의 상세

문의 내용과 답변을 확인할 수 있습니다.

문의 문의 제목

문의접수
2026-09-17작성자회사부서

첨부파일

첨부된 파일이 없습니다.
목록

답변 0

-
+ diff --git a/egbim/index.html b/egbim/index.html index 58ebd85..eede6b3 100644 --- a/egbim/index.html +++ b/egbim/index.html @@ -23,6 +23,6 @@
- + diff --git a/egbim/write.html b/egbim/write.html index f6c6799..3b3db2f 100644 --- a/egbim/write.html +++ b/egbim/write.html @@ -4,5 +4,5 @@
EG-BIM SUPPORT

Q&A

EG-BIM 관련 문의하기

문의 등록

로그인한 사용자 정보로 문의가 등록됩니다.

로그인 상태를 확인하고 있습니다.
공개 설정
개인정보, 비밀번호, 인증번호 등 민감한 정보는 입력하지 마세요.
파일은 문의와 함께 안전하게 업로드됩니다. 파일당 최대 30MB
-
+
diff --git a/src/index.js b/src/index.js index 78cb68d..677b935 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,10 @@ export default { if (url.pathname === '/auth/callback') return finishLogin(request, env); if (url.pathname === '/auth/logout') return logout(request, env); if (url.pathname === '/auth/session') return sessionResponse(request, env); - if (url.pathname === '/api/feedbacks') return createFeedback(request, env); + if (url.pathname === '/api/feedbacks') { + if (request.method === 'GET') return listFeedbacks(request, env, url); + return createFeedback(request, env); + } if (/^\/api\/feedbacks\/[^/]+\/comments$/.test(url.pathname)) return feedbackComments(request, env, url); if (/^\/api\/feedbacks\/[^/]+$/.test(url.pathname)) return feedbackDetail(request, env, url); @@ -187,6 +190,18 @@ function requiredAuthConfig(env) { return keys.filter((key) => !env[key]); } +function companyNameFromEmail(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 normalizeClaims(claims) { const profile = claims.profile && typeof claims.profile === 'object' ? claims.profile : {}; const custom = claims.customAttributes || claims.custom_attributes || {}; @@ -195,11 +210,17 @@ function normalizeClaims(claims) { const profilePhone = profile.phone || profile.phone_number || profile.phoneNumber || profile.mobile || profile.mobilePhone || firstProfilePhone?.phoneNumber || firstProfilePhone?.phone_number || firstProfilePhone?.number || firstProfilePhone?.value || firstProfilePhone?.phone || firstProfilePhone || ''; const customPhone = custom.phone || custom.phone_number || custom.phoneNumber || custom.mobile || custom.mobilePhone || ''; const tenants = claims.tenants || claims.tenantIds || custom.tenants || []; - const tenantList = Array.isArray(tenants) ? tenants : Object.keys(tenants).map((key) => ({ id: key, ...(tenants[key] || {}) })); + const tenantList = Array.isArray(tenants) ? tenants : Object.keys(tenants).map((key) => { + const value = tenants[key]; + return value && typeof value === 'object' ? { id: key, ...value } : { id: key, name: value || '' }; + }); const tenantId = claims.tenant_id || claims.tenantId || custom.tenant_id || custom.tenantId || tenantList[0]?.id || tenantList[0]?.tenantId || ''; const tenantIds = tenantList.map((tenant) => typeof tenant === 'string' ? tenant : (tenant.id || tenant.tenantId || tenant.key || '')).filter(Boolean); if (tenantId && !tenantIds.includes(tenantId)) tenantIds.unshift(tenantId); const primaryTenant = tenantList.find((tenant) => typeof tenant !== 'string' && (tenant.id || tenant.tenantId) === tenantId) || tenantList[0] || {}; + const tenantName = claims.tenant_name || claims.tenantName || custom.tenant_name || custom.tenantName || (typeof primaryTenant === 'object' ? (primaryTenant.name || primaryTenant.tenantName || primaryTenant.displayName || primaryTenant.label || '') : ''); + const email = claims.email || profile.email || ''; + const company = companyNameFromEmail(email) || custom.company || claims.company || claims.organization || profile.company || ''; const subject = claims.sub || claims.subject || claims.userId || claims.user_id || ''; return { userUuid: claims.userId || claims.user_id || subject, @@ -211,10 +232,11 @@ function normalizeClaims(claims) { scope: claims.scope || '', roles: claims.roles || claims.roleNames || [], loginId: claims.email || claims.loginId || claims.preferred_username || profile.email || '', - email: claims.email || profile.email || '', + email, name: claims.name || profile.name || claims.display_name || '', phone: claims.phone || claims.phone_number || claims.phoneNumber || claims.mobile || claims.mobilePhone || customPhone || profilePhone, - company: custom.company || claims.company || claims.organization || profile.company || '', + tenantName, + company, familyCompany: custom.familyCompany || custom.family_company || claims.familyCompany || claims.affiliation || '', department: custom.team || custom.department || claims.department || primaryTenant.name || '', position: custom.position || claims.position || profile.position || primaryTenant.position || primaryTenant.title || '', @@ -414,6 +436,45 @@ async function createFeedback(request, env) { return json({ id }); } +async function listFeedbacks(request, env, url) { + if (request.method !== "GET") return json({ error: "method_not_allowed" }, 405, { allow: "GET" }); + const user = await getSession(request, env); + if (!user) return json({ error: "unauthenticated" }, 401); + if (!env.ABC_API_KEY) return json({ error: "feedback_api_not_configured" }, 503); + if (!env.ABC_PROJECT_ID || !env.ABC_CHANNEL_ID) return json({ error: "feedback_target_not_configured" }, 503); + + const apiBaseUrl = String(env.ABC_API_BASE_URL || "https://feedback.hmac.kr").replace(/\/$/, ""); + const endpoint = new URL(apiBaseUrl + "/api/projects/" + encodeURIComponent(env.ABC_PROJECT_ID) + "/channels/" + encodeURIComponent(env.ABC_CHANNEL_ID) + "/feedbacks"); + ["cursor", "searchText", "fieldKey", "issueName"].forEach((key) => { + const value = url.searchParams.get(key); + if (value) endpoint.searchParams.set(key, value); + }); + + const requestedLimit = Number(url.searchParams.get("limit") || 100); + const requestedPage = Number(url.searchParams.get("page") || 1); + endpoint.searchParams.set("limit", String(Number.isFinite(requestedLimit) ? Math.min(Math.max(Math.floor(requestedLimit), 1), 100) : 100)); + endpoint.searchParams.set("page", String(Number.isFinite(requestedPage) ? Math.max(Math.floor(requestedPage), 1) : 1)); + + let response; + try { + response = await fetch(endpoint, { method: "GET", headers: { accept: "application/json", "x-api-key": env.ABC_API_KEY } }); + } catch (error) { + console.error("feedback_list_api_request_failed", error instanceof Error ? error.message : "unknown"); + return json({ error: "feedback_api_unreachable" }, 502); + } + + const result = await response.json().catch(() => ({})); + if (!response.ok) { + console.error("feedback_list_api_rejected", response.status); + const upstreamMessage = String(result.message || result.error || result.code || "").slice(0, 240); + return json({ error: "feedback_api_rejected", message: upstreamMessage || "문의 목록 조회에 실패했습니다.", status: response.status }, response.status >= 500 ? 502 : response.status); + } + + const items = Array.isArray(result) ? result : (result.items || result.data?.items || []); + const meta = result.meta || result.data?.meta || {}; + return json({ items: Array.isArray(items) ? items : [], meta }); +} + async function feedbackComments(request, env, url) { if (request.method !== 'GET' && request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, { allow: 'GET, POST' }); const user = await getSession(request, env);