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

File diff suppressed because it is too large Load Diff

6757
yarn.lock

File diff suppressed because it is too large Load Diff