From 46c8d9b29c9da7939adbf4e92b5bd354941192ac Mon Sep 17 00:00:00 2001 From: art Date: Thu, 29 Jan 2026 17:29:31 +0900 Subject: [PATCH] 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. --- src/main/main.ts | 104 ++++++++++++++++++++++++++++++++++--- src/preload/preload.ts | 6 ++- src/renderer/App.tsx | 83 ++++++++++++++++++++++++++--- src/renderer/vite-env.d.ts | 14 +++++ 4 files changed, 194 insertions(+), 13 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index e58d7b5..cf41481 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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}` } ); }); diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 06a9eb1..f59fb1b 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -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) }); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5181e74..5ae942d 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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초마다 자동으로 재시도합니다."}

- {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 = () => {

프로필 정보

-

아이디: {profile?.username ?? "-"}

-

닉네임: {profile?.nickname ?? "미등록"}

-

레벨: {typeof profile?.level === "number" ? profile.level : "-"}

+

아이디: {profile?.username ?? ""}

+

닉네임: {profile?.nickname ?? ""}

+

레벨: {typeof profile?.level === "number" ? profile.level : ""}

{profile?.side &&

진영: {profile.side}

}
- - +
diff --git a/src/renderer/vite-env.d.ts b/src/renderer/vite-env.d.ts index 30fb4f1..df7f4a8 100644 --- a/src/renderer/vite-env.d.ts +++ b/src/renderer/vite-env.d.ts @@ -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; + }>; }; }