Implement launcher profile fetching guide and update API interactions. Added detailed documentation for the Launcher API, including profile login and fetch methods. Modified main process to handle profile fetching with session management, and updated renderer components to utilize the new profile fetching functionality.
This commit is contained in:
parent
7cfd513b73
commit
062911a9d0
|
|
@ -299,6 +299,31 @@
|
||||||
|
|
||||||
### Launcher API
|
### 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 '<json>'`
|
||||||
|
- 로그인 과정(Login flow)
|
||||||
|
- 요청(Request)
|
||||||
|
- `POST /launcher/profile/login`
|
||||||
|
- 바디(Body): `{"username":"<id>"}`
|
||||||
|
- 응답(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=<sessionId>`
|
||||||
|
- 바디(Body): `{"username":"<id>"}`
|
||||||
|
- 예시(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`
|
- `/launcher/ping`
|
||||||
역할(Role): 런처 핑(ping)
|
역할(Role): 런처 핑(ping)
|
||||||
사용법(Usage): 요청 바디 없음(EmptyRequestData)
|
사용법(Usage): 요청 바디 없음(EmptyRequestData)
|
||||||
|
|
@ -314,6 +339,10 @@
|
||||||
- `/launcher/profile/get`
|
- `/launcher/profile/get`
|
||||||
역할(Role): 계정 조회(get profile)
|
역할(Role): 계정 조회(get profile)
|
||||||
사용법(Usage): 요청 바디 `LoginRequestData`
|
사용법(Usage): 요청 바디 `LoginRequestData`
|
||||||
|
- `/launcher/profiles`
|
||||||
|
역할(Role): 프로필 목록 조회(get profile list)
|
||||||
|
사용법(Usage): 요청 바디 없음(EmptyRequestData)
|
||||||
|
주의(Note): 응답이 압축(compressed)되는 경우가 있어 `responsecompressed: 0` 헤더를 권장
|
||||||
- `/launcher/profile/change/username`
|
- `/launcher/profile/change/username`
|
||||||
역할(Role): 사용자명 변경(change username)
|
역할(Role): 사용자명 변경(change username)
|
||||||
사용법(Usage): 요청 바디 `ChangeRequestData`
|
사용법(Usage): 요청 바디 `ChangeRequestData`
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,11 @@ const checkServerHealth = async (): Promise<ServerHealthResult> => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const postJson = async <T>(path: string, body: unknown) => {
|
const postJson = async <T>(
|
||||||
|
path: string,
|
||||||
|
body: unknown,
|
||||||
|
extraHeaders: Record<string, string> = {}
|
||||||
|
) => {
|
||||||
const url = new URL(path, SERVER_BASE_URL).toString();
|
const url = new URL(path, SERVER_BASE_URL).toString();
|
||||||
const payload = JSON.stringify(body);
|
const payload = JSON.stringify(body);
|
||||||
|
|
||||||
|
|
@ -95,8 +99,9 @@ const postJson = async <T>(path: string, body: unknown) => {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Accept-Encoding": "identity",
|
"Accept-Encoding": "identity",
|
||||||
"Content-Encoding": "identity",
|
requestcompressed: "0",
|
||||||
"Content-Length": Buffer.byteLength(payload).toString()
|
responsecompressed: "0",
|
||||||
|
...extraHeaders
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -151,22 +156,25 @@ const postJson = async <T>(path: string, body: unknown) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getJson = async <T>(path: string) => {
|
const postText = async (path: string, body: unknown) => {
|
||||||
const url = new URL(path, SERVER_BASE_URL).toString();
|
const url = new URL(path, SERVER_BASE_URL).toString();
|
||||||
|
const payload = JSON.stringify(body);
|
||||||
|
|
||||||
return await new Promise<{
|
return await new Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
status?: number;
|
status?: number;
|
||||||
data?: T;
|
data?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
url: string;
|
url: string;
|
||||||
}>((resolve) => {
|
}>((resolve) => {
|
||||||
const request = net.request({
|
const request = net.request({
|
||||||
method: "GET",
|
method: "POST",
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
"Content-Type": "application/json",
|
||||||
|
Accept: "text/plain",
|
||||||
"Accept-Encoding": "identity",
|
"Accept-Encoding": "identity",
|
||||||
|
requestcompressed: "0",
|
||||||
responsecompressed: "0"
|
responsecompressed: "0"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -191,19 +199,7 @@ const getJson = async <T>(path: string) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const status = response.statusCode;
|
const status = response.statusCode;
|
||||||
const ok = typeof status === "number" ? status >= 200 && status < 400 : false;
|
const ok = typeof status === "number" ? status >= 200 && status < 400 : false;
|
||||||
|
resolve({ ok, status, data: rawData, url });
|
||||||
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 });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -217,6 +213,7 @@ const getJson = async <T>(path: string) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
request.write(payload);
|
||||||
request.end();
|
request.end();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -269,8 +266,22 @@ app.whenReady().then(() => {
|
||||||
return await checkServerHealth();
|
return await checkServerHealth();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("spt:fetchProfiles", async () => {
|
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
|
||||||
return await getJson("/launcher/profiles");
|
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) 로그를 남겨
|
// 개발/운영 모두에서 부팅 시 1회 즉시 체크(instant check) 로그를 남겨
|
||||||
|
|
|
||||||
|
|
@ -5,5 +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"),
|
||||||
fetchProfiles: () => ipcRenderer.invoke("spt:fetchProfiles")
|
fetchProfile: (payload: { username: string }) =>
|
||||||
|
ipcRenderer.invoke("spt:fetchProfile", payload)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -108,22 +108,6 @@ const App = () => {
|
||||||
return undefined;
|
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 extractProfile = (data: unknown, fallbackUsername: string) => {
|
||||||
const root = asObject(data);
|
const root = asObject(data);
|
||||||
const profileCandidate = Array.isArray(root?.profiles)
|
const profileCandidate = Array.isArray(root?.profiles)
|
||||||
|
|
@ -135,7 +119,16 @@ const App = () => {
|
||||||
return {
|
return {
|
||||||
username: pickString(info.username, info.Username, profileObject.username, fallbackUsername) ?? fallbackUsername,
|
username: pickString(info.username, info.Username, profileObject.username, fallbackUsername) ?? fallbackUsername,
|
||||||
nickname: pickString(info.nickname, info.Nickname, profileObject.nickname, profileObject.Nickname),
|
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),
|
side: pickString(info.side, info.Side, profileObject.side, profileObject.Side),
|
||||||
id: pickString(info.id, info._id, profileObject.id, profileObject._id, profileObject.profileId)
|
id: pickString(info.id, info._id, profileObject.id, profileObject._id, profileObject.profileId)
|
||||||
};
|
};
|
||||||
|
|
@ -148,52 +141,28 @@ const App = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.sptLauncher?.fetchProfiles) {
|
if (!window.sptLauncher?.fetchProfile) {
|
||||||
window.alert("프로필 목록 조회 기능이 준비되지 않았습니다.");
|
window.alert("프로필 조회 기능이 준비되지 않았습니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setProfileLoading(true);
|
setProfileLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.sptLauncher.fetchProfiles();
|
const result = await window.sptLauncher.fetchProfile({
|
||||||
|
username: trimmedId
|
||||||
|
});
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
window.alert(result.error ?? "프로필 목록 조회에 실패했습니다.");
|
window.alert(result.error ?? "프로필 조회에 실패했습니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.data) {
|
if (!result.data) {
|
||||||
window.alert("프로필 목록 응답이 비어있습니다.");
|
window.alert("프로필 응답이 비어있습니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profiles = extractProfileList(result.data);
|
setProfile(extractProfile(result.data, trimmedId));
|
||||||
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));
|
|
||||||
setHasSession(true);
|
setHasSession(true);
|
||||||
setScreen("main");
|
setScreen("main");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ interface Window {
|
||||||
error?: string;
|
error?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}>;
|
}>;
|
||||||
fetchProfiles: () => Promise<{
|
fetchProfile: (payload: { username: string }) => Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
status?: number;
|
status?: number;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue