Add profile download and reset functionality. Implemented new IPC handlers in the main process for downloading and resetting profiles, and updated the renderer to support these actions with appropriate user feedback. Enhanced profile information display and ensured session management during API interactions.

This commit is contained in:
이정수 2026-01-29 17:29:31 +09:00
parent 062911a9d0
commit 46c8d9b29c
4 changed files with 194 additions and 13 deletions

View File

@ -1,4 +1,5 @@
import { app, BrowserWindow, ipcMain, net, session } from "electron";
import * as fs from "node:fs/promises";
import * as path from "node:path";
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
@ -218,6 +219,27 @@ 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({
@ -269,17 +291,87 @@ app.whenReady().then(() => {
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
const username = payload.username.trim();
const loginResult = await postText("/launcher/profile/login", { username });
if (!loginResult.ok || !loginResult.data) {
return loginResult;
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 sessionId = loginResult.data.trim();
return await postJson(
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=${sessionId}`
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}`
}
);
});

View File

@ -6,5 +6,9 @@ contextBridge.exposeInMainWorld("sptLauncher", {
appName: "SPT Launcher",
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
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)
});

View File

@ -25,6 +25,7 @@ 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);
@ -189,6 +190,69 @@ 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;
@ -232,7 +296,7 @@ const App = () => {
: "서버 연결이 불가합니다. 10초마다 자동으로 재시도합니다."}
</p>
<p className="notice">
{serverCheckedUrl ? `Endpoint(endpoint): ${serverCheckedUrl}` : "Endpoint(endpoint): (unknown)"}
{serverCheckedUrl ? `Endpoint(endpoint): ${serverCheckedUrl}` : "Endpoint(endpoint): "}
{serverStatusCode ? ` · HTTP ${serverStatusCode}` : ""}
{typeof serverLatencyMs === "number" ? ` · ${serverLatencyMs}ms` : ""}
{serverError ? ` · ${serverError}` : ""}
@ -349,14 +413,21 @@ 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"> </button>
<button type="button" className="ghost">
<button type="button" onClick={handleProfileDownload} disabled={profileActionInProgress}>
{profileActionInProgress ? "처리 중..." : "프로필 다운로드"}
</button>
<button
type="button"
className="ghost"
onClick={handleProfileReset}
disabled={profileActionInProgress}
>
</button>
</div>

View File

@ -17,5 +17,19 @@ 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;
}>;
};
}