Compare commits
No commits in common. "944c77f146d9467491df527d0dcfc683dcf73e30" and "062911a9d07d921fddac837fc9520527d42c9bcf" have entirely different histories.
944c77f146
...
062911a9d0
298
src/main/main.ts
298
src/main/main.ts
|
|
@ -1,6 +1,4 @@
|
||||||
import { app, BrowserWindow, ipcMain, net, session } from "electron";
|
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";
|
import * as path from "node:path";
|
||||||
|
|
||||||
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
|
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_PATH = "/launcher/ping";
|
||||||
const SERVER_HEALTHCHECK_TIMEOUT_MS = 2000;
|
const SERVER_HEALTHCHECK_TIMEOUT_MS = 2000;
|
||||||
const SERVER_REQUEST_TIMEOUT_MS = 4000;
|
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 checkServerHealth = async (): Promise<ServerHealthResult> => {
|
||||||
const startedAt = Date.now();
|
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>(
|
const postJson = async <T>(
|
||||||
path: string,
|
path: string,
|
||||||
body: unknown,
|
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 createWindow = () => {
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
|
|
@ -472,102 +266,20 @@ app.whenReady().then(() => {
|
||||||
return await checkServerHealth();
|
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 }) => {
|
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
|
||||||
const username = payload.username.trim();
|
const username = payload.username.trim();
|
||||||
|
|
||||||
const fetchProfileInfo = async (): Promise<{
|
const loginResult = await postText("/launcher/profile/login", { username });
|
||||||
ok: boolean;
|
if (!loginResult.ok || !loginResult.data) {
|
||||||
status?: number;
|
return loginResult;
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sessionId = loginResult.data.trim();
|
||||||
return await postJson(
|
return await postJson(
|
||||||
"/launcher/profile/info",
|
"/launcher/profile/info",
|
||||||
{ username },
|
{ username },
|
||||||
{
|
{
|
||||||
Cookie: `PHPSESSID=${sessionResult.sessionId}`
|
Cookie: `PHPSESSID=${sessionId}`
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const infoResult = await fetchProfileInfo();
|
|
||||||
if (infoResult.ok && infoResult.data) {
|
|
||||||
return infoResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
"/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}`
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,6 @@ console.info("[spt-launcher] preload loaded");
|
||||||
contextBridge.exposeInMainWorld("sptLauncher", {
|
contextBridge.exposeInMainWorld("sptLauncher", {
|
||||||
appName: "SPT Launcher",
|
appName: "SPT Launcher",
|
||||||
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
|
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
|
||||||
checkGitTools: () => ipcRenderer.invoke("spt:checkGitTools"),
|
|
||||||
getGitPaths: () => ipcRenderer.invoke("spt:getGitPaths"),
|
|
||||||
installGitTools: () => ipcRenderer.invoke("spt:installGitTools"),
|
|
||||||
fetchProfile: (payload: { username: string }) =>
|
fetchProfile: (payload: { username: string }) =>
|
||||||
ipcRenderer.invoke("spt:fetchProfile", payload),
|
ipcRenderer.invoke("spt:fetchProfile", payload)
|
||||||
downloadProfile: (payload: { username: string }) =>
|
|
||||||
ipcRenderer.invoke("spt:downloadProfile", payload),
|
|
||||||
resetProfile: (payload: { username: string }) =>
|
|
||||||
ipcRenderer.invoke("spt:resetProfile", payload)
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
type Screen = "serverCheck" | "login" | "signup" | "resetPassword" | "main";
|
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_INTERVAL_MS = 10000;
|
||||||
const HEALTHCHECK_ENDPOINT_FALLBACK = "https://pandoli365.com:5069/launcher/ping";
|
const HEALTHCHECK_ENDPOINT_FALLBACK = "https://pandoli365.com:5069/launcher/ping";
|
||||||
|
|
@ -36,14 +14,6 @@ const App = () => {
|
||||||
const [serverCheckedUrl, setServerCheckedUrl] = useState<string | undefined>();
|
const [serverCheckedUrl, setServerCheckedUrl] = useState<string | undefined>();
|
||||||
const [serverCheckInProgress, setServerCheckInProgress] = useState(false);
|
const [serverCheckInProgress, setServerCheckInProgress] = useState(false);
|
||||||
const [serverCheckedAt, setServerCheckedAt] = useState<number | undefined>();
|
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 [hasSession, setHasSession] = useState(false);
|
||||||
const [loginId, setLoginId] = useState("");
|
const [loginId, setLoginId] = useState("");
|
||||||
const [loginPassword, setLoginPassword] = useState("");
|
const [loginPassword, setLoginPassword] = useState("");
|
||||||
|
|
@ -55,48 +25,9 @@ const App = () => {
|
||||||
side?: string;
|
side?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
} | undefined>();
|
} | undefined>();
|
||||||
const [profileActionInProgress, setProfileActionInProgress] = useState(false);
|
|
||||||
const [syncInProgress, setSyncInProgress] = useState(false);
|
const [syncInProgress, setSyncInProgress] = useState(false);
|
||||||
const [simulateSyncFail, setSimulateSyncFail] = useState(false);
|
const [simulateSyncFail, setSimulateSyncFail] = useState(false);
|
||||||
const healthCheckInFlightRef = useRef(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 () => {
|
const runHealthCheck = useCallback(async () => {
|
||||||
if (healthCheckInFlightRef.current) {
|
if (healthCheckInFlightRef.current) {
|
||||||
|
|
@ -121,9 +52,6 @@ const App = () => {
|
||||||
setServerCheckedAt(Date.now());
|
setServerCheckedAt(Date.now());
|
||||||
|
|
||||||
if (screen === "serverCheck" && result.ok) {
|
if (screen === "serverCheck" && result.ok) {
|
||||||
if (!gitCheckedRef.current) {
|
|
||||||
await runGitCheck();
|
|
||||||
}
|
|
||||||
setScreen(hasSession ? "main" : "login");
|
setScreen(hasSession ? "main" : "login");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -160,91 +88,6 @@ const App = () => {
|
||||||
return serverHealthy ? "정상" : "불가";
|
return serverHealthy ? "정상" : "불가";
|
||||||
}, [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) =>
|
const asObject = (value: unknown) =>
|
||||||
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||||
const pickString = (...values: unknown[]) =>
|
const pickString = (...values: unknown[]) =>
|
||||||
|
|
@ -346,69 +189,6 @@ const App = () => {
|
||||||
setScreen("login");
|
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 = () => {
|
const handleLaunch = () => {
|
||||||
if (syncInProgress) {
|
if (syncInProgress) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -452,7 +232,7 @@ const App = () => {
|
||||||
: "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."}
|
: "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."}
|
||||||
</p>
|
</p>
|
||||||
<p className="notice">
|
<p className="notice">
|
||||||
{serverCheckedUrl ? `엔드포인트: ${serverCheckedUrl}` : "엔드포인트: "}
|
{serverCheckedUrl ? `Endpoint(endpoint): ${serverCheckedUrl}` : "Endpoint(endpoint): (unknown)"}
|
||||||
{serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""}
|
{serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""}
|
||||||
{typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""}
|
{typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""}
|
||||||
{serverError ? ` · ${serverError}` : ""}
|
{serverError ? ` · ${serverError}` : ""}
|
||||||
|
|
@ -465,19 +245,6 @@ const App = () => {
|
||||||
</button>
|
</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>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -582,21 +349,14 @@ const App = () => {
|
||||||
<section className="card profile">
|
<section className="card profile">
|
||||||
<div>
|
<div>
|
||||||
<h2>프로필 정보</h2>
|
<h2>프로필 정보</h2>
|
||||||
<p className="muted">아이디: {profile?.username ?? ""}</p>
|
<p className="muted">아이디: {profile?.username ?? "-"}</p>
|
||||||
<p className="muted">닉네임: {profile?.nickname ?? ""}</p>
|
<p className="muted">닉네임: {profile?.nickname ?? "미등록"}</p>
|
||||||
<p className="muted">레벨: {typeof profile?.level === "number" ? profile.level : ""}</p>
|
<p className="muted">레벨: {typeof profile?.level === "number" ? profile.level : "-"}</p>
|
||||||
{profile?.side && <p className="muted">진영: {profile.side}</p>}
|
{profile?.side && <p className="muted">진영: {profile.side}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-actions">
|
<div className="profile-actions">
|
||||||
<button type="button" onClick={handleProfileDownload} disabled={profileActionInProgress}>
|
<button type="button">프로필 다운로드</button>
|
||||||
{profileActionInProgress ? "처리 중..." : "프로필 다운로드"}
|
<button type="button" className="ghost">
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="ghost"
|
|
||||||
onClick={handleProfileReset}
|
|
||||||
disabled={profileActionInProgress}
|
|
||||||
>
|
|
||||||
프로필 리셋
|
프로필 리셋
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -637,80 +397,6 @@ const App = () => {
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -190,149 +190,3 @@ button:disabled {
|
||||||
.muted {
|
.muted {
|
||||||
color: #a8b0c1;
|
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;
|
error?: string;
|
||||||
url?: 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<{
|
fetchProfile: (payload: { username: string }) => Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
|
@ -76,19 +17,5 @@ interface Window {
|
||||||
error?: string;
|
error?: string;
|
||||||
url: 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