From dc135263604d03ec70609bda9fb8451c29623ebe Mon Sep 17 00:00:00 2001 From: art Date: Fri, 30 Jan 2026 10:24:26 +0900 Subject: [PATCH] Enhance server check functionality and UI updates. Added new state management for server checks, including recovery mode and transition scheduling. Improved error handling and introduced new styles for server check notifications and tool indicators. --- src/renderer/App.tsx | 324 +++++++++++++++++++++++++++++++--------- src/renderer/styles.css | 19 +++ 2 files changed, 274 insertions(+), 69 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 978f917..f49402f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -27,6 +27,8 @@ type GitPathsCheck = { const HEALTHCHECK_INTERVAL_MS = APP_CONFIG.healthcheckIntervalMs; const HEALTHCHECK_ENDPOINT_FALLBACK = SERVER_HEALTHCHECK_URL; +const MIN_SERVER_CHECK_SCREEN_MS = 1500; +const IS_DEV = import.meta.env.DEV; const App = () => { const [screen, setScreen] = useState("serverCheck"); @@ -37,6 +39,10 @@ const App = () => { const [serverCheckedUrl, setServerCheckedUrl] = useState(); const [serverCheckInProgress, setServerCheckInProgress] = useState(false); const [serverCheckedAt, setServerCheckedAt] = useState(); + const [serverCheckStartedAt, setServerCheckStartedAt] = useState(Date.now()); + const [serverRecoveryActive, setServerRecoveryActive] = useState(false); + const [skipToolsCheck, setSkipToolsCheck] = useState(false); + const [devProceedReady, setDevProceedReady] = useState(false); const [gitCheckInProgress, setGitCheckInProgress] = useState(false); const [gitCheckedAt, setGitCheckedAt] = useState(); const [gitCheckResult, setGitCheckResult] = useState(); @@ -62,6 +68,7 @@ const App = () => { const healthCheckInFlightRef = useRef(false); const gitCheckInFlightRef = useRef(false); const gitCheckedRef = useRef(false); + const transitionTimeoutRef = useRef(); const runGitCheck = useCallback(async (force = false) => { if (force) { @@ -69,7 +76,7 @@ const App = () => { } if (gitCheckInFlightRef.current || (!force && gitCheckedRef.current)) { - return; + return gitCheckResult; } gitCheckInFlightRef.current = true; @@ -84,78 +91,193 @@ const App = () => { setGitCheckResult(result); setGitCheckedAt(result.checkedAt ?? Date.now()); gitCheckedRef.current = true; + return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); - setGitCheckResult({ + const fallback = { git: { ok: false, command: "git", error: message }, lfs: { ok: false, command: "git lfs", error: message }, checkedAt: Date.now() - }); + }; + setGitCheckResult(fallback); setGitCheckedAt(Date.now()); gitCheckedRef.current = true; + return fallback; } finally { gitCheckInFlightRef.current = false; setGitCheckInProgress(false); } + }, [gitCheckResult]); + + const clearTransitionTimeout = useCallback(() => { + if (transitionTimeoutRef.current) { + window.clearTimeout(transitionTimeoutRef.current); + transitionTimeoutRef.current = undefined; + } }, []); - const runHealthCheck = useCallback(async () => { - if (healthCheckInFlightRef.current) { - return; - } + const scheduleScreenTransition = useCallback( + (nextScreen: Screen) => { + clearTransitionTimeout(); + const elapsedMs = Date.now() - serverCheckStartedAt; + const remainingMs = Math.max(0, MIN_SERVER_CHECK_SCREEN_MS - elapsedMs); - healthCheckInFlightRef.current = true; - setServerCheckInProgress(true); - setServerError(undefined); - - try { - if (!window.sptLauncher?.checkServerHealth) { - throw new Error("preload(preload) 미로딩: window.sptLauncher가 없습니다."); + if (remainingMs > 0) { + transitionTimeoutRef.current = window.setTimeout(() => { + setScreen(nextScreen); + }, remainingMs); + return; } - const result = await window.sptLauncher.checkServerHealth(); - setServerHealthy(result.ok); - setServerStatusCode(result.status); - setServerLatencyMs(result.latencyMs); - setServerError(result.error); - setServerCheckedUrl(result.url ?? HEALTHCHECK_ENDPOINT_FALLBACK); - setServerCheckedAt(Date.now()); + setScreen(nextScreen); + }, + [clearTransitionTimeout, serverCheckStartedAt] + ); - if (screen === "serverCheck" && result.ok) { - if (!gitCheckedRef.current) { - await runGitCheck(); - } - setScreen(hasSession ? "main" : "login"); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const enterRecoveryMode = useCallback( + (reason: string) => { + setServerRecoveryActive(true); setServerHealthy(false); setServerStatusCode(undefined); setServerLatencyMs(undefined); - setServerError(message); + setServerError(reason); setServerCheckedUrl(undefined); setServerCheckedAt(Date.now()); - } finally { - healthCheckInFlightRef.current = false; - setServerCheckInProgress(false); + setHasSession(false); + setProfile(undefined); + setSkipToolsCheck(false); + setDevProceedReady(false); + setScreen("serverCheck"); + clearTransitionTimeout(); + }, + [clearTransitionTimeout] + ); + + const isTimeoutError = (error?: string) => { + if (!error) { + return false; } - }, [screen, hasSession]); + return error.toLowerCase().includes("timeout"); + }; + + const runHealthCheck = useCallback( + async (trigger: "initial" | "manual" | "recovery" = "initial") => { + if (healthCheckInFlightRef.current) { + return; + } + + healthCheckInFlightRef.current = true; + setServerCheckInProgress(true); + setServerError(undefined); + let isHealthy = false; + let failureReason = "healthcheck_failed"; + + try { + if (!window.sptLauncher?.checkServerHealth) { + throw new Error("preload(preload) 미로딩: window.sptLauncher가 없습니다."); + } + + const result = await window.sptLauncher.checkServerHealth(); + isHealthy = result.ok; + if (!result.ok) { + failureReason = result.error ?? "healthcheck_failed"; + } + setServerHealthy(result.ok); + setServerStatusCode(result.status); + setServerLatencyMs(result.latencyMs); + setServerError(result.error); + setServerCheckedUrl(result.url ?? HEALTHCHECK_ENDPOINT_FALLBACK); + setServerCheckedAt(Date.now()); + + if (screen === "serverCheck" && result.ok) { + const gitResult = !gitCheckedRef.current ? await runGitCheck() : gitCheckResult; + const toolsReady = Boolean(gitResult?.git.ok && gitResult?.lfs.ok); + + if (serverRecoveryActive) { + setServerRecoveryActive(false); + if (IS_DEV) { + setDevProceedReady(true); + } else { + scheduleScreenTransition("login"); + } + return; + } + + if (!toolsReady && gitResult && !skipToolsCheck) { + return; + } + + if (IS_DEV) { + setDevProceedReady(true); + return; + } + + scheduleScreenTransition(hasSession ? "main" : "login"); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + failureReason = message; + setServerHealthy(false); + setServerStatusCode(undefined); + setServerLatencyMs(undefined); + setServerError(message); + setServerCheckedUrl(undefined); + setServerCheckedAt(Date.now()); + } finally { + healthCheckInFlightRef.current = false; + setServerCheckInProgress(false); + } + + if (trigger === "manual" && !isHealthy) { + enterRecoveryMode(failureReason); + } + }, + [ + screen, + hasSession, + serverRecoveryActive, + runGitCheck, + gitCheckResult, + scheduleScreenTransition, + enterRecoveryMode, + skipToolsCheck + ] + ); + + const handleManualHealthCheck = () => { + void runHealthCheck("manual"); + }; useEffect(() => { if (screen !== "serverCheck") { return; } - void runHealthCheck(); - }, [screen, runHealthCheck]); + void runHealthCheck(serverRecoveryActive ? "recovery" : "initial"); + }, [screen, serverRecoveryActive, runHealthCheck]); useEffect(() => { + if (screen !== "serverCheck") { + return; + } + setServerCheckStartedAt(Date.now()); + setDevProceedReady(false); + }, [screen]); + + useEffect(() => { + if (!serverRecoveryActive) { + return; + } const interval = window.setInterval(() => { - void runHealthCheck(); + void runHealthCheck("recovery"); }, HEALTHCHECK_INTERVAL_MS); return () => window.clearInterval(interval); - }, [runHealthCheck]); + }, [serverRecoveryActive, runHealthCheck]); + + useEffect(() => { + return () => clearTransitionTimeout(); + }, [clearTransitionTimeout]); const serverStatusLabel = useMemo(() => { return serverHealthy ? "정상" : "불가"; @@ -311,6 +433,10 @@ const App = () => { username: trimmedId }); if (!result.ok) { + if (isTimeoutError(result.error)) { + enterRecoveryMode("timeout"); + return; + } window.alert(result.error ?? "프로필 조회에 실패했습니다."); return; } @@ -325,6 +451,10 @@ const App = () => { setScreen("main"); } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (isTimeoutError(message)) { + enterRecoveryMode("timeout"); + return; + } window.alert(message); } finally { setProfileLoading(false); @@ -363,6 +493,10 @@ const App = () => { try { const result = await window.sptLauncher.downloadProfile({ username }); if (!result.ok) { + if (isTimeoutError(result.error)) { + enterRecoveryMode("timeout"); + return; + } window.alert(result.error ?? "프로필 다운로드에 실패했습니다."); return; } @@ -370,6 +504,10 @@ const App = () => { window.alert(`프로필을 다운로드했습니다: ${result.filePath ?? "-"}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (isTimeoutError(message)) { + enterRecoveryMode("timeout"); + return; + } window.alert(message); } finally { setProfileActionInProgress(false); @@ -397,6 +535,10 @@ const App = () => { try { const result = await window.sptLauncher.resetProfile({ username }); if (!result.ok) { + if (isTimeoutError(result.error)) { + enterRecoveryMode("timeout"); + return; + } window.alert(result.error ?? "프로필 리셋에 실패했습니다."); return; } @@ -404,6 +546,10 @@ const App = () => { window.alert("프로필 리셋 요청이 완료되었습니다."); } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (isTimeoutError(message)) { + enterRecoveryMode("timeout"); + return; + } window.alert(message); } finally { setProfileActionInProgress(false); @@ -442,43 +588,73 @@ const App = () => { {screen === "serverCheck" && ( -
-

서버 상태 확인 중

+
+

서버 연결 및 도구 상태 점검

{serverCheckInProgress ? "체크 중..." : serverHealthy ? "정상" : "불가"}

-

- {serverHealthy - ? "서버 상태 확인 완료. 로그인 화면으로 이동합니다." - : "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."} -

-

- {serverCheckedUrl ? `엔드포인트: ${serverCheckedUrl}` : "엔드포인트: "} - {serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""} - {typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""} - {serverError ? ` · ${serverError}` : ""} - {serverCheckedAt ? ` · ${new Date(serverCheckedAt).toLocaleTimeString()}` : ""} -

+
+
+ 서버 + + {serverRecoveryActive && 복구 중} +
+
+ Git + +
+
+ Git LFS + +
+
{!serverCheckInProgress && !serverHealthy && ( - <> - - + )} - {serverHealthy && ( + {serverHealthy && gitCheckResult && !(gitCheckResult.git.ok && gitCheckResult.lfs.ok) && (
-
- Git - + 도구 설치 필요 +
+ Git/Git LFS 감지에 실패했습니다. 자동 설치(Windows) 또는 수동 설치 후 재확인해주세요.
-
- Git LFS - +
+ + +
- {gitCheckedAt ?
{new Date(gitCheckedAt).toLocaleTimeString()}
: null}
)} + {IS_DEV && + serverHealthy && + (skipToolsCheck || + Boolean(gitCheckResult?.git.ok && gitCheckResult?.lfs.ok)) && ( + + )}
)} @@ -641,7 +817,9 @@ const App = () => { {screen !== "serverCheck" && (