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,228 +1121,148 @@ 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
<span className={`status ${sptInstallInfo?.ok ? "ok" : "blocked"}`}> type="button"
{sptInstallInfo?.ok ? "경로 확인됨" : "경로 필요"} className="launch-button-header"
</span> onClick={handleLaunch}
</div> 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="spt-row">
<span className="label">설치 경로</span> <div className="row-header">
<span className="value">{sptInstallInfo?.path ?? "-"}</span> <span className="label">설치 경로</span>
<div className="row-actions"> <span className={`status ${sptInstallInfo?.ok ? "ok" : "blocked"}`}>
<button {sptInstallInfo?.ok ? "확인됨" : "확인 필요"}
type="button" </span>
className="ghost icon-button" </div>
onClick={async () => { <div className="row-body">
if (!window.sptLauncher?.pickSptInstallPath) { <span className="value" title={sptInstallInfo?.path}>{sptInstallInfo?.path ?? "-"}</span>
window.alert("경로 선택 기능이 준비되지 않았습니다."); {sptInstallInfo?.source && (
return; <span className="sub-helper">
} ({sptInstallInfo.source === "auto" ? "자동" : "수동"})
const result = await window.sptLauncher.pickSptInstallPath(); </span>
if (!result.ok) { )}
if (result.error === "invalid_spt_path") { </div>
setSptInstallMessage("SPT 폴더가 아닙니다. 다시 선택해주세요."); <div className="row-controls">
}
return;
}
setSptInstallMessage(undefined);
await runSptInstallCheck();
}}
>
📁
</button>
<button type="button" className="ghost" onClick={runSptInstallCheck}>
재확인
</button>
</div>
</div>
{sptInstallInfo?.source && (
<div className="helper">
확인 방식: {sptInstallInfo.source === "auto" ? "자동 탐지" : "저장됨"}
</div>
)}
<div className="spt-row">
<span className="label">모드 버전</span>
<span className="value">
{modVersionStatus?.currentTag ?? "-"} → {modVersionStatus?.latestTag ?? "-"}
</span>
<span className={`status ${modStatusClass}`}>{modStatusLabel}</span>
<div className="row-actions">
<select
value={selectedModTag}
onChange={(event) => setSelectedModTag(event.target.value)}
disabled={modTagOptions.length === 0 || syncInProgress}
aria-label="모드 버전 선택"
>
{modTagOptions.length === 0 && <option value="">버전 없음</option>}
{modTagOptions.map((tag) => (
<option key={tag} value={tag}>
{tag}
</option>
))}
</select>
<button
type="button"
className="icon-action"
onClick={() => performModSync("optional", selectedModTag)}
disabled={syncInProgress || !selectedModTag}
aria-label="적용"
title="적용(패치)"
>
✓
</button>
<div className="sync-menu">
<button <button
type="button" type="button"
className="icon-action" className="ghost icon-button"
onClick={() => performModSync("optional")} onClick={async () => {
disabled={syncInProgress || !modVersionStatus?.latestTag} if (!window.sptLauncher?.pickSptInstallPath) {
aria-label="동기화" window.alert("준비 안됨");
title="동기화" return;
}
const result = await window.sptLauncher.pickSptInstallPath();
if (!result.ok && result.error === "invalid_spt_path") {
setSptInstallMessage("유효하지 않은 SPT 폴더입니다.");
return;
}
setSptInstallMessage(undefined);
await runSptInstallCheck();
}}
title="경로 변경"
> >
⟳ 📂
</button> </button>
<div className="sync-menu-panel" role="menu" aria-label="동기화 메뉴"> <button type="button" className="ghost" onClick={runSptInstallCheck}>
재확인
</button>
</div>
</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>
{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}
className="version-select"
>
<option value="">선택...</option>
{modTagOptions.map((tag) => (
<option key={tag} value={tag}>{tag}</option>
))}
</select>
<button
type="button"
className="ghost"
onClick={() => performModSync("optional", selectedModTag)}
disabled={syncInProgress || !selectedModTag}
title="선택 버전 적용"
>
적용
</button>
<div className="sync-menu">
<button <button
type="button" type="button"
className="ghost" className="ghost icon-button"
onClick={() => performModSync("optional")} onClick={() => performModSync("optional")}
disabled={syncInProgress || !modVersionStatus?.latestTag} disabled={syncInProgress || !modVersionStatus?.latestTag}
title="최신 동기화"
> >
동기화 ⟳
</button>
<button
type="button"
className="ghost"
onClick={() => performModSync("force")}
disabled={syncInProgress}
>
덮어쓰기
</button> </button>
{/* Hover menu logic handled by CSS usually, keeping simple for now */}
</div> </div>
</div> </div>
<button
type="button"
className="icon-action ghost"
onClick={() => runModVersionCheck(true)}
disabled={modCheckInProgress}
aria-label="새로고침"
title="새로고침"
>
⟲
</button>
</div>
</div> </div>
{modVersionStatus?.latestMinorTags && modVersionStatus.latestMinorTags.length > 0 && ( </div>
<div className="helper">
최신 패치 목록: {modVersionStatus.latestMinorTags.join(", ")} {/* Notices Area */}
</div> <div className="notices-area">
)}
{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}> <div className="tool-led">
{syncInProgress ? "동기화 중..." : "게임 시작"}
</button>
</section>
<footer className="footer compact-footer footer-right-only">
<div className="footer-right">
<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}> <div className="tool-led">
도구 상태
</button>
<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 상태" />
</div> </div>
<div className="tool-led"> <div className="tool-led">
<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