diff --git a/docs/endpoints.md b/docs/endpoints.md index 65c6393..a75348e 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -299,6 +299,31 @@ ### Launcher API +- #### 런처 프로필 조회 가이드(Launcher Profile Fetch Guide) + - 공통 포맷(Common format) + - 요청 헤더(Headers) + - `requestcompressed: 0` + - `responsecompressed: 0` + - `Content-Type: application/json` + - 기본(cURL) + - `curl -k -X POST "https://{ip}:{port}/{path}" -H "requestcompressed: 0" -H "responsecompressed: 0" -H "Content-Type: application/json" -d ''` + - 로그인 과정(Login flow) + - 요청(Request) + - `POST /launcher/profile/login` + - 바디(Body): `{"username":""}` + - 응답(Response) + - 본문(body)에 세션 아이디(session id) 문자열 반환 (예: `6971f8f02539470025aaafad`) + - 예시(Example) + - `curl -k -X POST "https://pandoli365.com:5069/launcher/profile/login" -H "requestcompressed: 0" -H "responsecompressed: 0" -H "Content-Type: application/json" -d '{"username":"art"}'` + - 프로필 불러오는 방법(Profile fetch) + - 로그인 응답으로 받은 세션 아이디(session id)를 쿠키(cookie)로 전달 + - 요청(Request) + - `POST /launcher/profile/info` + - 헤더(Header): `Cookie: PHPSESSID=` + - 바디(Body): `{"username":""}` + - 예시(Example) + - `curl -k -X POST "https://pandoli365.com:5069/launcher/profile/info" -H "requestcompressed: 0" -H "responsecompressed: 0" -H "Content-Type: application/json" -H "Cookie: PHPSESSID=6971f8f02539470025aaafad" -d '{"username":"art"}'` + - `/launcher/ping` 역할(Role): 런처 핑(ping) 사용법(Usage): 요청 바디 없음(EmptyRequestData) @@ -314,6 +339,10 @@ - `/launcher/profile/get` 역할(Role): 계정 조회(get profile) 사용법(Usage): 요청 바디 `LoginRequestData` +- `/launcher/profiles` + 역할(Role): 프로필 목록 조회(get profile list) + 사용법(Usage): 요청 바디 없음(EmptyRequestData) + 주의(Note): 응답이 압축(compressed)되는 경우가 있어 `responsecompressed: 0` 헤더를 권장 - `/launcher/profile/change/username` 역할(Role): 사용자명 변경(change username) 사용법(Usage): 요청 바디 `ChangeRequestData` diff --git a/src/main/main.ts b/src/main/main.ts index 8eddc65..e58d7b5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -77,7 +77,11 @@ const checkServerHealth = async (): Promise => { }); }; -const postJson = async (path: string, body: unknown) => { +const postJson = async ( + path: string, + body: unknown, + extraHeaders: Record = {} +) => { const url = new URL(path, SERVER_BASE_URL).toString(); const payload = JSON.stringify(body); @@ -95,8 +99,9 @@ const postJson = async (path: string, body: unknown) => { "Content-Type": "application/json", Accept: "application/json", "Accept-Encoding": "identity", - "Content-Encoding": "identity", - "Content-Length": Buffer.byteLength(payload).toString() + requestcompressed: "0", + responsecompressed: "0", + ...extraHeaders } }); @@ -151,22 +156,25 @@ const postJson = async (path: string, body: unknown) => { }); }; -const getJson = async (path: string) => { +const postText = async (path: string, body: unknown) => { const url = new URL(path, SERVER_BASE_URL).toString(); + const payload = JSON.stringify(body); return await new Promise<{ ok: boolean; status?: number; - data?: T; + data?: string; error?: string; url: string; }>((resolve) => { const request = net.request({ - method: "GET", + method: "POST", url, headers: { - Accept: "application/json", + "Content-Type": "application/json", + Accept: "text/plain", "Accept-Encoding": "identity", + requestcompressed: "0", responsecompressed: "0" } }); @@ -191,19 +199,7 @@ const getJson = async (path: string) => { clearTimeout(timeout); const status = response.statusCode; const ok = typeof status === "number" ? status >= 200 && status < 400 : false; - - if (!rawData) { - resolve({ ok, status, url }); - return; - } - - try { - const parsed = JSON.parse(rawData) as T; - resolve({ ok, status, data: parsed, url }); - } catch (error) { - const message = error instanceof Error ? error.message : "invalid_json"; - resolve({ ok: false, status, error: message, url }); - } + resolve({ ok, status, data: rawData, url }); }); }); @@ -217,6 +213,7 @@ const getJson = async (path: string) => { }); }); + request.write(payload); request.end(); }); }; @@ -269,8 +266,22 @@ app.whenReady().then(() => { return await checkServerHealth(); }); - ipcMain.handle("spt:fetchProfiles", async () => { - return await getJson("/launcher/profiles"); + 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 sessionId = loginResult.data.trim(); + return await postJson( + "/launcher/profile/info", + { username }, + { + Cookie: `PHPSESSID=${sessionId}` + } + ); }); // 개발/운영 모두에서 부팅 시 1회 즉시 체크(instant check) 로그를 남겨 diff --git a/src/preload/preload.ts b/src/preload/preload.ts index aa99685..06a9eb1 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -5,5 +5,6 @@ console.info("[spt-launcher] preload loaded"); contextBridge.exposeInMainWorld("sptLauncher", { appName: "SPT Launcher", checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"), - fetchProfiles: () => ipcRenderer.invoke("spt:fetchProfiles") + fetchProfile: (payload: { username: string }) => + ipcRenderer.invoke("spt:fetchProfile", payload) }); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 75df382..5181e74 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -108,22 +108,6 @@ const App = () => { return undefined; }; - const extractProfileList = (data: unknown) => { - if (Array.isArray(data)) { - return data; - } - - const root = asObject(data); - if (Array.isArray(root?.profiles)) { - return root?.profiles; - } - if (Array.isArray(root?.data)) { - return root?.data; - } - - return []; - }; - const extractProfile = (data: unknown, fallbackUsername: string) => { const root = asObject(data); const profileCandidate = Array.isArray(root?.profiles) @@ -135,7 +119,16 @@ const App = () => { return { username: pickString(info.username, info.Username, profileObject.username, fallbackUsername) ?? fallbackUsername, nickname: pickString(info.nickname, info.Nickname, profileObject.nickname, profileObject.Nickname), - level: pickNumber(info.level, info.Level, profileObject.level, profileObject.Level), + level: pickNumber( + info.level, + info.Level, + info.currlvl, + info.currLvl, + profileObject.level, + profileObject.Level, + profileObject.currlvl, + profileObject.currLvl + ), side: pickString(info.side, info.Side, profileObject.side, profileObject.Side), id: pickString(info.id, info._id, profileObject.id, profileObject._id, profileObject.profileId) }; @@ -148,52 +141,28 @@ const App = () => { return; } - if (!window.sptLauncher?.fetchProfiles) { - window.alert("프로필 목록 조회 기능이 준비되지 않았습니다."); + if (!window.sptLauncher?.fetchProfile) { + window.alert("프로필 조회 기능이 준비되지 않았습니다."); return; } setProfileLoading(true); try { - const result = await window.sptLauncher.fetchProfiles(); + const result = await window.sptLauncher.fetchProfile({ + username: trimmedId + }); if (!result.ok) { - window.alert(result.error ?? "프로필 목록 조회에 실패했습니다."); + window.alert(result.error ?? "프로필 조회에 실패했습니다."); return; } if (!result.data) { - window.alert("프로필 목록 응답이 비어있습니다."); + window.alert("프로필 응답이 비어있습니다."); return; } - const profiles = extractProfileList(result.data); - if (profiles.length === 0) { - window.alert("프로필 목록이 비어있습니다."); - return; - } - - const normalizedId = trimmedId.toLowerCase(); - const matched = profiles.find((profileEntry) => { - const profileObject = asObject(profileEntry) ?? {}; - const info = asObject(profileObject.info ?? profileObject.Info) ?? profileObject; - const username = pickString( - info.username, - info.Username, - profileObject.username, - profileObject.Username, - info.nickname, - profileObject.nickname - ); - return username?.toLowerCase() === normalizedId; - }); - - if (!matched) { - window.alert("입력한 아이디와 일치하는 프로필이 없습니다."); - return; - } - - setProfile(extractProfile(matched, trimmedId)); + setProfile(extractProfile(result.data, trimmedId)); setHasSession(true); setScreen("main"); } catch (error) { diff --git a/src/renderer/vite-env.d.ts b/src/renderer/vite-env.d.ts index 1a2c342..30fb4f1 100644 --- a/src/renderer/vite-env.d.ts +++ b/src/renderer/vite-env.d.ts @@ -10,7 +10,7 @@ interface Window { error?: string; url?: string; }>; - fetchProfiles: () => Promise<{ + fetchProfile: (payload: { username: string }) => Promise<{ ok: boolean; status?: number; data?: unknown;