From cb55967ca647464f5c679bddeaeba79447d48d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EC=84=9D?= Date: Sat, 28 Feb 2026 16:23:58 +0900 Subject: [PATCH 1/2] fix(app): add loading states, ARIA accessibility, and error handling - Add loading state with disabled buttons and dynamic text during async ops - Wrap ask() and runAction() in try-catch for network error resilience - Add ARIA attributes: aria-live, aria-busy, aria-disabled, aria-labelledby - Switch labels to htmlFor/id pattern for proper form accessibility - Add input validation for upload (empty text check) - Add hover, focus-visible, active, and disabled styles for buttons/inputs - Add .message class for visual feedback on status messages Co-Authored-By: Claude Opus 4.6 --- app/app/globals.css | 36 ++++++++++ app/app/page.tsx | 161 +++++++++++++++++++++++++++----------------- 2 files changed, 136 insertions(+), 61 deletions(-) diff --git a/app/app/globals.css b/app/app/globals.css index 1b662ef..59ef8fa 100644 --- a/app/app/globals.css +++ b/app/app/globals.css @@ -47,6 +47,15 @@ select { border: 1px solid #b9c6db; border-radius: 8px; padding: 0.65rem; + transition: border-color 0.15s, outline-color 0.15s; +} + +input:focus, +textarea:focus, +select:focus { + border-color: #3b82f6; + outline: 2px solid #3b82f6; + outline-offset: -1px; } button { @@ -56,6 +65,25 @@ button { border-radius: 8px; padding: 0.65rem 1rem; cursor: pointer; + transition: background 0.15s; +} + +button:hover { + background: #1e40af; +} + +button:active { + background: #1e3a8a; +} + +button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +button:disabled { + background: #93c5fd; + cursor: not-allowed; } pre { @@ -65,3 +93,11 @@ pre { padding: 0.75rem; border-radius: 10px; } + +.message { + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 8px; + padding: 0.75rem 1rem; + margin-top: 1rem; +} diff --git a/app/app/page.tsx b/app/app/page.tsx index 36cd5af..1c1e5b2 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -45,6 +45,7 @@ export default function HomePage() { const [queryResult, setQueryResult] = useState(null); const [actionResult, setActionResult] = useState(""); const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); useEffect(() => { void refreshAll(); @@ -67,7 +68,12 @@ export default function HomePage() { async function upload(event: FormEvent) { event.preventDefault(); + if (!sourceText.trim()) { + setMessage("텍스트를 입력하세요"); + return; + } setMessage(""); + setLoading(true); const form = new FormData(); form.set("project_id", projectId); form.set("source_text", sourceText); @@ -85,6 +91,8 @@ export default function HomePage() { await refreshAll(); } catch { setMessage("문서 업로드 실패"); + } finally { + setLoading(false); } } @@ -94,97 +102,126 @@ export default function HomePage() { setMessage("질문을 입력하세요"); return; } + setMessage(""); + setLoading(true); - const response = await fetch(ENDPOINT.queries, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - project_id: projectId, - question, - }), - }); - if (!response.ok) { - setMessage("질의 처리 실패"); - return; + try { + const response = await fetch(ENDPOINT.queries, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + project_id: projectId, + question, + }), + }); + if (!response.ok) { + setMessage("질의 처리 실패"); + return; + } + const payload: QueryResponse = await response.json(); + setQueryResult(payload); + setQuestion(""); + await refreshAll(); + } catch { + setMessage("네트워크 오류가 발생했습니다. 잠시 후 다시 시도하세요."); + } finally { + setLoading(false); } - const payload: QueryResponse = await response.json(); - setQueryResult(payload); - setQuestion(""); - await refreshAll(); } async function runAction() { - const response = await fetch(ENDPOINT.actions, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - project_id: projectId, - type: "summary", - payload: { - documents: documents.map((doc) => doc.id), + setLoading(true); + setActionResult(""); + + try { + const response = await fetch(ENDPOINT.actions, { + method: "POST", + headers: { + "Content-Type": "application/json", }, - }), - }); - if (!response.ok) { - setActionResult("액션 실행 실패"); - return; + body: JSON.stringify({ + project_id: projectId, + type: "summary", + payload: { + documents: documents.map((doc) => doc.id), + }, + }), + }); + if (!response.ok) { + setActionResult("액션 실행 실패"); + return; + } + const payload = await response.json(); + setActionResult(payload.result); + } catch { + setActionResult("네트워크 오류가 발생했습니다. 잠시 후 다시 시도하세요."); + } finally { + setLoading(false); } - const payload = await response.json(); - setActionResult(payload.result); } return (
-

Knowledge Copilot

+

Knowledge Copilot

RAG, 액션형 AI, API 로그 추적까지 포함한 포트폴리오 프로젝트

-
-

1) 문서 업로드

-
- -