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; + }>; }; }