문의 문의 제목
문의접수담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.
diff --git a/README.md b/README.md index fe105cd..2639632 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ python3 -m http.server 4173 상세 페이지에는 공개 댓글 입력 UI가 있으며, 댓글은 같은 Worker를 통해 ABC의 `feedback_comments`에 저장하고 `is_internal=false`인 공개 댓글만 조회합니다. 댓글 이미지가 있으면 `attachments` multipart 필드로 Worker가 ABC에 전달하며, 응답으로 받은 이미지 URL을 썸네일로 표시합니다. 목록·상세 진입 시에는 ABC의 `feedback_status`를 다시 조회해 관리페이지에서 변경된 상태를 반영합니다. +게시글과 댓글의 수정·삭제 버튼은 로그인 사용자의 기본 requester UUID와 작성자 ID가 일치할 때만 표시합니다. 이름·이메일이 같다는 이유로 표시하지 않으며, 작성자 ID가 없거나 로그인 확인 중·만료·조회 실패 상태이면 숨깁니다. 다른 사람의 게시글에서도 본인이 작성한 댓글의 버튼은 표시합니다. 공통 버튼 CSS는 `hidden` 속성을 덮어쓰지 않습니다. + 목록 검색은 제목·내용·회사·부서·작성자를 대상으로 오타, 띄어쓰기·대소문자 차이, 두 글자 이상의 한글 초성을 처리합니다. 예를 들어 `로그임`, `ㄹㄱㅇ`으로 `로그인`을, `파일첨부`로 `파일 첨부`를 찾을 수 있습니다. 검색 버튼이나 Enter로 실행하며, 여러 단어를 입력하면 각 단어가 모두 일치해야 합니다. 정확한 제목 일치를 우선하고 같은 점수는 기존 목록 순서를 유지합니다. 분류와 `내가 작성한 글` 조건도 함께 적용합니다. 너무 짧은 검색어의 과도한 일치를 줄이기 위해 1~2글자는 정확한 포함 검색을 사용합니다. 오타는 3~5글자에 1회, 6~32글자에 최대 2회의 삽입·삭제·치환·인접 문자 교환을 허용합니다. 긴 단어는 정확한 포함 검색을 사용합니다. 목록은 API가 반환하는 페이지를 순서대로 수집하므로 첫 100건 이후의 글도 검색합니다. 로딩 중과 일부 페이지 조회 실패는 화면에 표시하며, 조회하지 못한 글까지 검색했다고 안내하지 않습니다. diff --git a/assets/styles.css b/assets/styles.css index 67f43f2..30827b6 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -210,6 +210,7 @@ button { cursor: pointer; } .search-form input { width: 100%; min-width: 0; height: 40px; padding: 0 13px; border: 1px solid #d2d2d2; background: #fff; outline: 0; } .search-form input:focus { border-color: var(--navy); } .button { display: inline-flex; align-items: center; justify-content: center; height: 42px; padding: 0 21px; border: 1px solid transparent; border-radius: 3px; font-weight: 500; white-space: nowrap; } +.button[hidden] { display: none !important; } .button-primary { background: var(--navy); color: #fff; } .button-primary:hover { background: #0e315f; } .button-outline { border-color: #bfc5cb; background: #fff; color: #555; } diff --git a/egbim/app.js b/egbim/app.js index 708d8c5..0391b5d 100644 --- a/egbim/app.js +++ b/egbim/app.js @@ -266,13 +266,16 @@ localStorage.setItem(STORAGE_KEY, JSON.stringify(posts)); } - function isPostOwner(post, user) { - if (!post || !user) return false; + function matchesAuthorId(authorId, user) { + if (!user) return false; const normalizeId = function (id) { return String(id || '').trim().toLowerCase(); }; - const postIds = [post.requesterId, post.requesterUuid].map(normalizeId).filter(Boolean); - const userIds = [user.requesterId, user.ssoSubject, user.userUuid].map(normalizeId).filter(Boolean); - if (postIds.length && userIds.length) return postIds.some(function (id) { return userIds.indexOf(id) > -1; }); - return Boolean(post.author && [user.name, user.loginId, user.email].filter(Boolean).indexOf(post.author) > -1); + const ownerId = normalizeId(authorId); + const userId = normalizeId(user.requesterId || user.ssoSubject || user.userUuid); + return Boolean(ownerId && userId && ownerId === userId); + } + + function isPostOwner(post, user) { + return Boolean(post) && matchesAuthorId(post.requesterId || post.requesterUuid, user); } function normalizeStatus(value) { @@ -405,15 +408,15 @@ 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 ssoSubject = raw.ssoSubject || raw.oauthSubject || raw.subject || claims.sub || claims.subject || raw.requesterId || raw.userUuid || raw.userId || sessionStorage.getItem('descopeUserId') || ''; + const userUuid = raw.userUuid || raw.userId || claims.userId || claims.user_id || ssoSubject || 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 = raw.company || custom.company || claims.company || sessionStorage.getItem('company') || getCompanyNameFromEmail(email) || ''; return { userUuid: userUuid, ssoSubject: ssoSubject, - requesterId: ssoSubject, + requesterId: raw.requesterId || ssoSubject, tenantId: tenantId, requesterTenantId: tenantId, tenantIds: tenantIds, @@ -463,7 +466,12 @@ const response = await fetch(config.ssoSessionEndpoint, { credentials: 'include', headers: { Accept: 'application/json' } }); if (!response.ok) throw new Error('SSO 세션 확인에 실패했습니다.'); const body = await response.json(); - const remoteUser = normalizeAuthUser(body.user || body, body.claims || body.scope || {}); + const rawUser = body.user || body; + if (body.authenticated === false || !rawUser || !(rawUser.requesterId || rawUser.ssoSubject || rawUser.userUuid || rawUser.userId || rawUser.subject)) { + resolvedAuthUser = null; + return null; + } + const remoteUser = normalizeAuthUser(rawUser, body.claims || body.scope || {}); if (remoteUser.loginId || remoteUser.ssoSubject) { resolvedAuthUser = remoteUser; return remoteUser; @@ -471,8 +479,8 @@ } catch (error) { console.warn('[QA] SSO session bridge unavailable:', error.message); } - resolvedAuthUser = localUser; - return localUser; + resolvedAuthUser = null; + return null; } function renderAuth(user) { @@ -727,7 +735,7 @@ renderDetailStatus(); qs('[data-detail-date]').textContent = post.dateTime || post.date; qs('[data-detail-author]').textContent = post.author; - let detailUser = getAuthUser(); + 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; @@ -747,20 +755,19 @@ const completionDoneNotice = qs("#completionDoneNotice"); const completionButton = qs("#completionButton"); const renderCompletionNotice = function () { - const owner = isPostOwner(post, detailUser || getAuthUser()); + const owner = isPostOwner(post, detailUser); const completed = normalizeStatus(post.status) === "완료" || post.completionConfirmed; const visible = owner && !completed && post.completionRequested; if (completionNotice) completionNotice.hidden = !visible; if (completionDoneNotice) completionDoneNotice.hidden = !(owner && completed); }; const ownerActions = function () { - const owner = isPostOwner(post, detailUser || getAuthUser()); + const owner = isPostOwner(post, detailUser); if (editButton) editButton.hidden = !owner; if (deleteButton) deleteButton.hidden = !owner; renderCompletionNotice(); }; ownerActions(); - if (authReady) authReady.then(function (user) { detailUser = user || detailUser; ownerActions(); renderCompletionNotice(); comments = renderComments(comments); }); if (completionButton) completionButton.addEventListener("click", async function () { if (!post.completionRequested || post.completionConfirmed) return; completionButton.disabled = true; @@ -890,15 +897,13 @@ let editingCommentId = null; function isCommentOwner(comment, user) { - const authorId = String(comment.authorId || '').trim().toLowerCase(); - const userIds = [user && user.requesterId, user && user.ssoSubject, user && user.userUuid].map(function (id) { return String(id || '').trim().toLowerCase(); }).filter(Boolean); - return Boolean(authorId && userIds.indexOf(authorId) !== -1); + return matchesAuthorId(comment.authorId, user); } function renderComments(comments) { comments = (comments || []).map(normalizeComment).filter(function (comment) { return comment.content; }); qs('#commentCount').textContent = comments.length; - const user = detailUser || getAuthUser(); + const user = detailUser; qs('#comments').innerHTML = comments.length ? comments.map(function (comment) { const owner = Boolean(comment.id) && isCommentOwner(comment, user); const editing = Boolean(editingCommentId) && editingCommentId === comment.id && owner; diff --git a/egbim/detail.html b/egbim/detail.html index bbc0e4d..c718c34 100644 --- a/egbim/detail.html +++ b/egbim/detail.html @@ -1,8 +1,8 @@ -
EG-BIM 관련 문의하기
문의 내용과 답변을 확인할 수 있습니다.
담당자가 처리를 완료했습니다. 내용을 확인하셨다면 완료 확인을 눌러주세요.
답변 0