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.
This commit is contained in:
parent
4b28133a1a
commit
dc13526360
|
|
@ -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<Screen>("serverCheck");
|
||||
|
|
@ -37,6 +39,10 @@ const App = () => {
|
|||
const [serverCheckedUrl, setServerCheckedUrl] = useState<string | undefined>();
|
||||
const [serverCheckInProgress, setServerCheckInProgress] = useState(false);
|
||||
const [serverCheckedAt, setServerCheckedAt] = useState<number | undefined>();
|
||||
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<number | undefined>();
|
||||
const [gitCheckResult, setGitCheckResult] = useState<GitToolsCheck | undefined>();
|
||||
|
|
@ -62,6 +68,7 @@ const App = () => {
|
|||
const healthCheckInFlightRef = useRef(false);
|
||||
const gitCheckInFlightRef = useRef(false);
|
||||
const gitCheckedRef = useRef(false);
|
||||
const transitionTimeoutRef = useRef<number | undefined>();
|
||||
|
||||
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 = () => {
|
|||
</header>
|
||||
|
||||
{screen === "serverCheck" && (
|
||||
<section className="card hero">
|
||||
<h2>서버 상태 확인 중</h2>
|
||||
<section className="card hero server-check">
|
||||
<h2>서버 연결 및 도구 상태 점검</h2>
|
||||
<p className={`status ${serverCheckInProgress ? "idle" : serverHealthy ? "ok" : "blocked"}`}>
|
||||
{serverCheckInProgress ? "체크 중..." : serverHealthy ? "정상" : "불가"}
|
||||
</p>
|
||||
<p className="helper">
|
||||
{serverHealthy
|
||||
? "서버 상태 확인 완료. 로그인 화면으로 이동합니다."
|
||||
: "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."}
|
||||
</p>
|
||||
<p className="notice">
|
||||
{serverCheckedUrl ? `엔드포인트: ${serverCheckedUrl}` : "엔드포인트: "}
|
||||
{serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""}
|
||||
{typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""}
|
||||
{serverError ? ` · ${serverError}` : ""}
|
||||
{serverCheckedAt ? ` · ${new Date(serverCheckedAt).toLocaleTimeString()}` : ""}
|
||||
</p>
|
||||
<div className="notice">
|
||||
<div className="tool-led">
|
||||
<span className="tool-label">서버</span>
|
||||
<span className={`led ${serverStatusLedClass}`} aria-label="서버 상태" />
|
||||
{serverRecoveryActive && <span className="helper">복구 중</span>}
|
||||
</div>
|
||||
<div className="tool-led">
|
||||
<span className="tool-label">Git</span>
|
||||
<span className={`led ${gitLedClass}`} aria-label="Git 상태" />
|
||||
</div>
|
||||
<div className="tool-led">
|
||||
<span className="tool-label">Git LFS</span>
|
||||
<span className={`led ${lfsLedClass}`} aria-label="Git LFS 상태" />
|
||||
</div>
|
||||
</div>
|
||||
{!serverCheckInProgress && !serverHealthy && (
|
||||
<>
|
||||
<button type="button" onClick={() => void runHealthCheck()}>
|
||||
다시 시도
|
||||
</button>
|
||||
</>
|
||||
<button type="button" onClick={handleManualHealthCheck}>
|
||||
다시 확인
|
||||
</button>
|
||||
)}
|
||||
{serverHealthy && (
|
||||
{serverHealthy && gitCheckResult && !(gitCheckResult.git.ok && gitCheckResult.lfs.ok) && (
|
||||
<div className="notice">
|
||||
<div className="tool-led">
|
||||
<span className="tool-label">Git</span>
|
||||
<span className={`led ${gitLedClass}`} aria-label="Git 상태" />
|
||||
<strong>도구 설치 필요</strong>
|
||||
<div className="helper">
|
||||
Git/Git LFS 감지에 실패했습니다. 자동 설치(Windows) 또는 수동 설치 후 재확인해주세요.
|
||||
</div>
|
||||
<div className="tool-led">
|
||||
<span className="tool-label">Git LFS</span>
|
||||
<span className={`led ${lfsLedClass}`} aria-label="Git LFS 상태" />
|
||||
<div className="modal-actions">
|
||||
<button type="button" onClick={handleInstallTools} disabled={installInProgress}>
|
||||
{installInProgress ? "설치 중..." : "자동 설치"}
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={handleRecheckTools} disabled={gitCheckInProgress}>
|
||||
{gitCheckInProgress ? "점검 중..." : "재확인"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => {
|
||||
setSkipToolsCheck(true);
|
||||
scheduleScreenTransition(IS_DEV ? "login" : hasSession ? "main" : "login");
|
||||
}}
|
||||
>
|
||||
나중에 진행
|
||||
</button>
|
||||
</div>
|
||||
{gitCheckedAt ? <div className="helper">{new Date(gitCheckedAt).toLocaleTimeString()}</div> : null}
|
||||
</div>
|
||||
)}
|
||||
{IS_DEV &&
|
||||
serverHealthy &&
|
||||
(skipToolsCheck ||
|
||||
Boolean(gitCheckResult?.git.ok && gitCheckResult?.lfs.ok)) && (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => {
|
||||
setDevProceedReady(false);
|
||||
scheduleScreenTransition("login");
|
||||
}}
|
||||
disabled={!devProceedReady}
|
||||
>
|
||||
다음으로 이동
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
@ -641,7 +817,9 @@ const App = () => {
|
|||
{screen !== "serverCheck" && (
|
||||
<nav className="status-bar">
|
||||
<div className="status-item">
|
||||
<span className="label">서버 상태</span>
|
||||
<button type="button" className="status-button" onClick={handleManualHealthCheck}>
|
||||
서버 상태
|
||||
</button>
|
||||
<div className="status-item-right">
|
||||
<span className={`led ${serverStatusLedClass}`} aria-label="서버 상태" />
|
||||
</div>
|
||||
|
|
@ -691,13 +869,21 @@ const App = () => {
|
|||
<button type="button" onClick={handleRecheckTools} disabled={gitCheckInProgress}>
|
||||
{gitCheckInProgress ? "점검 중..." : "재확인"}
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={handleInstallTools} disabled={installInProgress}>
|
||||
{installInProgress ? "설치 중..." : "자동 재설치"}
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={handleInstallTools} disabled={installInProgress}>
|
||||
{installInProgress ? "설치 중..." : "자동 설치"}
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={handleFetchGitPaths} disabled={gitPathsLoading}>
|
||||
{gitPathsLoading ? "확인 중..." : "경로 확인"}
|
||||
</button>
|
||||
</div>
|
||||
{gitCheckResult && !(gitCheckResult.git.ok && gitCheckResult.lfs.ok) && (
|
||||
<div className="notice">
|
||||
<strong>도구 설치 필요</strong>
|
||||
<div className="modal-help">
|
||||
Git/Git LFS가 감지되지 않았습니다. 자동 설치(Windows) 또는 수동 설치 후 재확인해주세요.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{installMessage && <div className="notice">{installMessage}</div>}
|
||||
{gitPathsResult && (
|
||||
<div className="notice">
|
||||
|
|
|
|||
|
|
@ -105,6 +105,25 @@ button:disabled {
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.server-check .notice {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.server-check .tool-led {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-check .tool-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.server-check .led {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.helper {
|
||||
color: #a8b0c1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue