feat: Initialize Electron application with core features for server health, Git tooling, mod synchronization, and user session management.

This commit is contained in:
이정수 2026-01-31 00:06:17 +09:00
parent 9486a624cb
commit 36c2d7121f
4 changed files with 3760 additions and 4999 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1088,7 +1088,7 @@ const App = () => {
)}
{screen === "main" && (
<main className="main-layout compact">
<main className="main-layout">
<section className="card top-bar">
<div className="profile-summary">
<div className="profile-title">
@ -1105,7 +1105,7 @@ const App = () => {
</div>
<div className="profile-actions">
<button type="button" onClick={handleProfileDownload} disabled={profileActionInProgress}>
{profileActionInProgress ? "처리 중..." : "프로필 다운로드"}
{profileActionInProgress ? "처리 중..." : "다운로드"}
</button>
<button
type="button"
@ -1113,7 +1113,7 @@ const App = () => {
onClick={handleProfileReset}
disabled={profileActionInProgress}
>
</button>
<button type="button" className="ghost" onClick={handleLogout}>
@ -1121,219 +1121,137 @@ const App = () => {
</div>
</section>
<section className="card spt-runner">
<div className="spt-info">
<section className="card spt-runner-card">
<div className="section-head">
<h2>SPT </h2>
<h2>SPT </h2>
<button
type="button"
className="launch-button-header"
onClick={handleLaunch}
disabled={!startReady}
>
<span className="launch-icon">🚀</span>
<div className="launch-info">
<span className="launch-text">{syncInProgress ? "동기화 중..." : "게임 시작"}</span>
{/* <span className="launch-sub">Server & Launcher</span> */}
</div>
</button>
</div>
<div className="spt-rows-container">
{/* Install Path Row */}
<div className="spt-row">
<div className="row-header">
<span className="label"> </span>
<span className={`status ${sptInstallInfo?.ok ? "ok" : "blocked"}`}>
{sptInstallInfo?.ok ? "경로 확인됨" : "경로 필요"}
{sptInstallInfo?.ok ? "확인됨" : "확인 필요"}
</span>
</div>
<div className="spt-row">
<span className="label"> </span>
<span className="value">{sptInstallInfo?.path ?? "-"}</span>
<div className="row-actions">
<div className="row-body">
<span className="value" title={sptInstallInfo?.path}>{sptInstallInfo?.path ?? "-"}</span>
{sptInstallInfo?.source && (
<span className="sub-helper">
({sptInstallInfo.source === "auto" ? "자동" : "수동"})
</span>
)}
</div>
<div className="row-controls">
<button
type="button"
className="ghost icon-button"
onClick={async () => {
if (!window.sptLauncher?.pickSptInstallPath) {
window.alert("경로 선택 기능이 준비되지 않았습니다.");
window.alert("준비 안됨");
return;
}
const result = await window.sptLauncher.pickSptInstallPath();
if (!result.ok) {
if (result.error === "invalid_spt_path") {
setSptInstallMessage("SPT 폴더가 아닙니다. 다시 선택해주세요.");
}
if (!result.ok && result.error === "invalid_spt_path") {
setSptInstallMessage("유효하지 않은 SPT 폴더입니다.");
return;
}
setSptInstallMessage(undefined);
await runSptInstallCheck();
}}
title="경로 변경"
>
📁
📂
</button>
<button type="button" className="ghost" onClick={runSptInstallCheck}>
</button>
</div>
</div>
{sptInstallInfo?.source && (
<div className="helper">
: {sptInstallInfo.source === "auto" ? "자동 탐지" : "저장됨"}
</div>
)}
{/* Mod Version Row */}
<div className="spt-row">
<div className="row-header">
<span className="label"> </span>
<span className={`status ${modStatusClass}`}>{modStatusLabel}</span>
</div>
<div className="row-body">
<span className="value">
{modVersionStatus?.currentTag ?? "-"} {modVersionStatus?.latestTag ?? "-"}
</span>
<span className={`status ${modStatusClass}`}>{modStatusLabel}</span>
<div className="row-actions">
{modVersionStatus?.latestMinorTags && modVersionStatus.latestMinorTags.length > 0 && (
<span className="sub-helper">
(+{modVersionStatus.latestMinorTags.length} )
</span>
)}
</div>
<div className="row-controls">
<select
value={selectedModTag}
onChange={(event) => setSelectedModTag(event.target.value)}
disabled={modTagOptions.length === 0 || syncInProgress}
aria-label="모드 버전 선택"
className="version-select"
>
{modTagOptions.length === 0 && <option value=""> </option>}
<option value="">...</option>
{modTagOptions.map((tag) => (
<option key={tag} value={tag}>
{tag}
</option>
<option key={tag} value={tag}>{tag}</option>
))}
</select>
<button
type="button"
className="icon-action"
className="ghost"
onClick={() => performModSync("optional", selectedModTag)}
disabled={syncInProgress || !selectedModTag}
aria-label="적용"
title="적용(패치)"
title="선택 버전 적용"
>
</button>
<div className="sync-menu">
<button
type="button"
className="icon-action"
className="ghost icon-button"
onClick={() => performModSync("optional")}
disabled={syncInProgress || !modVersionStatus?.latestTag}
aria-label="동기화"
title="동기화"
title="최신 동기화"
>
</button>
<div className="sync-menu-panel" role="menu" aria-label="동기화 메뉴">
<button
type="button"
className="ghost"
onClick={() => performModSync("optional")}
disabled={syncInProgress || !modVersionStatus?.latestTag}
>
</button>
<button
type="button"
className="ghost"
onClick={() => performModSync("force")}
disabled={syncInProgress}
>
</button>
{/* Hover menu logic handled by CSS usually, keeping simple for now */}
</div>
</div>
<button
type="button"
className="icon-action ghost"
onClick={() => runModVersionCheck(true)}
disabled={modCheckInProgress}
aria-label="새로고침"
title="새로고침"
>
</button>
</div>
</div>
{modVersionStatus?.latestMinorTags && modVersionStatus.latestMinorTags.length > 0 && (
<div className="helper">
: {modVersionStatus.latestMinorTags.join(", ")}
</div>
)}
{/* Notices Area */}
<div className="notices-area">
{modVersionStatus?.status === "majorMinorMismatch" && (
<div className="notice">
<strong> </strong>
<div className="modal-help">
/ . .
</div>
</div>
<div className="notice warning"> ! .</div>
)}
{modVersionStatus?.status === "patchMismatch" && (
<div className="notice">
<strong> </strong>
<div className="modal-help">
. .
</div>
</div>
)}
{sptInstallMessage && <div className="notice">{sptInstallMessage}</div>}
{modCheckMessage && <div className="notice">{modCheckMessage}</div>}
{DEV_UI_ENABLED && (
<label className="checkbox">
<input
type="checkbox"
checked={simulateSyncFail}
onChange={(event) => setSimulateSyncFail(event.target.checked)}
/>
</label>
)}
{DEV_UI_ENABLED && (
<div className="notice">
<strong> </strong>
<div className="modal-help">
.
</div>
<div className="login-form">
<input
type="text"
placeholder="예: D:\\SPT"
value={sptPathInput}
onChange={(event) => setSptPathInput(event.target.value)}
/>
</div>
<label className="checkbox">
<input
type="checkbox"
checked={allowMissingSptPath}
onChange={(event) => setAllowMissingSptPath(event.target.checked)}
/>
</label>
<button
type="button"
className="ghost"
onClick={async () => {
const value = sptPathInput.trim();
if (!value) {
window.alert("경로를 입력해주세요.");
return;
}
const result = await window.sptLauncher?.setSptInstallPath({
path: value,
allowMissing: allowMissingSptPath
});
if (!result?.ok) {
setSptInstallMessage(result?.error ?? "경로 저장에 실패했습니다.");
return;
}
setSptInstallMessage(undefined);
await runSptInstallCheck();
}}
>
</button>
</div>
<div className="notice info"> .</div>
)}
{sptInstallMessage && <div className="notice error">{sptInstallMessage}</div>}
</div>
</section>
<section className="card spt-launch">
<button type="button" onClick={handleLaunch} disabled={!startReady}>
{syncInProgress ? "동기화 중..." : "게임 시작"}
</button>
</section>
<footer className="footer compact-footer footer-right-only">
<div className="footer-right">
<footer className="footer">
<div className="tool-led">
<span className="tool-label"></span>
<span className={`led ${serverStatusLedClass}`} aria-label="서버 상태" />
</div>
<button type="button" className="ghost" onClick={handleOpenToolsModal}>
</button>
<div className="tool-led">
<span className="tool-label">Git</span>
<span className={`led ${gitLedClass}`} aria-label="Git 상태" />
@ -1342,7 +1260,9 @@ const App = () => {
<span className="tool-label">LFS</span>
<span className={`led ${lfsLedClass}`} aria-label="Git LFS 상태" />
</div>
</div>
<button type="button" className="ghost status-button" onClick={handleOpenToolsModal}>
</button>
</footer>
</main>
)}

File diff suppressed because it is too large Load Diff

6757
yarn.lock

File diff suppressed because it is too large Load Diff