From f4c6617059df65e2b2085f8618946cc03b6b4e3a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:54:57 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=91=EC=97=85=20=EC=A4=91=20`aria-busy`=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비동기 작업(`submitDocument`, `loadDemoData`, `refreshKpiEvidence`, `retryActiveJob`, `Details` 핸들러) 시 스크린 리더 등 보조 기술이 로딩 상태를 올바르게 인지할 수 있도록 명시적으로 `aria-busy="true"`를 부여함. - `finally` 블록에서 안전하게 속성을 제거하여 원상 복구함. - `demo.js`의 버튼 클릭 로딩 상태에 관한 웹 접근성 향상. --- .jules/palette.md | 4 ++++ src/main/resources/static/assets/viewer/demo.js | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59..45b8169 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-07-20 - 비동기 작업 중 `aria-busy` 속성 활용 +**Learning:** 비동기 작업 중에 로딩 상태를 시각적으로만 표시하는 것을 넘어서 `aria-busy="true"` 속성을 동적으로 추가하면 스크린 리더 및 보조 기술에 상태를 정확히 전달할 수 있어 웹 접근성이 크게 향상됩니다. +**Action:** 버튼이나 요소가 비동기 통신을 대기하는 동안 상태 변경이 일어날 때 항상 `aria-busy` 속성을 설정하고 작업이 완료되는 `finally` 블록에서 안전하게 제거하도록 합니다. diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 51466a8..e24e426 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -147,10 +147,12 @@ function renderHistory(history = loadHistory()) { actionsCell.appendChild(createActionButton("Details", (e) => { const btn = e.currentTarget; const initialChildren = Array.from(btn.childNodes); + btn.setAttribute("aria-busy", "true"); btn.disabled = true; btn.textContent = "Loading..."; openJobDetail(job).finally(() => { btn.replaceChildren(...initialChildren); + btn.removeAttribute("aria-busy"); btn.disabled = false; }); })); @@ -298,6 +300,7 @@ async function retryActiveJob() { const jobId = activeJobDetail.jobId; const initialChildren = Array.from(el.retryJobBtn.childNodes); + el.retryJobBtn.setAttribute("aria-busy", "true"); el.retryJobBtn.disabled = true; el.retryJobBtn.textContent = "Retrying..."; setStatus("Requesting operator retry..."); @@ -339,6 +342,7 @@ async function retryActiveJob() { setError("Network error while requesting retry. Retry when the service is reachable."); } finally { el.retryJobBtn.replaceChildren(...initialChildren); + el.retryJobBtn.removeAttribute("aria-busy"); el.retryJobBtn.disabled = false; } } @@ -413,6 +417,7 @@ async function refreshKpis() { async function refreshKpiEvidence() { const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes); + el.refreshEvidenceBtn.setAttribute("aria-busy", "true"); el.refreshEvidenceBtn.disabled = true; el.refreshEvidenceBtn.textContent = "Refreshing..."; @@ -428,12 +433,14 @@ async function refreshKpiEvidence() { el.kpiExportStatus.textContent = "Snapshot evidence is unavailable while the service is unreachable."; } finally { el.refreshEvidenceBtn.replaceChildren(...initialChildren); + el.refreshEvidenceBtn.removeAttribute("aria-busy"); el.refreshEvidenceBtn.disabled = false; } } async function loadDemoData() { const initialChildren = Array.from(el.loadDemoDataBtn.childNodes); + el.loadDemoDataBtn.setAttribute("aria-busy", "true"); el.loadDemoDataBtn.disabled = true; el.loadDemoDataBtn.textContent = "Loading..."; setStatus("Loading seeded buyer-demo story..."); @@ -459,6 +466,7 @@ async function loadDemoData() { setError("Unable to load seeded demo story."); } finally { el.loadDemoDataBtn.replaceChildren(...initialChildren); + el.loadDemoDataBtn.removeAttribute("aria-busy"); el.loadDemoDataBtn.disabled = false; } } @@ -506,6 +514,7 @@ async function submitDocument(event) { } const initialChildren = Array.from(el.submitBtn.childNodes); + el.submitBtn.setAttribute("aria-busy", "true"); el.submitBtn.disabled = true; el.submitBtn.textContent = "Submitting..."; setStatus("Submitting document..."); @@ -551,6 +560,7 @@ async function submitDocument(event) { setError("Network error while submitting. Retry when the service is reachable."); } finally { el.submitBtn.replaceChildren(...initialChildren); + el.submitBtn.removeAttribute("aria-busy"); el.submitBtn.disabled = false; } }