feat: Initialize Electron application with core features for server health, Git tooling, mod synchronization, and user session management.
This commit is contained in:
parent
9486a624cb
commit
36c2d7121f
476
src/main/main.ts
476
src/main/main.ts
File diff suppressed because it is too large
Load Diff
|
|
@ -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
Loading…
Reference in New Issue