From 2e303c1731a6aba1d99650b1570414ee72aee13f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:05:09 +0000 Subject: [PATCH] =?UTF-8?q?=EC=95=88=EB=85=95=ED=95=98=EC=84=B8=EC=9A=94,?= =?UTF-8?q?=20AI=20=EC=BD=94=EB=94=A9=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20Jules=EC=9E=85=EB=8B=88=EB=8B=A4!=20=F0=9F=8E=A8=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EA=B4=80=EB=A0=A8=20=EC=A0=91=EA=B7=BC=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EC=9E=91=EC=97=85=EC=9D=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=ED=95=98=EC=97=AC=20=EC=95=88=EB=82=B4=ED=95=B4=20?= =?UTF-8?q?=EB=93=9C=EB=A6=BD=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에는 비동기 작업 중 버튼이 비활성화(disabled)될 때 시각적으로만 'Loading...'을 표시했습니다. 저는 접근성을 더욱 향상시키기 위해, 동적으로 `aria-busy="true"` 속성을 추가하고 작업 완료 후 이를 제거하는 패턴을 적용해 두었습니다. - 영향을 받는 버튼: Details, Retry, Refresh evidence, Load demo story, 문서 제출 - 저의 향후 작업을 위해 `.jules/palette.md` 파일에 이번 학습 내용 기록을 추가했습니다. 더 필요하신 작업이나 수정 사항이 있다면 언제든 말씀해 주세요! --- .jules/palette.md | 3 +++ src/main/resources/static/assets/viewer/demo.js | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59..dd52abf 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -13,3 +13,6 @@ ## 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. +## 2026-07-20 - 비동기 버튼 로딩 시 aria-busy 상태 관리 +**Learning:** 비동기 동작(예: API 요청)을 수행하는 버튼이 `disabled` 상태로 변경될 때, 시각적으로 'Loading...' 텍스트를 제공하는 것만으로는 스크린 리더 사용자에게 로딩 상태가 명확하게 전달되지 않음을 확인했습니다. +**Action:** 버튼이 로딩 상태일 때 `aria-busy="true"` 속성을 동적으로 추가하고, 작업이 완료된 후 `.finally()` 블록을 통해 안전하게 제거하는 패턴을 적용하여 접근성을 개선합니다. diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 51466a8..dce390a 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -148,10 +148,12 @@ function renderHistory(history = loadHistory()) { const btn = e.currentTarget; const initialChildren = Array.from(btn.childNodes); btn.disabled = true; + btn.setAttribute("aria-busy", "true"); btn.textContent = "Loading..."; openJobDetail(job).finally(() => { btn.replaceChildren(...initialChildren); btn.disabled = false; + btn.removeAttribute("aria-busy"); }); })); actionsCell.appendChild(createActionButton("Status JSON", () => { @@ -299,6 +301,7 @@ async function retryActiveJob() { const jobId = activeJobDetail.jobId; const initialChildren = Array.from(el.retryJobBtn.childNodes); el.retryJobBtn.disabled = true; + el.retryJobBtn.setAttribute("aria-busy", "true"); el.retryJobBtn.textContent = "Retrying..."; setStatus("Requesting operator retry..."); @@ -340,6 +343,7 @@ async function retryActiveJob() { } finally { el.retryJobBtn.replaceChildren(...initialChildren); el.retryJobBtn.disabled = false; + el.retryJobBtn.removeAttribute("aria-busy"); } } @@ -414,6 +418,7 @@ async function refreshKpis() { async function refreshKpiEvidence() { const initialChildren = Array.from(el.refreshEvidenceBtn.childNodes); el.refreshEvidenceBtn.disabled = true; + el.refreshEvidenceBtn.setAttribute("aria-busy", "true"); el.refreshEvidenceBtn.textContent = "Refreshing..."; try { @@ -429,12 +434,14 @@ async function refreshKpiEvidence() { } finally { el.refreshEvidenceBtn.replaceChildren(...initialChildren); el.refreshEvidenceBtn.disabled = false; + el.refreshEvidenceBtn.removeAttribute("aria-busy"); } } async function loadDemoData() { const initialChildren = Array.from(el.loadDemoDataBtn.childNodes); el.loadDemoDataBtn.disabled = true; + el.loadDemoDataBtn.setAttribute("aria-busy", "true"); el.loadDemoDataBtn.textContent = "Loading..."; setStatus("Loading seeded buyer-demo story..."); @@ -460,6 +467,7 @@ async function loadDemoData() { } finally { el.loadDemoDataBtn.replaceChildren(...initialChildren); el.loadDemoDataBtn.disabled = false; + el.loadDemoDataBtn.removeAttribute("aria-busy"); } } @@ -507,6 +515,7 @@ async function submitDocument(event) { const initialChildren = Array.from(el.submitBtn.childNodes); el.submitBtn.disabled = true; + el.submitBtn.setAttribute("aria-busy", "true"); el.submitBtn.textContent = "Submitting..."; setStatus("Submitting document..."); @@ -552,6 +561,7 @@ async function submitDocument(event) { } finally { el.submitBtn.replaceChildren(...initialChildren); el.submitBtn.disabled = false; + el.submitBtn.removeAttribute("aria-busy"); } }