From 0d97b85e08d2d709a66287d29cb1745d9319d947 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:16:27 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=9E=AC?= =?UTF-8?q?=EC=8B=9C=EB=8F=84=20=EB=B2=84=ED=8A=BC=EC=97=90=20aria-busy=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비동기 네트워크 요청을 시작하는 버튼에 명시적인 `aria-busy="true"` 상태를 적용하고, 버튼 내부 원래의 DOM 요소를 보존 및 복원하기 위해 `viewer.js`의 관련 기능을 업데이트했습니다. 1. `setLoading` 실행 시 `el.retryBtn`에 `aria-busy` 적용 및 기존 `childNodes` 캐싱. 2. `showError` 및 `poll` 성공 시 `aria-busy` 제거 및 기존 `childNodes` 복원. 3. 관련 학습 내용을 `.jules/palette.md`에 기록. --- .jules/palette.md | 4 ++++ src/main/resources/static/assets/viewer/viewer.js | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59b2..6150562b9 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -13,3 +13,7 @@ ## 2026-07-13 - Async Table Actions UX **Learning:** Adding explicit loading and disabled states to table action buttons that invoke asynchronous processes helps prevent redundant API calls and visually assures the user that their request is being handled. **Action:** Consistently apply `disabled` state and `Loading...` text changes to inline table action buttons linked to async workflows, and carefully preserve underlying DOM structures with `Array.from(btn.childNodes)` during the loading cycle to avoid rendering regressions. + +## 2024-08-17 - 비동기 버튼 로딩 시 명시적 aria-busy 속성 적용 및 상태 복원 +**Learning:** 단독으로 작동하는 비동기 액션 버튼은 로딩 중일 때 시각적인 변화뿐만 아니라 스크린 리더 등 보조 기기에 정확한 상태를 전달하기 위해 동적으로 `aria-busy="true"` 속성을 부여해야 합니다. 또한, 로딩이 완료된 후 버튼 내부의 원래 DOM 구조를 안전하게 복원하기 위해 `innerHTML` 대신 `childNodes`를 저장해 두고 `replaceChildren`으로 복원하는 것이 권장됩니다. +**Action:** 비동기 네트워크 요청을 수행하는 버튼의 경우, 시작 시 `disabled` 설정과 함께 `aria-busy="true"`를 부여하고 원본 `childNodes`를 별도로 저장하여, 종료 시 상태 복원 및 `aria-busy` 속성을 제거(`removeAttribute`)하도록 일관되게 구현합니다. diff --git a/src/main/resources/static/assets/viewer/viewer.js b/src/main/resources/static/assets/viewer/viewer.js index a998675c7..ea3c59102 100644 --- a/src/main/resources/static/assets/viewer/viewer.js +++ b/src/main/resources/static/assets/viewer/viewer.js @@ -19,6 +19,7 @@ const el = { }; let pdfJsModulePromise; +let originalRetryBtnNodes = null; function getMetaContent(name) { const meta = document.querySelector(`meta[name="${name}"]`); @@ -55,6 +56,10 @@ function setLoading(message) { el.liveStatus.textContent = message; el.preview.setAttribute("aria-busy", "true"); el.retryBtn.disabled = true; + el.retryBtn.setAttribute("aria-busy", "true"); + if (!originalRetryBtnNodes) { + originalRetryBtnNodes = Array.from(el.retryBtn.childNodes); + } el.retryBtn.textContent = "Refreshing..."; } @@ -65,7 +70,10 @@ function showError(message) { el.preview.setAttribute("aria-busy", "false"); el.errorTitle.focus(); el.retryBtn.disabled = false; - el.retryBtn.textContent = "Refresh"; + el.retryBtn.removeAttribute("aria-busy"); + if (originalRetryBtnNodes) { + el.retryBtn.replaceChildren(...originalRetryBtnNodes); + } } function clearPreview() { @@ -274,7 +282,10 @@ async function poll(docId, abortSignal) { el.preview.setAttribute("aria-busy", "false"); el.liveStatus.textContent = "Ready."; el.retryBtn.disabled = false; - el.retryBtn.textContent = "Refresh"; + el.retryBtn.removeAttribute("aria-busy"); + if (originalRetryBtnNodes) { + el.retryBtn.replaceChildren(...originalRetryBtnNodes); + } } catch (_error) { if (abortSignal.aborted) { return;