From 4140c4b5de70666b8e5473a1f74c604dd6120812 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:21: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?=20=EC=BD=94=EB=94=A9=20=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8=20?= =?UTF-8?q?Jules=EC=9E=85=EB=8B=88=EB=8B=A4!=20=EC=9A=94=EC=B2=AD=ED=95=98?= =?UTF-8?q?=EC=8B=A0=20Palette=EC=9D=98=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EB=B2=84=ED=8A=BC=EC=97=90=20`aria-busy`?= =?UTF-8?q?=20=EC=86=8D=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=9E=91=EC=97=85=EC=9D=84=20=EC=84=B1=EA=B3=B5?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=A7=88=EC=B3=A4=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비동기 작업이 진행 중일 때 스크린 리더 사용자에게 명확한 상태 피드백을 제공할 수 있도록, 각 액션 버튼에 동적으로 `aria-busy="true"` 속성을 추가하고 제거하는 로직을 구현했습니다. - **적용 대상**: Details, Retry, Refresh evidence, Load demo story, 문서 제출(Document) 버튼 - **작업 내역**: `demo.js` 로직 수정 및 이에 관련된 `ViewerUiControllerTest.java` 테스트 코드 업데이트 작업된 내용을 확인해 보시고, 추가로 필요하신 사항이나 수정할 부분이 있다면 언제든 말씀해 주세요! --- .jules/palette.md | 4 ++++ src/main/resources/static/assets/viewer/demo.js | 10 ++++++++++ .../viewer/controller/ViewerUiControllerTest.java | 1 + 3 files changed, 15 insertions(+) diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59..0c853a8 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. + +## 2026-07-18 - Async Loading States with aria-busy +**Learning:** 비동기 작업 중 사용자에게 명확한 로딩 피드백을 제공하려면 버튼의 텍스트와 disabled 속성만 변경하는 것보다 \`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..029846f 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -148,9 +148,11 @@ 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.removeAttribute("aria-busy"); btn.disabled = false; }); })); @@ -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..."); @@ -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; } } @@ -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 { @@ -428,6 +433,7 @@ 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; } } @@ -435,6 +441,7 @@ async function refreshKpiEvidence() { 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..."); @@ -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; } } @@ -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..."); @@ -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; } } diff --git a/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java index fa2dcfc..27237a6 100644 --- a/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java @@ -135,6 +135,7 @@ void demoScriptUsesExistingApiAndSessionHistory() throws Exception { assertTrue(script.contains("X-Clearfolio-Tenant-Id")); assertTrue(script.contains("X-Clearfolio-Permissions")); assertTrue(script.contains("deadLettered")); + assertTrue(script.contains("aria-busy")); } }