Compare commits
2 Commits
9f7244185d
...
18dcf789c8
| Author | SHA1 | Date |
|---|---|---|
|
|
18dcf789c8 | |
|
|
03a3f85974 |
190
src/main/main.ts
190
src/main/main.ts
|
|
@ -22,6 +22,7 @@ const SERVER_HEALTHCHECK_TIMEOUT_MS = APP_CONFIG.serverHealthcheckTimeoutMs;
|
||||||
const SERVER_REQUEST_TIMEOUT_MS = APP_CONFIG.serverRequestTimeoutMs;
|
const SERVER_REQUEST_TIMEOUT_MS = APP_CONFIG.serverRequestTimeoutMs;
|
||||||
const COMMAND_TIMEOUT_MS = APP_CONFIG.commandTimeoutMs;
|
const COMMAND_TIMEOUT_MS = APP_CONFIG.commandTimeoutMs;
|
||||||
const INSTALL_TIMEOUT_MS = APP_CONFIG.installTimeoutMs;
|
const INSTALL_TIMEOUT_MS = APP_CONFIG.installTimeoutMs;
|
||||||
|
const SESSION_TTL_MS = APP_CONFIG.sessionTtlMs;
|
||||||
|
|
||||||
type CommandCheckResult = {
|
type CommandCheckResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
|
@ -71,6 +72,13 @@ type SptInstallInfo = {
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SessionRecord = {
|
||||||
|
username: string;
|
||||||
|
sessionId: string;
|
||||||
|
expiresAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
type InstallStep = {
|
type InstallStep = {
|
||||||
name: string;
|
name: string;
|
||||||
result: CommandRunResult;
|
result: CommandRunResult;
|
||||||
|
|
@ -224,11 +232,63 @@ const getGitPaths = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOD_VERSION_FILE_NAME = "mod-version.json";
|
const MOD_VERSION_FILE_NAME = "mod-version.json";
|
||||||
|
const SESSION_FILE_NAME = "session.json";
|
||||||
|
|
||||||
const getModVersionFilePath = () => {
|
const getModVersionFilePath = () => {
|
||||||
return path.join(app.getPath("userData"), MOD_VERSION_FILE_NAME);
|
return path.join(app.getPath("userData"), MOD_VERSION_FILE_NAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSessionRecordPath = () => {
|
||||||
|
return path.join(app.getPath("userData"), SESSION_FILE_NAME);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readSessionRecord = async (): Promise<SessionRecord | null> => {
|
||||||
|
const filePath = getSessionRecordPath();
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as SessionRecord;
|
||||||
|
if (
|
||||||
|
typeof parsed.username !== "string" ||
|
||||||
|
typeof parsed.sessionId !== "string" ||
|
||||||
|
typeof parsed.expiresAt !== "number" ||
|
||||||
|
typeof parsed.updatedAt !== "number"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeSessionRecord = async (payload: { username: string; sessionId: string }) => {
|
||||||
|
const filePath = getSessionRecordPath();
|
||||||
|
const record: SessionRecord = {
|
||||||
|
username: payload.username,
|
||||||
|
sessionId: payload.sessionId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + SESSION_TTL_MS
|
||||||
|
};
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(record, null, 2), "utf8");
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshSessionRecord = async (payload: { username: string; sessionId: string }) => {
|
||||||
|
return await writeSessionRecord(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSessionRecord = async () => {
|
||||||
|
const filePath = getSessionRecordPath();
|
||||||
|
await fs.rm(filePath, { force: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSessionExpired = (record: SessionRecord | null) => {
|
||||||
|
if (!record) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return record.expiresAt <= Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
const readModVersionRecord = async () => {
|
const readModVersionRecord = async () => {
|
||||||
const filePath = getModVersionFilePath();
|
const filePath = getModVersionFilePath();
|
||||||
try {
|
try {
|
||||||
|
|
@ -890,6 +950,28 @@ const requestSessionId = async (username: string) => {
|
||||||
return { ok: true, sessionId, url: loginResult.url };
|
return { ok: true, sessionId, url: loginResult.url };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSessionInvalidStatus = (status?: number) => status === 401 || status === 403;
|
||||||
|
|
||||||
|
const getSessionIdForUser = async (username: string) => {
|
||||||
|
const cached = await readSessionRecord();
|
||||||
|
if (cached && cached.username === username && !isSessionExpired(cached)) {
|
||||||
|
await refreshSessionRecord({ username: cached.username, sessionId: cached.sessionId });
|
||||||
|
return { ok: true, sessionId: cached.sessionId, url: SERVER_BASE_URL, source: "cache" as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached && cached.username !== username) {
|
||||||
|
await clearSessionRecord();
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginResult = await requestSessionId(username);
|
||||||
|
if (!loginResult.ok) {
|
||||||
|
return loginResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeSessionRecord({ username, sessionId: loginResult.sessionId });
|
||||||
|
return { ok: true, sessionId: loginResult.sessionId, url: loginResult.url, source: "login" as const };
|
||||||
|
};
|
||||||
|
|
||||||
const registerProfile = async (username: string) => {
|
const registerProfile = async (username: string) => {
|
||||||
return await postJson("/launcher/profile/register", {
|
return await postJson("/launcher/profile/register", {
|
||||||
username,
|
username,
|
||||||
|
|
@ -1035,6 +1117,7 @@ app.whenReady().then(() => {
|
||||||
|
|
||||||
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
|
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
|
||||||
const username = payload.username.trim();
|
const username = payload.username.trim();
|
||||||
|
let activeSessionId: string | undefined;
|
||||||
|
|
||||||
const fetchProfileInfo = async (): Promise<{
|
const fetchProfileInfo = async (): Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
|
@ -1043,7 +1126,7 @@ app.whenReady().then(() => {
|
||||||
error?: string;
|
error?: string;
|
||||||
url: string;
|
url: string;
|
||||||
}> => {
|
}> => {
|
||||||
const sessionResult = await requestSessionId(username);
|
const sessionResult = await getSessionIdForUser(username);
|
||||||
if (!sessionResult.ok) {
|
if (!sessionResult.ok) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -1051,6 +1134,7 @@ app.whenReady().then(() => {
|
||||||
url: sessionResult.url
|
url: sessionResult.url
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
activeSessionId = sessionResult.sessionId;
|
||||||
|
|
||||||
return await postJson(
|
return await postJson(
|
||||||
"/launcher/profile/info",
|
"/launcher/profile/info",
|
||||||
|
|
@ -1061,8 +1145,30 @@ app.whenReady().then(() => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoResult = await fetchProfileInfo();
|
let infoResult = await fetchProfileInfo();
|
||||||
|
if (!infoResult.ok && isSessionInvalidStatus(infoResult.status)) {
|
||||||
|
const relogin = await requestSessionId(username);
|
||||||
|
if (!relogin.ok) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: relogin.error ?? "login_failed",
|
||||||
|
url: relogin.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await writeSessionRecord({ username, sessionId: relogin.sessionId });
|
||||||
|
activeSessionId = relogin.sessionId;
|
||||||
|
infoResult = await postJson(
|
||||||
|
"/launcher/profile/info",
|
||||||
|
{ username },
|
||||||
|
{
|
||||||
|
Cookie: `PHPSESSID=${relogin.sessionId}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
if (infoResult.ok && infoResult.data) {
|
if (infoResult.ok && infoResult.data) {
|
||||||
|
if (activeSessionId) {
|
||||||
|
await refreshSessionRecord({ username, sessionId: activeSessionId });
|
||||||
|
}
|
||||||
return infoResult;
|
return infoResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1073,12 +1179,13 @@ app.whenReady().then(() => {
|
||||||
ipcMain.handle("spt:downloadProfile", async (_event, payload: { username: string }) => {
|
ipcMain.handle("spt:downloadProfile", async (_event, payload: { username: string }) => {
|
||||||
const username = payload.username.trim();
|
const username = payload.username.trim();
|
||||||
|
|
||||||
const sessionResult = await requestSessionId(username);
|
const sessionResult = await getSessionIdForUser(username);
|
||||||
if (!sessionResult.ok) {
|
if (!sessionResult.ok) {
|
||||||
return sessionResult;
|
return sessionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const infoResult = await postJson(
|
let activeSessionId = sessionResult.sessionId;
|
||||||
|
let infoResult = await postJson(
|
||||||
"/launcher/profile/info",
|
"/launcher/profile/info",
|
||||||
{ username },
|
{ username },
|
||||||
{
|
{
|
||||||
|
|
@ -1086,10 +1193,28 @@ app.whenReady().then(() => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!infoResult.ok && isSessionInvalidStatus(infoResult.status)) {
|
||||||
|
const relogin = await requestSessionId(username);
|
||||||
|
if (!relogin.ok) {
|
||||||
|
return relogin;
|
||||||
|
}
|
||||||
|
await writeSessionRecord({ username, sessionId: relogin.sessionId });
|
||||||
|
activeSessionId = relogin.sessionId;
|
||||||
|
infoResult = await postJson(
|
||||||
|
"/launcher/profile/info",
|
||||||
|
{ username },
|
||||||
|
{
|
||||||
|
Cookie: `PHPSESSID=${relogin.sessionId}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!infoResult.ok || !infoResult.data) {
|
if (!infoResult.ok || !infoResult.data) {
|
||||||
return infoResult;
|
return infoResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await refreshSessionRecord({ username, sessionId: activeSessionId });
|
||||||
|
|
||||||
const downloadsDir = app.getPath("downloads");
|
const downloadsDir = app.getPath("downloads");
|
||||||
const fileName = `spt-profile-${username}-${Date.now()}.json`;
|
const fileName = `spt-profile-${username}-${Date.now()}.json`;
|
||||||
const filePath = path.join(downloadsDir, fileName);
|
const filePath = path.join(downloadsDir, fileName);
|
||||||
|
|
@ -1107,18 +1232,71 @@ app.whenReady().then(() => {
|
||||||
ipcMain.handle("spt:resetProfile", async (_event, payload: { username: string }) => {
|
ipcMain.handle("spt:resetProfile", async (_event, payload: { username: string }) => {
|
||||||
const username = payload.username.trim();
|
const username = payload.username.trim();
|
||||||
|
|
||||||
const sessionResult = await requestSessionId(username);
|
const sessionResult = await getSessionIdForUser(username);
|
||||||
if (!sessionResult.ok) {
|
if (!sessionResult.ok) {
|
||||||
return sessionResult;
|
return sessionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await postJson(
|
let activeSessionId = sessionResult.sessionId;
|
||||||
|
let wipeResult = await postJson(
|
||||||
"/launcher/profile/change/wipe",
|
"/launcher/profile/change/wipe",
|
||||||
{ username },
|
{ username },
|
||||||
{
|
{
|
||||||
Cookie: `PHPSESSID=${sessionResult.sessionId}`
|
Cookie: `PHPSESSID=${sessionResult.sessionId}`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!wipeResult.ok && isSessionInvalidStatus(wipeResult.status)) {
|
||||||
|
const relogin = await requestSessionId(username);
|
||||||
|
if (!relogin.ok) {
|
||||||
|
return relogin;
|
||||||
|
}
|
||||||
|
await writeSessionRecord({ username, sessionId: relogin.sessionId });
|
||||||
|
activeSessionId = relogin.sessionId;
|
||||||
|
wipeResult = await postJson(
|
||||||
|
"/launcher/profile/change/wipe",
|
||||||
|
{ username },
|
||||||
|
{
|
||||||
|
Cookie: `PHPSESSID=${relogin.sessionId}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wipeResult.ok) {
|
||||||
|
await refreshSessionRecord({ username, sessionId: activeSessionId });
|
||||||
|
}
|
||||||
|
return wipeResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("spt:resumeSession", async () => {
|
||||||
|
const record = await readSessionRecord();
|
||||||
|
if (!record || isSessionExpired(record)) {
|
||||||
|
await clearSessionRecord();
|
||||||
|
return { ok: false, error: "session_expired", url: SERVER_BASE_URL };
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoResult = await postJson(
|
||||||
|
"/launcher/profile/info",
|
||||||
|
{ username: record.username },
|
||||||
|
{
|
||||||
|
Cookie: `PHPSESSID=${record.sessionId}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!infoResult.ok && isSessionInvalidStatus(infoResult.status)) {
|
||||||
|
await clearSessionRecord();
|
||||||
|
return { ok: false, error: "session_expired", url: infoResult.url };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infoResult.ok) {
|
||||||
|
await refreshSessionRecord({ username: record.username, sessionId: record.sessionId });
|
||||||
|
}
|
||||||
|
return infoResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("spt:clearSession", async () => {
|
||||||
|
await clearSessionRecord();
|
||||||
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 개발/운영 모두에서 부팅 시 1회 즉시 체크(instant check) 로그를 남겨
|
// 개발/운영 모두에서 부팅 시 1회 즉시 체크(instant check) 로그를 남겨
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,7 @@ contextBridge.exposeInMainWorld("sptLauncher", {
|
||||||
downloadProfile: (payload: { username: string }) =>
|
downloadProfile: (payload: { username: string }) =>
|
||||||
ipcRenderer.invoke("spt:downloadProfile", payload),
|
ipcRenderer.invoke("spt:downloadProfile", payload),
|
||||||
resetProfile: (payload: { username: string }) =>
|
resetProfile: (payload: { username: string }) =>
|
||||||
ipcRenderer.invoke("spt:resetProfile", payload)
|
ipcRenderer.invoke("spt:resetProfile", payload),
|
||||||
|
resumeSession: () => ipcRenderer.invoke("spt:resumeSession"),
|
||||||
|
clearSession: () => ipcRenderer.invoke("spt:clearSession")
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,56 @@ const App = () => {
|
||||||
const gitCheckedRef = useRef(false);
|
const gitCheckedRef = useRef(false);
|
||||||
const modCheckInFlightRef = useRef(false);
|
const modCheckInFlightRef = useRef(false);
|
||||||
const modCheckedRef = useRef(false);
|
const modCheckedRef = useRef(false);
|
||||||
|
const sessionResumeAttemptedRef = useRef(false);
|
||||||
const transitionTimeoutRef = useRef<number | undefined>();
|
const transitionTimeoutRef = useRef<number | undefined>();
|
||||||
const simulateToolsMissingRef = useRef(false);
|
const simulateToolsMissingRef = useRef(false);
|
||||||
|
|
||||||
|
const asObject = (value: unknown) =>
|
||||||
|
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||||
|
const pickString = (...values: unknown[]) =>
|
||||||
|
values.find((value) => typeof value === "string" && value.trim().length > 0) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
const pickNumber = (...values: unknown[]) => {
|
||||||
|
const candidate = values.find(
|
||||||
|
(value) => typeof value === "number" || (typeof value === "string" && value.trim() !== "")
|
||||||
|
);
|
||||||
|
if (typeof candidate === "number") {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
if (typeof candidate === "string") {
|
||||||
|
const parsed = Number(candidate);
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractProfile = (data: unknown, fallbackUsername: string) => {
|
||||||
|
const root = asObject(data);
|
||||||
|
const profileCandidate = Array.isArray(root?.profiles)
|
||||||
|
? root?.profiles?.[0]
|
||||||
|
: root?.profile ?? root?.data ?? root;
|
||||||
|
const profileObject = asObject(profileCandidate) ?? {};
|
||||||
|
const info = asObject(profileObject.info ?? profileObject.Info) ?? profileObject;
|
||||||
|
|
||||||
|
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,
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const runGitCheck = useCallback(async (force = false) => {
|
const runGitCheck = useCallback(async (force = false) => {
|
||||||
if (force) {
|
if (force) {
|
||||||
gitCheckedRef.current = false;
|
gitCheckedRef.current = false;
|
||||||
|
|
@ -113,7 +160,7 @@ const App = () => {
|
||||||
setGitCheckInProgress(true);
|
setGitCheckInProgress(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (IS_DEV && simulateToolsMissingRef.current) {
|
if (DEV_UI_ENABLED && simulateToolsMissingRef.current) {
|
||||||
const simulated = {
|
const simulated = {
|
||||||
git: { ok: false, command: "git", error: "simulated_missing" },
|
git: { ok: false, command: "git", error: "simulated_missing" },
|
||||||
lfs: { ok: false, command: "git lfs", error: "simulated_missing" },
|
lfs: { ok: false, command: "git lfs", error: "simulated_missing" },
|
||||||
|
|
@ -255,6 +302,7 @@ const App = () => {
|
||||||
setProfile(undefined);
|
setProfile(undefined);
|
||||||
setSkipToolsCheck(false);
|
setSkipToolsCheck(false);
|
||||||
setDevProceedReady(false);
|
setDevProceedReady(false);
|
||||||
|
sessionResumeAttemptedRef.current = false;
|
||||||
setScreen("serverCheck");
|
setScreen("serverCheck");
|
||||||
clearTransitionTimeout();
|
clearTransitionTimeout();
|
||||||
},
|
},
|
||||||
|
|
@ -303,7 +351,7 @@ const App = () => {
|
||||||
|
|
||||||
if (serverRecoveryActive) {
|
if (serverRecoveryActive) {
|
||||||
setServerRecoveryActive(false);
|
setServerRecoveryActive(false);
|
||||||
if (IS_DEV) {
|
if (DEV_UI_ENABLED) {
|
||||||
setDevProceedReady(true);
|
setDevProceedReady(true);
|
||||||
} else {
|
} else {
|
||||||
scheduleScreenTransition("login");
|
scheduleScreenTransition("login");
|
||||||
|
|
@ -315,7 +363,7 @@ const App = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_DEV) {
|
if (DEV_UI_ENABLED) {
|
||||||
setDevProceedReady(true);
|
setDevProceedReady(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -364,6 +412,50 @@ const App = () => {
|
||||||
void runHealthCheck(serverRecoveryActive ? "recovery" : "initial");
|
void runHealthCheck(serverRecoveryActive ? "recovery" : "initial");
|
||||||
}, [screen, serverRecoveryActive, runHealthCheck]);
|
}, [screen, serverRecoveryActive, runHealthCheck]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
screen !== "serverCheck" ||
|
||||||
|
!serverHealthy ||
|
||||||
|
hasSession ||
|
||||||
|
serverCheckInProgress ||
|
||||||
|
!serverCheckedAt
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sessionResumeAttemptedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.sptLauncher?.resumeSession) {
|
||||||
|
sessionResumeAttemptedRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionResumeAttemptedRef.current = true;
|
||||||
|
void (async () => {
|
||||||
|
const result = await window.sptLauncher.resumeSession();
|
||||||
|
if (!result.ok || !result.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resumedProfile = extractProfile(result.data, loginId.trim());
|
||||||
|
setProfile(resumedProfile);
|
||||||
|
setHasSession(true);
|
||||||
|
if (resumedProfile.username) {
|
||||||
|
setLoginId(resumedProfile.username);
|
||||||
|
}
|
||||||
|
clearTransitionTimeout();
|
||||||
|
setScreen("main");
|
||||||
|
})();
|
||||||
|
}, [
|
||||||
|
screen,
|
||||||
|
serverHealthy,
|
||||||
|
hasSession,
|
||||||
|
serverCheckInProgress,
|
||||||
|
serverCheckedAt,
|
||||||
|
loginId,
|
||||||
|
clearTransitionTimeout,
|
||||||
|
extractProfile
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (screen !== "serverCheck") {
|
if (screen !== "serverCheck") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -526,52 +618,6 @@ const App = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asObject = (value: unknown) =>
|
|
||||||
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
|
||||||
const pickString = (...values: unknown[]) =>
|
|
||||||
values.find((value) => typeof value === "string" && value.trim().length > 0) as
|
|
||||||
| string
|
|
||||||
| undefined;
|
|
||||||
const pickNumber = (...values: unknown[]) => {
|
|
||||||
const candidate = values.find(
|
|
||||||
(value) => typeof value === "number" || (typeof value === "string" && value.trim() !== "")
|
|
||||||
);
|
|
||||||
if (typeof candidate === "number") {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
if (typeof candidate === "string") {
|
|
||||||
const parsed = Number(candidate);
|
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractProfile = (data: unknown, fallbackUsername: string) => {
|
|
||||||
const root = asObject(data);
|
|
||||||
const profileCandidate = Array.isArray(root?.profiles)
|
|
||||||
? root?.profiles?.[0]
|
|
||||||
: root?.profile ?? root?.data ?? root;
|
|
||||||
const profileObject = asObject(profileCandidate) ?? {};
|
|
||||||
const info = asObject(profileObject.info ?? profileObject.Info) ?? profileObject;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
const trimmedId = loginId.trim();
|
const trimmedId = loginId.trim();
|
||||||
if (!trimmedId) {
|
if (!trimmedId) {
|
||||||
|
|
@ -630,6 +676,9 @@ const App = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
|
if (window.sptLauncher?.clearSession) {
|
||||||
|
void window.sptLauncher.clearSession();
|
||||||
|
}
|
||||||
setHasSession(false);
|
setHasSession(false);
|
||||||
setProfile(undefined);
|
setProfile(undefined);
|
||||||
setScreen("login");
|
setScreen("login");
|
||||||
|
|
@ -870,7 +919,9 @@ const App = () => {
|
||||||
className="ghost"
|
className="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSkipToolsCheck(true);
|
setSkipToolsCheck(true);
|
||||||
scheduleScreenTransition(IS_DEV ? "login" : hasSession ? "main" : "login");
|
scheduleScreenTransition(
|
||||||
|
DEV_UI_ENABLED ? "login" : hasSession ? "main" : "login"
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
나중에 진행
|
나중에 진행
|
||||||
|
|
@ -878,7 +929,7 @@ const App = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{IS_DEV &&
|
{DEV_UI_ENABLED &&
|
||||||
serverHealthy &&
|
serverHealthy &&
|
||||||
(skipToolsCheck ||
|
(skipToolsCheck ||
|
||||||
Boolean(gitCheckResult?.git.ok && gitCheckResult?.lfs.ok)) && (
|
Boolean(gitCheckResult?.git.ok && gitCheckResult?.lfs.ok)) && (
|
||||||
|
|
@ -894,7 +945,7 @@ const App = () => {
|
||||||
다음으로 이동
|
다음으로 이동
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{IS_DEV && (
|
{DEV_UI_ENABLED && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ghost"
|
className="ghost"
|
||||||
|
|
@ -950,22 +1001,6 @@ const App = () => {
|
||||||
>
|
>
|
||||||
비밀번호 변경
|
비밀번호 변경
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
const fallbackId = loginId.trim() || "demo";
|
|
||||||
setProfile({
|
|
||||||
username: fallbackId,
|
|
||||||
nickname: "Demo",
|
|
||||||
level: 1
|
|
||||||
});
|
|
||||||
setHasSession(true);
|
|
||||||
setScreen("main");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
유효 세션 가정(데모)
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,16 @@ interface Window {
|
||||||
error?: string;
|
error?: string;
|
||||||
url: string;
|
url: string;
|
||||||
}>;
|
}>;
|
||||||
|
resumeSession: () => Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
status?: number;
|
||||||
|
data?: unknown;
|
||||||
|
error?: string;
|
||||||
|
url: string;
|
||||||
|
}>;
|
||||||
|
clearSession: () => Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export const APP_CONFIG = {
|
||||||
serverHealthcheckPath: "/launcher/ping",
|
serverHealthcheckPath: "/launcher/ping",
|
||||||
serverHealthcheckTimeoutMs: 2000,
|
serverHealthcheckTimeoutMs: 2000,
|
||||||
serverRequestTimeoutMs: 4000,
|
serverRequestTimeoutMs: 4000,
|
||||||
|
sessionTtlMs: 7 * 24 * 60 * 60 * 1000,
|
||||||
commandTimeoutMs: 4000,
|
commandTimeoutMs: 4000,
|
||||||
installTimeoutMs: 10 * 60 * 1000,
|
installTimeoutMs: 10 * 60 * 1000,
|
||||||
healthcheckIntervalMs: 10000,
|
healthcheckIntervalMs: 10000,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue