Compare commits
No commits in common. "944c77f146d9467491df527d0dcfc683dcf73e30" and "062911a9d07d921fddac837fc9520527d42c9bcf" have entirely different histories.
944c77f146
...
062911a9d0
300
src/main/main.ts
300
src/main/main.ts
|
|
@ -1,6 +1,4 @@
|
|||
import { app, BrowserWindow, ipcMain, net, session } from "electron";
|
||||
import { execFile } from "node:child_process";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
|
||||
|
|
@ -19,42 +17,6 @@ const SERVER_BASE_URL = "https://pandoli365.com:5069/";
|
|||
const SERVER_HEALTHCHECK_PATH = "/launcher/ping";
|
||||
const SERVER_HEALTHCHECK_TIMEOUT_MS = 2000;
|
||||
const SERVER_REQUEST_TIMEOUT_MS = 4000;
|
||||
const COMMAND_TIMEOUT_MS = 4000;
|
||||
const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
type CommandCheckResult = {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type CommandPathResult = {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type CommandRunResult = {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
args: string[];
|
||||
output?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type InstallStep = {
|
||||
name: string;
|
||||
result: CommandRunResult;
|
||||
};
|
||||
|
||||
type InstallGitToolsResult = {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
steps: InstallStep[];
|
||||
check?: Awaited<ReturnType<typeof checkGitTools>>;
|
||||
};
|
||||
|
||||
const checkServerHealth = async (): Promise<ServerHealthResult> => {
|
||||
const startedAt = Date.now();
|
||||
|
|
@ -115,153 +77,6 @@ const checkServerHealth = async (): Promise<ServerHealthResult> => {
|
|||
});
|
||||
};
|
||||
|
||||
const checkCommand = async (command: string, args: string[]): Promise<CommandCheckResult> => {
|
||||
return await new Promise<CommandCheckResult>((resolve) => {
|
||||
execFile(command, args, { timeout: COMMAND_TIMEOUT_MS }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve({ ok: false, command, error: error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const output = `${stdout ?? ""}${stderr ?? ""}`.trim();
|
||||
resolve({ ok: true, command, version: output });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkGitTools = async () => {
|
||||
const git = await checkCommand("git", ["--version"]);
|
||||
const lfs = await checkCommand("git", ["lfs", "version"]);
|
||||
|
||||
return {
|
||||
git,
|
||||
lfs,
|
||||
checkedAt: Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
const resolveCommandPath = async (command: string): Promise<CommandPathResult> => {
|
||||
const locator = process.platform === "win32" ? "where" : "which";
|
||||
|
||||
return await new Promise<CommandPathResult>((resolve) => {
|
||||
execFile(locator, [command], { timeout: COMMAND_TIMEOUT_MS }, (error, stdout) => {
|
||||
if (error) {
|
||||
resolve({ ok: false, command, error: error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const output = String(stdout ?? "").trim();
|
||||
const firstLine = output.split(/\r?\n/).find((line) => line.trim().length > 0);
|
||||
resolve({ ok: Boolean(firstLine), command, path: firstLine });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const runCommand = async (
|
||||
command: string,
|
||||
args: string[],
|
||||
timeoutMs: number
|
||||
): Promise<CommandRunResult> => {
|
||||
return await new Promise<CommandRunResult>((resolve) => {
|
||||
execFile(command, args, { timeout: timeoutMs }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve({
|
||||
ok: false,
|
||||
command,
|
||||
args,
|
||||
error: error.message,
|
||||
output: `${stdout ?? ""}${stderr ?? ""}`.trim()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const output = `${stdout ?? ""}${stderr ?? ""}`.trim();
|
||||
resolve({ ok: true, command, args, output });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getGitPaths = async () => {
|
||||
const git = await resolveCommandPath("git");
|
||||
const lfs = await resolveCommandPath("git-lfs");
|
||||
|
||||
return {
|
||||
git,
|
||||
lfs,
|
||||
checkedAt: Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
||||
if (process.platform !== "win32") {
|
||||
return { ok: false, error: "Windows에서만 자동 설치가 가능합니다.", steps: [] };
|
||||
}
|
||||
|
||||
const wingetPath = await resolveCommandPath("winget");
|
||||
if (!wingetPath.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "winget이 설치되어 있지 않습니다.",
|
||||
steps: [
|
||||
{
|
||||
name: "winget 확인",
|
||||
result: {
|
||||
ok: false,
|
||||
command: "winget",
|
||||
args: ["--version"],
|
||||
error: wingetPath.error
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const steps: InstallStep[] = [];
|
||||
|
||||
const gitInstall = await runCommand(
|
||||
"winget",
|
||||
[
|
||||
"install",
|
||||
"--id",
|
||||
"Git.Git",
|
||||
"-e",
|
||||
"--source",
|
||||
"winget",
|
||||
"--accept-source-agreements",
|
||||
"--accept-package-agreements",
|
||||
"--silent"
|
||||
],
|
||||
INSTALL_TIMEOUT_MS
|
||||
);
|
||||
steps.push({ name: "Git 설치", result: gitInstall });
|
||||
|
||||
const lfsInstall = await runCommand(
|
||||
"winget",
|
||||
[
|
||||
"install",
|
||||
"--id",
|
||||
"GitHub.GitLFS",
|
||||
"-e",
|
||||
"--source",
|
||||
"winget",
|
||||
"--accept-source-agreements",
|
||||
"--accept-package-agreements",
|
||||
"--silent"
|
||||
],
|
||||
INSTALL_TIMEOUT_MS
|
||||
);
|
||||
steps.push({ name: "Git LFS 설치", result: lfsInstall });
|
||||
|
||||
const ok = steps.every((step) => step.result.ok);
|
||||
const check = await checkGitTools();
|
||||
|
||||
return {
|
||||
ok,
|
||||
steps,
|
||||
check
|
||||
};
|
||||
};
|
||||
|
||||
const postJson = async <T>(
|
||||
path: string,
|
||||
body: unknown,
|
||||
|
|
@ -403,27 +218,6 @@ const postText = async (path: string, body: unknown) => {
|
|||
});
|
||||
};
|
||||
|
||||
const requestSessionId = async (username: string) => {
|
||||
const loginResult = await postText("/launcher/profile/login", { username });
|
||||
if (!loginResult.ok) {
|
||||
return { ok: false, error: loginResult.error ?? "login_failed", url: loginResult.url };
|
||||
}
|
||||
|
||||
const sessionId = loginResult.data?.trim();
|
||||
if (!sessionId) {
|
||||
return { ok: false, error: "empty_session", url: loginResult.url };
|
||||
}
|
||||
|
||||
return { ok: true, sessionId, url: loginResult.url };
|
||||
};
|
||||
|
||||
const registerProfile = async (username: string) => {
|
||||
return await postJson("/launcher/profile/register", {
|
||||
username,
|
||||
edition: "Standard"
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
|
|
@ -472,102 +266,20 @@ app.whenReady().then(() => {
|
|||
return await checkServerHealth();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:checkGitTools", async () => {
|
||||
return await checkGitTools();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:getGitPaths", async () => {
|
||||
return await getGitPaths();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:installGitTools", async () => {
|
||||
return await installGitTools();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
|
||||
const username = payload.username.trim();
|
||||
|
||||
const fetchProfileInfo = async (): Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
url: string;
|
||||
}> => {
|
||||
const sessionResult = await requestSessionId(username);
|
||||
if (!sessionResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: sessionResult.error ?? "login_failed",
|
||||
url: sessionResult.url
|
||||
};
|
||||
}
|
||||
|
||||
return await postJson(
|
||||
"/launcher/profile/info",
|
||||
{ username },
|
||||
{
|
||||
Cookie: `PHPSESSID=${sessionResult.sessionId}`
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const infoResult = await fetchProfileInfo();
|
||||
if (infoResult.ok && infoResult.data) {
|
||||
return infoResult;
|
||||
const loginResult = await postText("/launcher/profile/login", { username });
|
||||
if (!loginResult.ok || !loginResult.data) {
|
||||
return loginResult;
|
||||
}
|
||||
|
||||
await registerProfile(username);
|
||||
return await fetchProfileInfo();
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:downloadProfile", async (_event, payload: { username: string }) => {
|
||||
const username = payload.username.trim();
|
||||
|
||||
const sessionResult = await requestSessionId(username);
|
||||
if (!sessionResult.ok) {
|
||||
return sessionResult;
|
||||
}
|
||||
|
||||
const infoResult = await postJson(
|
||||
const sessionId = loginResult.data.trim();
|
||||
return await postJson(
|
||||
"/launcher/profile/info",
|
||||
{ username },
|
||||
{
|
||||
Cookie: `PHPSESSID=${sessionResult.sessionId}`
|
||||
}
|
||||
);
|
||||
|
||||
if (!infoResult.ok || !infoResult.data) {
|
||||
return infoResult;
|
||||
}
|
||||
|
||||
const downloadsDir = app.getPath("downloads");
|
||||
const fileName = `spt-profile-${username}-${Date.now()}.json`;
|
||||
const filePath = path.join(downloadsDir, fileName);
|
||||
|
||||
await fs.writeFile(filePath, JSON.stringify(infoResult.data, null, 2), "utf8");
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: infoResult.status,
|
||||
url: infoResult.url,
|
||||
filePath
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle("spt:resetProfile", async (_event, payload: { username: string }) => {
|
||||
const username = payload.username.trim();
|
||||
|
||||
const sessionResult = await requestSessionId(username);
|
||||
if (!sessionResult.ok) {
|
||||
return sessionResult;
|
||||
}
|
||||
|
||||
return await postJson(
|
||||
"/launcher/profile/change/wipe",
|
||||
{ username },
|
||||
{
|
||||
Cookie: `PHPSESSID=${sessionResult.sessionId}`
|
||||
Cookie: `PHPSESSID=${sessionId}`
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,13 +5,6 @@ console.info("[spt-launcher] preload loaded");
|
|||
contextBridge.exposeInMainWorld("sptLauncher", {
|
||||
appName: "SPT Launcher",
|
||||
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
|
||||
checkGitTools: () => ipcRenderer.invoke("spt:checkGitTools"),
|
||||
getGitPaths: () => ipcRenderer.invoke("spt:getGitPaths"),
|
||||
installGitTools: () => ipcRenderer.invoke("spt:installGitTools"),
|
||||
fetchProfile: (payload: { username: string }) =>
|
||||
ipcRenderer.invoke("spt:fetchProfile", payload),
|
||||
downloadProfile: (payload: { username: string }) =>
|
||||
ipcRenderer.invoke("spt:downloadProfile", payload),
|
||||
resetProfile: (payload: { username: string }) =>
|
||||
ipcRenderer.invoke("spt:resetProfile", payload)
|
||||
ipcRenderer.invoke("spt:fetchProfile", payload)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,6 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type Screen = "serverCheck" | "login" | "signup" | "resetPassword" | "main";
|
||||
type CommandCheck = {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
type GitToolsCheck = {
|
||||
git: CommandCheck;
|
||||
lfs: CommandCheck;
|
||||
checkedAt: number;
|
||||
};
|
||||
type CommandPath = {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
};
|
||||
type GitPathsCheck = {
|
||||
git: CommandPath;
|
||||
lfs: CommandPath;
|
||||
checkedAt: number;
|
||||
};
|
||||
|
||||
const HEALTHCHECK_INTERVAL_MS = 10000;
|
||||
const HEALTHCHECK_ENDPOINT_FALLBACK = "https://pandoli365.com:5069/launcher/ping";
|
||||
|
|
@ -36,14 +14,6 @@ const App = () => {
|
|||
const [serverCheckedUrl, setServerCheckedUrl] = useState<string | undefined>();
|
||||
const [serverCheckInProgress, setServerCheckInProgress] = useState(false);
|
||||
const [serverCheckedAt, setServerCheckedAt] = useState<number | undefined>();
|
||||
const [gitCheckInProgress, setGitCheckInProgress] = useState(false);
|
||||
const [gitCheckedAt, setGitCheckedAt] = useState<number | undefined>();
|
||||
const [gitCheckResult, setGitCheckResult] = useState<GitToolsCheck | undefined>();
|
||||
const [gitPathsLoading, setGitPathsLoading] = useState(false);
|
||||
const [gitPathsResult, setGitPathsResult] = useState<GitPathsCheck | undefined>();
|
||||
const [toolsModalOpen, setToolsModalOpen] = useState(false);
|
||||
const [installInProgress, setInstallInProgress] = useState(false);
|
||||
const [installMessage, setInstallMessage] = useState<string | undefined>();
|
||||
const [hasSession, setHasSession] = useState(false);
|
||||
const [loginId, setLoginId] = useState("");
|
||||
const [loginPassword, setLoginPassword] = useState("");
|
||||
|
|
@ -55,48 +25,9 @@ const App = () => {
|
|||
side?: string;
|
||||
id?: string;
|
||||
} | undefined>();
|
||||
const [profileActionInProgress, setProfileActionInProgress] = useState(false);
|
||||
const [syncInProgress, setSyncInProgress] = useState(false);
|
||||
const [simulateSyncFail, setSimulateSyncFail] = useState(false);
|
||||
const healthCheckInFlightRef = useRef(false);
|
||||
const gitCheckInFlightRef = useRef(false);
|
||||
const gitCheckedRef = useRef(false);
|
||||
|
||||
const runGitCheck = useCallback(async (force = false) => {
|
||||
if (force) {
|
||||
gitCheckedRef.current = false;
|
||||
}
|
||||
|
||||
if (gitCheckInFlightRef.current || (!force && gitCheckedRef.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
gitCheckInFlightRef.current = true;
|
||||
setGitCheckInProgress(true);
|
||||
|
||||
try {
|
||||
if (!window.sptLauncher?.checkGitTools) {
|
||||
throw new Error("preload(preload) 미로딩: window.sptLauncher.checkGitTools가 없습니다.");
|
||||
}
|
||||
|
||||
const result = await window.sptLauncher.checkGitTools();
|
||||
setGitCheckResult(result);
|
||||
setGitCheckedAt(result.checkedAt ?? Date.now());
|
||||
gitCheckedRef.current = true;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setGitCheckResult({
|
||||
git: { ok: false, command: "git", error: message },
|
||||
lfs: { ok: false, command: "git lfs", error: message },
|
||||
checkedAt: Date.now()
|
||||
});
|
||||
setGitCheckedAt(Date.now());
|
||||
gitCheckedRef.current = true;
|
||||
} finally {
|
||||
gitCheckInFlightRef.current = false;
|
||||
setGitCheckInProgress(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const runHealthCheck = useCallback(async () => {
|
||||
if (healthCheckInFlightRef.current) {
|
||||
|
|
@ -121,9 +52,6 @@ const App = () => {
|
|||
setServerCheckedAt(Date.now());
|
||||
|
||||
if (screen === "serverCheck" && result.ok) {
|
||||
if (!gitCheckedRef.current) {
|
||||
await runGitCheck();
|
||||
}
|
||||
setScreen(hasSession ? "main" : "login");
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -160,91 +88,6 @@ const App = () => {
|
|||
return serverHealthy ? "정상" : "불가";
|
||||
}, [serverHealthy]);
|
||||
|
||||
const serverStatusLedClass = serverCheckInProgress ? "idle" : serverHealthy ? "ok" : "blocked";
|
||||
const gitStatusLedClass = (() => {
|
||||
if (gitCheckInProgress) {
|
||||
return "idle";
|
||||
}
|
||||
if (!gitCheckResult) {
|
||||
return "idle";
|
||||
}
|
||||
if (gitCheckResult.git.ok && gitCheckResult.lfs.ok) {
|
||||
return "ok";
|
||||
}
|
||||
return "blocked";
|
||||
})();
|
||||
const gitLedClass = gitCheckInProgress
|
||||
? "idle"
|
||||
: !gitCheckResult
|
||||
? "idle"
|
||||
: gitCheckResult.git.ok
|
||||
? "ok"
|
||||
: "blocked";
|
||||
const lfsLedClass = gitCheckInProgress
|
||||
? "idle"
|
||||
: !gitCheckResult
|
||||
? "idle"
|
||||
: gitCheckResult.lfs.ok
|
||||
? "ok"
|
||||
: "blocked";
|
||||
|
||||
const handleOpenToolsModal = () => {
|
||||
setToolsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleRecheckTools = async () => {
|
||||
await runGitCheck(true);
|
||||
};
|
||||
|
||||
const handleInstallTools = async () => {
|
||||
if (!window.sptLauncher?.installGitTools) {
|
||||
window.alert("자동 설치 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setInstallInProgress(true);
|
||||
setInstallMessage(undefined);
|
||||
try {
|
||||
const result = await window.sptLauncher.installGitTools();
|
||||
if (!result.ok) {
|
||||
setInstallMessage(result.error ?? "자동 설치에 실패했습니다.");
|
||||
} else {
|
||||
setInstallMessage("자동 설치가 완료되었습니다.");
|
||||
}
|
||||
if (result.check) {
|
||||
setGitCheckResult(result.check);
|
||||
setGitCheckedAt(result.check.checkedAt ?? Date.now());
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setInstallMessage(message);
|
||||
} finally {
|
||||
setInstallInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchGitPaths = async () => {
|
||||
if (!window.sptLauncher?.getGitPaths) {
|
||||
window.alert("경로 확인 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setGitPathsLoading(true);
|
||||
try {
|
||||
const result = await window.sptLauncher.getGitPaths();
|
||||
setGitPathsResult(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setGitPathsResult({
|
||||
git: { ok: false, command: "git", error: message },
|
||||
lfs: { ok: false, command: "git-lfs", error: message },
|
||||
checkedAt: Date.now()
|
||||
});
|
||||
} finally {
|
||||
setGitPathsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const asObject = (value: unknown) =>
|
||||
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||
const pickString = (...values: unknown[]) =>
|
||||
|
|
@ -346,69 +189,6 @@ const App = () => {
|
|||
setScreen("login");
|
||||
};
|
||||
|
||||
const handleProfileDownload = async () => {
|
||||
const username = profile?.username?.trim() ?? loginId.trim();
|
||||
if (!username) {
|
||||
window.alert("아이디를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.sptLauncher?.downloadProfile) {
|
||||
window.alert("프로필 다운로드 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileActionInProgress(true);
|
||||
try {
|
||||
const result = await window.sptLauncher.downloadProfile({ username });
|
||||
if (!result.ok) {
|
||||
window.alert(result.error ?? "프로필 다운로드에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert(`프로필을 다운로드했습니다: ${result.filePath ?? "-"}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
window.alert(message);
|
||||
} finally {
|
||||
setProfileActionInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileReset = async () => {
|
||||
const username = profile?.username?.trim() ?? loginId.trim();
|
||||
if (!username) {
|
||||
window.alert("아이디를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.sptLauncher?.resetProfile) {
|
||||
window.alert("프로필 리셋 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = window.confirm("프로필을 리셋할까요?");
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileActionInProgress(true);
|
||||
try {
|
||||
const result = await window.sptLauncher.resetProfile({ username });
|
||||
if (!result.ok) {
|
||||
window.alert(result.error ?? "프로필 리셋에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert("프로필 리셋 요청이 완료되었습니다.");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
window.alert(message);
|
||||
} finally {
|
||||
setProfileActionInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLaunch = () => {
|
||||
if (syncInProgress) {
|
||||
return;
|
||||
|
|
@ -452,7 +232,7 @@ const App = () => {
|
|||
: "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."}
|
||||
</p>
|
||||
<p className="notice">
|
||||
{serverCheckedUrl ? `엔드포인트: ${serverCheckedUrl}` : "엔드포인트: "}
|
||||
{serverCheckedUrl ? `Endpoint(endpoint): ${serverCheckedUrl}` : "Endpoint(endpoint): (unknown)"}
|
||||
{serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""}
|
||||
{typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""}
|
||||
{serverError ? ` · ${serverError}` : ""}
|
||||
|
|
@ -465,19 +245,6 @@ const App = () => {
|
|||
</button>
|
||||
</>
|
||||
)}
|
||||
{serverHealthy && (
|
||||
<div className="notice">
|
||||
<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>
|
||||
{gitCheckedAt ? <div className="helper">{new Date(gitCheckedAt).toLocaleTimeString()}</div> : null}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
@ -582,21 +349,14 @@ const App = () => {
|
|||
<section className="card profile">
|
||||
<div>
|
||||
<h2>프로필 정보</h2>
|
||||
<p className="muted">아이디: {profile?.username ?? ""}</p>
|
||||
<p className="muted">닉네임: {profile?.nickname ?? ""}</p>
|
||||
<p className="muted">레벨: {typeof profile?.level === "number" ? profile.level : ""}</p>
|
||||
<p className="muted">아이디: {profile?.username ?? "-"}</p>
|
||||
<p className="muted">닉네임: {profile?.nickname ?? "미등록"}</p>
|
||||
<p className="muted">레벨: {typeof profile?.level === "number" ? profile.level : "-"}</p>
|
||||
{profile?.side && <p className="muted">진영: {profile.side}</p>}
|
||||
</div>
|
||||
<div className="profile-actions">
|
||||
<button type="button" onClick={handleProfileDownload} disabled={profileActionInProgress}>
|
||||
{profileActionInProgress ? "처리 중..." : "프로필 다운로드"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={handleProfileReset}
|
||||
disabled={profileActionInProgress}
|
||||
>
|
||||
<button type="button">프로필 다운로드</button>
|
||||
<button type="button" className="ghost">
|
||||
프로필 리셋
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -637,80 +397,6 @@ const App = () => {
|
|||
</footer>
|
||||
</main>
|
||||
)}
|
||||
{screen !== "serverCheck" && (
|
||||
<nav className="status-bar">
|
||||
<div className="status-item">
|
||||
<span className="label">서버 상태</span>
|
||||
<div className="status-item-right">
|
||||
<span className={`led ${serverStatusLedClass}`} aria-label="서버 상태" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="status-item">
|
||||
<button type="button" className="status-button" onClick={handleOpenToolsModal}>
|
||||
도구 상태
|
||||
</button>
|
||||
<div className="status-item-right">
|
||||
<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">LFS</span>
|
||||
<span className={`led ${lfsLedClass}`} aria-label="Git LFS 상태" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
{screen !== "serverCheck" && toolsModalOpen && (
|
||||
<div className="modal-overlay" role="presentation" onClick={() => setToolsModalOpen(false)}>
|
||||
<section
|
||||
className="modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="도구 상태"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="modal-header">
|
||||
<h3>도구 상태</h3>
|
||||
<button type="button" className="ghost" onClick={() => setToolsModalOpen(false)}>
|
||||
닫기
|
||||
</button>
|
||||
</header>
|
||||
<div className="modal-body">
|
||||
<div className="modal-row">
|
||||
<span className="label">Git</span>
|
||||
<span className={`led ${gitLedClass}`} aria-label="Git 상태" />
|
||||
</div>
|
||||
<div className="modal-row">
|
||||
<span className="label">Git LFS</span>
|
||||
<span className={`led ${lfsLedClass}`} aria-label="Git LFS 상태" />
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<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={handleFetchGitPaths} disabled={gitPathsLoading}>
|
||||
{gitPathsLoading ? "확인 중..." : "경로 확인"}
|
||||
</button>
|
||||
</div>
|
||||
{installMessage && <div className="notice">{installMessage}</div>}
|
||||
{gitPathsResult && (
|
||||
<div className="notice">
|
||||
<strong>경로 확인</strong>
|
||||
<div className="modal-help">
|
||||
<div>Git: {gitPathsResult.git.ok ? gitPathsResult.git.path : gitPathsResult.git.error}</div>
|
||||
<div>Git LFS: {gitPathsResult.lfs.ok ? gitPathsResult.lfs.path : gitPathsResult.lfs.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -190,149 +190,3 @@ button:disabled {
|
|||
.muted {
|
||||
color: #a8b0c1;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
position: fixed;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
background: #141923;
|
||||
border: 1px solid #242a35;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 4px 8px;
|
||||
background: #0f141d;
|
||||
border: 1px solid #242a35;
|
||||
border-radius: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.status-item-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-bar .label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-bar .status {
|
||||
margin-bottom: 0;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #a8b0c1;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-button:hover {
|
||||
color: #c7d0e0;
|
||||
}
|
||||
|
||||
.tool-led {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 11px;
|
||||
color: #a8b0c1;
|
||||
}
|
||||
|
||||
.led {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #2b2f3a;
|
||||
box-shadow: 0 0 0 2px #0f1218;
|
||||
}
|
||||
|
||||
.led.ok {
|
||||
background: #44d38f;
|
||||
box-shadow: 0 0 6px rgba(68, 211, 143, 0.8);
|
||||
}
|
||||
|
||||
.led.idle {
|
||||
background: #9aa3b2;
|
||||
box-shadow: 0 0 6px rgba(154, 163, 178, 0.6);
|
||||
}
|
||||
|
||||
.led.blocked {
|
||||
background: #f07171;
|
||||
box-shadow: 0 0 6px rgba(240, 113, 113, 0.7);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(7, 10, 16, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(520px, 90vw);
|
||||
background: #141923;
|
||||
border: 1px solid #242a35;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-help {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #cbd2e0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,65 +10,6 @@ interface Window {
|
|||
error?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
checkGitTools: () => Promise<{
|
||||
git: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
lfs: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
checkedAt: number;
|
||||
}>;
|
||||
getGitPaths: () => Promise<{
|
||||
git: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
};
|
||||
lfs: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
};
|
||||
checkedAt: number;
|
||||
}>;
|
||||
installGitTools: () => Promise<{
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
steps: Array<{
|
||||
name: string;
|
||||
result: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
args: string[];
|
||||
output?: string;
|
||||
error?: string;
|
||||
};
|
||||
}>;
|
||||
check?: {
|
||||
git: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
lfs: {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
checkedAt: number;
|
||||
};
|
||||
}>;
|
||||
fetchProfile: (payload: { username: string }) => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
|
|
@ -76,19 +17,5 @@ interface Window {
|
|||
error?: string;
|
||||
url: string;
|
||||
}>;
|
||||
downloadProfile: (payload: { username: string }) => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
url: string;
|
||||
filePath?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
resetProfile: (payload: { username: string }) => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
url: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue