Compare commits
No commits in common. "18dcf789c88e636bfcb04da5eb5801325cdbd078" and "9f7244185d74b4f09eeeff1005e096366177f840" have entirely different histories.
18dcf789c8
...
9f7244185d
190
src/main/main.ts
190
src/main/main.ts
|
|
@ -22,7 +22,6 @@ const SERVER_HEALTHCHECK_TIMEOUT_MS = APP_CONFIG.serverHealthcheckTimeoutMs;
|
|||
const SERVER_REQUEST_TIMEOUT_MS = APP_CONFIG.serverRequestTimeoutMs;
|
||||
const COMMAND_TIMEOUT_MS = APP_CONFIG.commandTimeoutMs;
|
||||
const INSTALL_TIMEOUT_MS = APP_CONFIG.installTimeoutMs;
|
||||
const SESSION_TTL_MS = APP_CONFIG.sessionTtlMs;
|
||||
|
||||
type CommandCheckResult = {
|
||||
ok: boolean;
|
||||
|
|
@ -72,13 +71,6 @@ type SptInstallInfo = {
|
|||
error?: string;
|
||||
};
|
||||
|
||||
type SessionRecord = {
|
||||
username: string;
|
||||
sessionId: string;
|
||||
expiresAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type InstallStep = {
|
||||
name: string;
|
||||
result: CommandRunResult;
|
||||
|
|
@ -232,63 +224,11 @@ const getGitPaths = async () => {
|
|||
};
|
||||
|
||||
const MOD_VERSION_FILE_NAME = "mod-version.json";
|
||||
const SESSION_FILE_NAME = "session.json";
|
||||
|
||||
const getModVersionFilePath = () => {
|
||||
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 filePath = getModVersionFilePath();
|
||||
try {
|
||||
|
|
@ -950,28 +890,6 @@ const requestSessionId = async (username: string) => {
|
|||
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) => {
|
||||
return await postJson("/launcher/profile/register", {
|
||||
username,
|
||||
|
|
@ -1117,7 +1035,6 @@ app.whenReady().then(() => {
|
|||
|
||||
ipcMain.handle("spt:fetchProfile", async (_event, payload: { username: string }) => {
|
||||
const username = payload.username.trim();
|
||||
let activeSessionId: string | undefined;
|
||||
|
||||
const fetchProfileInfo = async (): Promise<{
|
||||
ok: boolean;
|
||||
|
|
@ -1126,7 +1043,7 @@ app.whenReady().then(() => {
|
|||
error?: string;
|
||||
url: string;
|
||||
}> => {
|
||||
const sessionResult = await getSessionIdForUser(username);
|
||||
const sessionResult = await requestSessionId(username);
|
||||
if (!sessionResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -1134,7 +1051,6 @@ app.whenReady().then(() => {
|
|||
url: sessionResult.url
|
||||
};
|
||||
}
|
||||
activeSessionId = sessionResult.sessionId;
|
||||
|
||||
return await postJson(
|
||||
"/launcher/profile/info",
|
||||
|
|
@ -1145,30 +1061,8 @@ app.whenReady().then(() => {
|
|||
);
|
||||
};
|
||||
|
||||
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}`
|
||||
}
|
||||
);
|
||||
}
|
||||
const infoResult = await fetchProfileInfo();
|
||||
if (infoResult.ok && infoResult.data) {
|
||||
if (activeSessionId) {
|
||||
await refreshSessionRecord({ username, sessionId: activeSessionId });
|
||||
}
|
||||
return infoResult;
|
||||
}
|
||||
|
||||
|
|
@ -1179,13 +1073,12 @@ app.whenReady().then(() => {
|
|||
ipcMain.handle("spt:downloadProfile", async (_event, payload: { username: string }) => {
|
||||
const username = payload.username.trim();
|
||||
|
||||
const sessionResult = await getSessionIdForUser(username);
|
||||
const sessionResult = await requestSessionId(username);
|
||||
if (!sessionResult.ok) {
|
||||
return sessionResult;
|
||||
}
|
||||
|
||||
let activeSessionId = sessionResult.sessionId;
|
||||
let infoResult = await postJson(
|
||||
const infoResult = await postJson(
|
||||
"/launcher/profile/info",
|
||||
{ username },
|
||||
{
|
||||
|
|
@ -1193,28 +1086,10 @@ 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) {
|
||||
return infoResult;
|
||||
}
|
||||
|
||||
await refreshSessionRecord({ username, sessionId: activeSessionId });
|
||||
|
||||
const downloadsDir = app.getPath("downloads");
|
||||
const fileName = `spt-profile-${username}-${Date.now()}.json`;
|
||||
const filePath = path.join(downloadsDir, fileName);
|
||||
|
|
@ -1232,71 +1107,18 @@ app.whenReady().then(() => {
|
|||
ipcMain.handle("spt:resetProfile", async (_event, payload: { username: string }) => {
|
||||
const username = payload.username.trim();
|
||||
|
||||
const sessionResult = await getSessionIdForUser(username);
|
||||
const sessionResult = await requestSessionId(username);
|
||||
if (!sessionResult.ok) {
|
||||
return sessionResult;
|
||||
}
|
||||
|
||||
let activeSessionId = sessionResult.sessionId;
|
||||
let wipeResult = await postJson(
|
||||
return await postJson(
|
||||
"/launcher/profile/change/wipe",
|
||||
{ username },
|
||||
{
|
||||
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) 로그를 남겨
|
||||
|
|
|
|||
|
|
@ -22,7 +22,5 @@ contextBridge.exposeInMainWorld("sptLauncher", {
|
|||
downloadProfile: (payload: { username: string }) =>
|
||||
ipcRenderer.invoke("spt:downloadProfile", payload),
|
||||
resetProfile: (payload: { username: string }) =>
|
||||
ipcRenderer.invoke("spt:resetProfile", payload),
|
||||
resumeSession: () => ipcRenderer.invoke("spt:resumeSession"),
|
||||
clearSession: () => ipcRenderer.invoke("spt:clearSession")
|
||||
ipcRenderer.invoke("spt:resetProfile", payload)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,56 +97,9 @@ const App = () => {
|
|||
const gitCheckedRef = useRef(false);
|
||||
const modCheckInFlightRef = useRef(false);
|
||||
const modCheckedRef = useRef(false);
|
||||
const sessionResumeAttemptedRef = useRef(false);
|
||||
const transitionTimeoutRef = useRef<number | undefined>();
|
||||
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) => {
|
||||
if (force) {
|
||||
gitCheckedRef.current = false;
|
||||
|
|
@ -160,7 +113,7 @@ const App = () => {
|
|||
setGitCheckInProgress(true);
|
||||
|
||||
try {
|
||||
if (DEV_UI_ENABLED && simulateToolsMissingRef.current) {
|
||||
if (IS_DEV && simulateToolsMissingRef.current) {
|
||||
const simulated = {
|
||||
git: { ok: false, command: "git", error: "simulated_missing" },
|
||||
lfs: { ok: false, command: "git lfs", error: "simulated_missing" },
|
||||
|
|
@ -302,7 +255,6 @@ const App = () => {
|
|||
setProfile(undefined);
|
||||
setSkipToolsCheck(false);
|
||||
setDevProceedReady(false);
|
||||
sessionResumeAttemptedRef.current = false;
|
||||
setScreen("serverCheck");
|
||||
clearTransitionTimeout();
|
||||
},
|
||||
|
|
@ -351,7 +303,7 @@ const App = () => {
|
|||
|
||||
if (serverRecoveryActive) {
|
||||
setServerRecoveryActive(false);
|
||||
if (DEV_UI_ENABLED) {
|
||||
if (IS_DEV) {
|
||||
setDevProceedReady(true);
|
||||
} else {
|
||||
scheduleScreenTransition("login");
|
||||
|
|
@ -363,7 +315,7 @@ const App = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (DEV_UI_ENABLED) {
|
||||
if (IS_DEV) {
|
||||
setDevProceedReady(true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -412,50 +364,6 @@ const App = () => {
|
|||
void runHealthCheck(serverRecoveryActive ? "recovery" : "initial");
|
||||
}, [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(() => {
|
||||
if (screen !== "serverCheck") {
|
||||
return;
|
||||
|
|
@ -618,6 +526,52 @@ 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 trimmedId = loginId.trim();
|
||||
if (!trimmedId) {
|
||||
|
|
@ -676,9 +630,6 @@ const App = () => {
|
|||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
if (window.sptLauncher?.clearSession) {
|
||||
void window.sptLauncher.clearSession();
|
||||
}
|
||||
setHasSession(false);
|
||||
setProfile(undefined);
|
||||
setScreen("login");
|
||||
|
|
@ -919,9 +870,7 @@ const App = () => {
|
|||
className="ghost"
|
||||
onClick={() => {
|
||||
setSkipToolsCheck(true);
|
||||
scheduleScreenTransition(
|
||||
DEV_UI_ENABLED ? "login" : hasSession ? "main" : "login"
|
||||
);
|
||||
scheduleScreenTransition(IS_DEV ? "login" : hasSession ? "main" : "login");
|
||||
}}
|
||||
>
|
||||
나중에 진행
|
||||
|
|
@ -929,7 +878,7 @@ const App = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{DEV_UI_ENABLED &&
|
||||
{IS_DEV &&
|
||||
serverHealthy &&
|
||||
(skipToolsCheck ||
|
||||
Boolean(gitCheckResult?.git.ok && gitCheckResult?.lfs.ok)) && (
|
||||
|
|
@ -945,7 +894,7 @@ const App = () => {
|
|||
다음으로 이동
|
||||
</button>
|
||||
)}
|
||||
{DEV_UI_ENABLED && (
|
||||
{IS_DEV && (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
|
|
@ -1001,6 +950,22 @@ const App = () => {
|
|||
>
|
||||
비밀번호 변경
|
||||
</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>
|
||||
</section>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -140,16 +140,6 @@ interface Window {
|
|||
error?: string;
|
||||
url: string;
|
||||
}>;
|
||||
resumeSession: () => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
url: string;
|
||||
}>;
|
||||
clearSession: () => Promise<{
|
||||
ok: boolean;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export const APP_CONFIG = {
|
|||
serverHealthcheckPath: "/launcher/ping",
|
||||
serverHealthcheckTimeoutMs: 2000,
|
||||
serverRequestTimeoutMs: 4000,
|
||||
sessionTtlMs: 7 * 24 * 60 * 60 * 1000,
|
||||
commandTimeoutMs: 4000,
|
||||
installTimeoutMs: 10 * 60 * 1000,
|
||||
healthcheckIntervalMs: 10000,
|
||||
|
|
|
|||
Loading…
Reference in New Issue