From 966d657a83ab7ed3aba2982429c2c5673305f64a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:55:27 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20viewer=20=ED=99=94=EB=A9=B4=20=EB=82=B4?= =?UTF-8?q?=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=83=81=ED=83=9C=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit viewer 화면의 retry 동작 시 텍스트 하드코딩 대신 setBusyState 유틸리티를 사용하도록 변경하여 기존 DOM 요소와 ARIA 상태를 안전하게 보존하면서 로딩 피드백을 제공하도록 구현했습니다. --- .jules/palette.md | 4 ++++ replace.sh | 1 + replace_viewer.sh | 1 + .../resources/static/assets/viewer/viewer.js | 18 ++++++++++++------ .../controller/ViewerUiControllerTest.java | 4 ++-- 5 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 replace.sh create mode 100644 replace_viewer.sh diff --git a/.jules/palette.md b/.jules/palette.md index a34ea59b2..aedffdc0d 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-18 - Async Status Feedback in Viewer +**Learning:** viewer 화면에서도 비동기 상태 피드백을 제공할 때 하드코딩된 텍스트 변경 대신 `setBusyState`를 사용하여 기존 DOM 요소 및 ARIA 상태를 보존하는 것이 중요합니다. +**Action:** 비동기 동작이 수반되는 버튼(`Retry` 등)에 대해 항상 `setBusyState`를 적용하고 콜백을 통해 원래 상태를 복원하도록 합니다. diff --git a/replace.sh b/replace.sh new file mode 100644 index 000000000..d2c78d1d7 --- /dev/null +++ b/replace.sh @@ -0,0 +1 @@ +sed -i 's/assertTrue(body.contains("\/assets\/viewer\/viewer.js"));/assertTrue(body.contains("type=\\"module\\" src=\\"\/assets\/viewer\/viewer.js\\""));/g' src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java diff --git a/replace_viewer.sh b/replace_viewer.sh new file mode 100644 index 000000000..d2c78d1d7 --- /dev/null +++ b/replace_viewer.sh @@ -0,0 +1 @@ +sed -i 's/assertTrue(body.contains("\/assets\/viewer\/viewer.js"));/assertTrue(body.contains("type=\\"module\\" src=\\"\/assets\/viewer\/viewer.js\\""));/g' src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java diff --git a/src/main/resources/static/assets/viewer/viewer.js b/src/main/resources/static/assets/viewer/viewer.js index a998675c7..c25f5ae26 100644 --- a/src/main/resources/static/assets/viewer/viewer.js +++ b/src/main/resources/static/assets/viewer/viewer.js @@ -7,6 +7,8 @@ const DEMO_AUTH_HEADERS = { "X-Clearfolio-Permissions": "job:read,viewer:read", }; +import { setBusyState } from "./dom-utils.js"; + const el = { docMeta: document.getElementById("doc-meta"), liveStatus: document.getElementById("live-status"), @@ -19,6 +21,7 @@ const el = { }; let pdfJsModulePromise; +let restoreRetryBtn = null; function getMetaContent(name) { const meta = document.querySelector(`meta[name="${name}"]`); @@ -54,8 +57,7 @@ function setLoading(message) { el.error.hidden = true; el.liveStatus.textContent = message; el.preview.setAttribute("aria-busy", "true"); - el.retryBtn.disabled = true; - el.retryBtn.textContent = "Refreshing..."; + restoreRetryBtn = setBusyState(el.retryBtn, "Refreshing..."); } function showError(message) { @@ -64,8 +66,10 @@ function showError(message) { el.liveStatus.textContent = ""; el.preview.setAttribute("aria-busy", "false"); el.errorTitle.focus(); - el.retryBtn.disabled = false; - el.retryBtn.textContent = "Refresh"; + if (restoreRetryBtn) { + restoreRetryBtn(); + restoreRetryBtn = null; + } } function clearPreview() { @@ -273,8 +277,10 @@ async function poll(docId, abortSignal) { el.preview.setAttribute("aria-busy", "false"); el.liveStatus.textContent = "Ready."; - el.retryBtn.disabled = false; - el.retryBtn.textContent = "Refresh"; + if (restoreRetryBtn) { + restoreRetryBtn(); + restoreRetryBtn = null; + } } catch (_error) { if (abortSignal.aborted) { return; diff --git a/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java index fa2dcfcac..38df23bb8 100644 --- a/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/ViewerUiControllerTest.java @@ -79,7 +79,7 @@ void viewerReturnsLoadingShellWithoutLeakingJobExistence() { assertTrue(body.contains("clearfolio-doc-id\" content=\"" + docId)); assertTrue(body.contains("clearfolio-initial-state\" content=\"LOADING\"")); assertTrue(body.contains("/assets/viewer/viewer.css")); - assertTrue(body.contains("/assets/viewer/viewer.js")); + assertTrue(body.contains("type=\"module\" src=\"/assets/viewer/viewer.js\"")); assertTrue(body.contains("target=\"_blank\" rel=\"noopener noreferrer\"")); assertTrue(body.contains("aria-label=\"Open JSON bootstrap in a new tab\"")); }); @@ -199,7 +199,7 @@ void viewerRejectsNonUuidDocIdWithFriendlyShell() { assertTrue(body.contains("clearfolio-doc-id\" content=\"invalid\"")); assertTrue(!body.contains("not-a-uuid")); assertTrue(body.contains("clearfolio-initial-state\" content=\"NOT_FOUND")); - assertTrue(body.contains("/assets/viewer/viewer.js")); + assertTrue(body.contains("type=\"module\" src=\"/assets/viewer/viewer.js\"")); }); }