Q&A
EG-BIM 관련 문의하기
문의 상세
문의 내용과 답변을 확인할 수 있습니다.
문의 상세
문의 내용과 답변을 확인할 수 있습니다.
diff --git a/assets/styles.css b/assets/styles.css index 4e8cc1c..d27d4df 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -274,6 +274,11 @@ button { cursor: pointer; } .attachment-item { padding: 7px 0; color: #555; font-size: 13px; } .attachments a { color: var(--navy); text-decoration: underline; } .detail-actions { display: flex; justify-content: space-between; padding: 23px 22px 0; } +.owner-actions { display: flex; gap: 8px; } +.button-danger { border-color: #c43d3d; background: #c43d3d; color: #fff; } +.button-danger:hover { border-color: #a52f2f; background: #a52f2f; } +.edit-form { padding-top: 24px; border-top: 2px solid #242424; } +.edit-form h3 { margin: 0; padding: 0 22px 18px; font-size: 18px; } .comment-box { margin-top: 54px; } .comment-box h3 { padding-bottom: 15px; margin: 0; border-bottom: 2px solid #222; font-size: 17px; } .comment { padding: 19px 10px; border-bottom: 1px solid var(--line); } @@ -282,9 +287,9 @@ button { cursor: pointer; } .comment-empty { padding: 32px; border-bottom: 1px solid var(--line); color: #999; text-align: center; } .comment-form { padding: 20px 10px 0; } .comment-form .textarea { min-height: 120px; } -.comment-attachment-input { padding-top: 14px; color: #555; font-size: 13px; } -.comment-attachment-input label { display: block; margin-bottom: 8px; font-weight: 600; } -.comment-attachment-input input { max-width: 100%; } +.comment-attachment-input { padding-top: 14px; } +.comment-attachment-input input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); } +.comment-image-button { height: 34px; padding: 0 13px; font-size: 13px; } .comment-image-preview, .comment-attachments-list { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .comment-image-preview-item { width: 104px; overflow: hidden; border: 1px solid #ddd; background: #fafafa; } .comment-image-preview-item img { display: block; width: 104px; height: 78px; object-fit: cover; } @@ -332,7 +337,7 @@ button { cursor: pointer; } .qa-list-page .sub-nav a[aria-current='page'], .egbim-page .sub-nav a[aria-current='page'] { background: rgba(0, 121, 116, .5); color: #ffc400; } .qa-list-page .sub-nav a.disabled-tab, .egbim-page .sub-nav a.disabled-tab { cursor: default; } .qa-list-page .sub-nav a.disabled-tab:hover, .egbim-page .sub-nav a.disabled-tab:hover { background: transparent; color: #fff; } -.qa-list-page .page-shell { max-width: none; padding: 51px 10px 62px; } +.qa-list-page .page-shell { width: calc(100% - 80px); max-width: 1200px; padding: 51px 0 62px; margin-right: auto; margin-left: auto; } .egbim-page .page-shell { padding-top: 51px; } .qa-list-heading { margin: 0 0 40px; text-align: center; } .qa-list-heading h1 { margin: 0; font-size: 30px; line-height: 1.35; letter-spacing: -.06em; } @@ -380,7 +385,7 @@ button { cursor: pointer; } .qa-list-page .page-hero p, .egbim-page .page-hero p { font-size: 16px; } .qa-list-page .sub-nav-inner, .egbim-page .sub-nav-inner { max-width: none; } .qa-list-page .sub-nav a, .egbim-page .sub-nav a { padding: 14px 10px; font-size: 14px; } - .qa-list-page .page-shell { padding: 39px 14px 55px; } + .qa-list-page .page-shell { width: 100%; max-width: none; padding: 39px 14px 55px; } .qa-list-heading { margin-bottom: 28px; } .qa-list-heading h1 { font-size: 26px; } .qa-list-page .filter-row { flex-wrap: wrap; } diff --git a/docs/qa-feedback-api-integration-task.md b/docs/qa-feedback-api-integration-task.md index 61ce642..b33561d 100644 --- a/docs/qa-feedback-api-integration-task.md +++ b/docs/qa-feedback-api-integration-task.md @@ -85,6 +85,14 @@ - [x] 상세페이지 진입 시 최신 상태 재조회 - [ ] 실제 관리페이지에서 상태 변경 후 Q&A 화면 반영 테스트 +## 5차 범위: 작성자 문의 수정·삭제 + +- [x] 작성자 본인 확인 후 상세페이지 수정·삭제 버튼 표시 +- [x] Worker `PATCH /api/feedbacks/{feedbackId}` 프록시 추가 +- [x] Worker `DELETE /api/feedbacks/{feedbackId}` 프록시 추가 +- [x] 제목·내용·카테고리·비밀글 수정 UI 추가 +- [ ] 실제 배포 환경에서 본인/타인 수정·삭제 권한 테스트 + ## 보안 요구사항 - [x] API Key를 정적 JavaScript, HTML, `egbim/config.js`에 넣지 않음 diff --git a/egbim/app.js b/egbim/app.js index cc8975f..e98b433 100644 --- a/egbim/app.js +++ b/egbim/app.js @@ -46,6 +46,19 @@ localStorage.setItem(STORAGE_KEY, JSON.stringify(posts)); } + function removeStoredPost(id) { + const posts = readJson(STORAGE_KEY, []).filter(function (post) { return post.id !== id; }); + localStorage.setItem(STORAGE_KEY, JSON.stringify(posts)); + } + + function isPostOwner(post, user) { + if (!post || !user) return false; + const postIds = [post.requesterId, post.requesterUuid].filter(Boolean); + const userIds = [user.requesterId, user.ssoSubject, user.userUuid].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); + } + function normalizeStatus(value) { if (value && typeof value === 'object') value = value.code || value.key || value.value || value.name || ''; const raw = String(value || '').trim(); @@ -365,7 +378,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, 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.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 submitButton = qs('#submitButton'); submitButton.disabled = true; submitButton.textContent = '등록 중...'; @@ -413,6 +426,83 @@ if (attachmentBox) attachmentBox.innerHTML = attachments.length ? attachments.map(function (attachment) { return '
'; }).join('') : '첨부된 파일이 없습니다.'; + const editButton = qs('#editButton'); + const deleteButton = qs('#deleteButton'); + const editForm = qs('#editForm'); + const editError = qs('#editError'); + const ownerActions = function () { + const owner = isPostOwner(post, getAuthUser()); + if (editButton) editButton.hidden = !owner; + if (deleteButton) deleteButton.hidden = !owner; + }; + ownerActions(); + if (authReady) authReady.then(ownerActions); + 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); + editError.hidden = true; + editError.style.display = 'none'; + editForm.hidden = false; + editForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + const editCancel = qs('#editCancel'); + if (editCancel && editForm) editCancel.addEventListener('click', function () { editForm.hidden = true; }); + 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; + if (!title || !content || !category) { + editError.textContent = '구분, 제목, 내용을 모두 입력해주세요.'; + editError.hidden = false; + editError.style.display = 'block'; + return; + } + const saveButton = qs('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' }) + }); + post.category = category; + post.title = title; + post.content = content; + post.secret = qs('#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; + editForm.hidden = true; + showToast('문의가 수정되었습니다.'); + } catch (error) { + editError.textContent = error.message || '문의 수정에 실패했습니다.'; + editError.hidden = false; + editError.style.display = 'block'; + } finally { + saveButton.disabled = false; + saveButton.textContent = '수정 저장'; + } + }); + if (deleteButton) deleteButton.addEventListener('click', async function () { + if (!window.confirm('이 문의를 삭제하시겠습니까?')) return; + deleteButton.disabled = true; + try { + await requestJson('/api/feedbacks/' + encodeURIComponent(post.id), { method: 'DELETE' }); + removeStoredPost(post.id); + window.location.href = 'index.html'; + } catch (error) { + deleteButton.disabled = false; + showToast(error.message || '문의 삭제에 실패했습니다.'); + } + }); function safeMediaUrl(value) { value = String(value || ''); return /^(https?:\/\/|\/|blob:)/i.test(value) ? value : ''; diff --git a/egbim/detail.html b/egbim/detail.html index 68295b9..8d776c9 100644 --- a/egbim/detail.html +++ b/egbim/detail.html @@ -3,6 +3,6 @@EG-BIM 관련 문의하기
문의 내용과 답변을 확인할 수 있습니다.
문의 내용과 답변을 확인할 수 있습니다.
답변 0