From 00547000664558cd4afccb956d2d74cb720d8936 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:27:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=A1=9C=EB=94=A9=20=EC=83=81=ED=83=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `demo.js`의 비동기 작업 버튼에 명시적인 로딩 상태(비활성화 및 텍스트 변경) 적용 - 중복 제출 방지 및 사용자 피드백 제공을 통한 UX 향상 - 관련 테스트 코드 작성 및 검증 완료 --- .jules/palette.md | 4 ++++ CHANGELOG.md | 5 +++++ src/main/resources/static/assets/viewer/demo.js | 12 ++++++++++++ .../viewer/controller/ViewerUiControllerTest.java | 3 +++ 4 files changed, 24 insertions(+) create mode 100644 CHANGELOG.md diff --git a/.jules/palette.md b/.jules/palette.md index a1633627..77b98dcd 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,7 @@ ## 2024-05-18 - Disabled and Loading States for Async Actions **Learning:** Adding explicit loading and disabled states to asynchronous action buttons (like "Refresh") provides immediate feedback, reducing user confusion and preventing double-submissions. **Action:** Always ensure that buttons triggering network requests visually indicate the loading state and are disabled until the request completes. + +## 2026-07-06 - Async Button Loading States +**Learning:** Asynchronous action buttons (e.g., Refresh, Submit) in this app require explicit visual loading states (updating textContent and disabled=true) to prevent double-submissions and improve UX. +**Action:** Always implement disabled and text state changes on buttons triggering network requests, restoring them in a finally block. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3f327cd6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# 변경 사항 (CHANGELOG) + +## [Unreleased] +### 추가된 기능 (Added) +- `demo.js` 내의 비동기 버튼(제출, 재시도, 새로고침)에 명시적인 시각적 로딩 상태(버튼 비활성화 및 텍스트 변경)를 추가하여 중복 제출을 방지하고 사용자 경험(UX)을 향상시켰습니다. diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 84eb8f3a..ddf5b2f0 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -271,6 +271,8 @@ async function retryActiveJob() { const jobId = activeJobDetail.jobId; setStatus("Requesting operator retry..."); el.retryJobBtn.disabled = true; + const originalText = el.retryJobBtn.textContent; + el.retryJobBtn.textContent = "Retrying..."; try { const res = await fetch(`/api/v1/convert/jobs/${encodeURIComponent(jobId)}/retry`, { method: "POST", @@ -308,6 +310,7 @@ async function retryActiveJob() { setError("Network error while requesting retry. Retry when the service is reachable."); } finally { el.retryJobBtn.disabled = false; + el.retryJobBtn.textContent = originalText; } } @@ -380,6 +383,9 @@ async function refreshKpis() { } async function refreshKpiEvidence() { + el.refreshEvidenceBtn.disabled = true; + const originalText = el.refreshEvidenceBtn.textContent; + el.refreshEvidenceBtn.textContent = "Refreshing..."; try { const { res, data } = await fetchJson(KPI_EXPORTS_ENDPOINT); if (!res.ok) { @@ -390,6 +396,9 @@ async function refreshKpiEvidence() { renderKpiEvidence(data); } catch (err) { el.kpiExportStatus.textContent = "Snapshot evidence is unavailable while the service is unreachable."; + } finally { + el.refreshEvidenceBtn.disabled = false; + el.refreshEvidenceBtn.textContent = originalText; } } @@ -436,6 +445,8 @@ async function submitDocument(event) { } el.submitBtn.disabled = true; + const originalText = el.submitBtn.textContent; + el.submitBtn.textContent = "Submitting..."; setStatus("Submitting document..."); try { @@ -479,6 +490,7 @@ async function submitDocument(event) { setError("Network error while submitting. Retry when the service is reachable."); } finally { el.submitBtn.disabled = false; + el.submitBtn.textContent = originalText; } } diff --git a/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java index dadb8949..4bb69a5a 100644 --- a/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java @@ -121,6 +121,9 @@ void demoScriptUsesExistingApiAndSessionHistory() throws Exception { assertTrue(script.contains("X-Clearfolio-Tenant-Id")); assertTrue(script.contains("X-Clearfolio-Permissions")); assertTrue(script.contains("deadLettered")); + assertTrue(script.contains("Refreshing...")); + assertTrue(script.contains("Submitting...")); + assertTrue(script.contains("Retrying...")); } }