This commit is contained in:
+1
-1
@@ -483,7 +483,7 @@ function Sidebar({
|
|||||||
const isParentActive = hasChildren
|
const isParentActive = hasChildren
|
||||||
? item.children.some((child) => resolveNavView(child) === activeView)
|
? item.children.some((child) => resolveNavView(child) === activeView)
|
||||||
: activeView === resolveNavView(item);
|
: activeView === resolveNavView(item);
|
||||||
const isMenuOpen = Boolean(normalizedMenuSearch) || openMenus[item.label] || isParentActive;
|
const isMenuOpen = Boolean(normalizedMenuSearch) || (openMenus[item.label] ?? isParentActive);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.label}>
|
<div key={item.label}>
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export function LauncherBuildPage() {
|
|||||||
<div className="rounded-md border border-border bg-background p-3">
|
<div className="rounded-md border border-border bg-background p-3">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-muted-foreground">클라이언트 빌드 환경변수</p>
|
<p className="text-xs font-medium text-foreground">클라이언트 빌드 환경변수</p>
|
||||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">저장 후 실행한 빌드부터 반영됩니다.</p>
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">저장 후 실행한 빌드부터 반영됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={persistBuildEnv} disabled={buildEnvSaving}>
|
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={persistBuildEnv} disabled={buildEnvSaving}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Plus, RotateCcw } from 'lucide-react';
|
import { Play, Plus, RotateCcw } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||||
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
||||||
import { useToast } from '../../components/toast';
|
import { useToast } from '../../components/toast';
|
||||||
@@ -6,8 +6,12 @@ import { PageTitle } from '../../layout/AppLayout';
|
|||||||
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
|
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
|
||||||
import {
|
import {
|
||||||
createLauncherVersion,
|
createLauncherVersion,
|
||||||
|
deployLauncherBackend,
|
||||||
|
getLauncherBackendDeployment,
|
||||||
listLauncherBuildJobs,
|
listLauncherBuildJobs,
|
||||||
listLauncherVersions,
|
listLauncherVersions,
|
||||||
|
type LauncherBackendDeployment,
|
||||||
|
type LauncherBackendDeployResult,
|
||||||
type LauncherBuildJob,
|
type LauncherBuildJob,
|
||||||
type LauncherVersion
|
type LauncherVersion
|
||||||
} from './launcher-api';
|
} from './launcher-api';
|
||||||
@@ -22,11 +26,16 @@ function displayValue(value?: string | null) {
|
|||||||
|
|
||||||
export function LauncherVersionsPage() {
|
export function LauncherVersionsPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [tab, setTab] = useState<'client' | 'backend'>('client');
|
||||||
const [versions, setVersions] = useState<LauncherVersion[]>([]);
|
const [versions, setVersions] = useState<LauncherVersion[]>([]);
|
||||||
const [buildJobs, setBuildJobs] = useState<LauncherBuildJob[]>([]);
|
const [buildJobs, setBuildJobs] = useState<LauncherBuildJob[]>([]);
|
||||||
|
const [backendDeployment, setBackendDeployment] = useState<LauncherBackendDeployment | null>(null);
|
||||||
|
const [deployResult, setDeployResult] = useState<LauncherBackendDeployResult | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [releaseOpen, setReleaseOpen] = useState(false);
|
const [releaseOpen, setReleaseOpen] = useState(false);
|
||||||
|
const [deployOpen, setDeployOpen] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [deploying, setDeploying] = useState(false);
|
||||||
const [releaseForm, setReleaseForm] = useState({
|
const [releaseForm, setReleaseForm] = useState({
|
||||||
buildJobId: '',
|
buildJobId: '',
|
||||||
version: '',
|
version: '',
|
||||||
@@ -36,7 +45,15 @@ export function LauncherVersionsPage() {
|
|||||||
});
|
});
|
||||||
const loadingVisible = useMinimumLoading(loading);
|
const loadingVisible = useMinimumLoading(loading);
|
||||||
const launcher = useMemo(() => versions.find((item) => item.isActive) ?? versions[0], [versions]);
|
const launcher = useMemo(() => versions.find((item) => item.isActive) ?? versions[0], [versions]);
|
||||||
const successfulBuildJobs = useMemo(() => buildJobs.filter((job) => job.status === 'SUCCEEDED'), [buildJobs]);
|
const successfulBuildJobs = useMemo(
|
||||||
|
() => buildJobs.filter((job) => job.target !== 'launcher-backend' && job.status === 'SUCCEEDED'),
|
||||||
|
[buildJobs]
|
||||||
|
);
|
||||||
|
const successfulBackendBuildJobs = useMemo(
|
||||||
|
() => buildJobs.filter((job) => job.target === 'launcher-backend' && job.status === 'SUCCEEDED'),
|
||||||
|
[buildJobs]
|
||||||
|
);
|
||||||
|
const latestBackendBuild = backendDeployment?.latestBuild ?? successfulBackendBuildJobs[0] ?? null;
|
||||||
const selectedBuildJob = useMemo(
|
const selectedBuildJob = useMemo(
|
||||||
() => successfulBuildJobs.find((job) => job.id === releaseForm.buildJobId),
|
() => successfulBuildJobs.find((job) => job.id === releaseForm.buildJobId),
|
||||||
[releaseForm.buildJobId, successfulBuildJobs]
|
[releaseForm.buildJobId, successfulBuildJobs]
|
||||||
@@ -45,10 +62,11 @@ export function LauncherVersionsPage() {
|
|||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([listLauncherVersions(), listLauncherBuildJobs()])
|
Promise.all([listLauncherVersions(), listLauncherBuildJobs(), getLauncherBackendDeployment()])
|
||||||
.then(([nextVersions, nextBuildJobs]) => {
|
.then(([nextVersions, nextBuildJobs, nextBackendDeployment]) => {
|
||||||
setVersions(nextVersions);
|
setVersions(nextVersions);
|
||||||
setBuildJobs(nextBuildJobs);
|
setBuildJobs(nextBuildJobs);
|
||||||
|
setBackendDeployment(nextBackendDeployment);
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
toast({ type: 'error', title: '런처 버전 조회 실패', message: error.message });
|
toast({ type: 'error', title: '런처 버전 조회 실패', message: error.message });
|
||||||
@@ -107,29 +125,61 @@ export function LauncherVersionsPage() {
|
|||||||
.finally(() => setSaving(false));
|
.finally(() => setSaving(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deployBackend = () => {
|
||||||
|
if (!latestBackendBuild?.downloadUrl) return;
|
||||||
|
setDeploying(true);
|
||||||
|
deployLauncherBackend(latestBackendBuild.downloadUrl)
|
||||||
|
.then((result) => {
|
||||||
|
setDeployResult(result);
|
||||||
|
toast({ type: 'success', message: '런처 백엔드를 배포했습니다.' });
|
||||||
|
setDeployOpen(false);
|
||||||
|
refresh();
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
toast({ type: 'error', title: '백엔드 배포 실패', message: error.message });
|
||||||
|
})
|
||||||
|
.finally(() => setDeploying(false));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
eyebrow="런처 운영"
|
eyebrow="런처 운영"
|
||||||
title="버전 관리"
|
title="버전 관리"
|
||||||
description="스튜디오 메인 런처의 버전과 배포 파일 정보를 관리합니다."
|
description="스튜디오 메인 런처 클라이언트 릴리즈와 백엔드 배포를 관리합니다."
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={refresh} disabled={loading}>
|
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={refresh} disabled={loading}>
|
||||||
<RotateCcw className={loading ? 'size-4 animate-spin' : 'size-4'} />
|
<RotateCcw className={loading ? 'size-4 animate-spin' : 'size-4'} />
|
||||||
새로고침
|
새로고침
|
||||||
</Button>
|
</Button>
|
||||||
|
{tab === 'client' ? (
|
||||||
<Button className="h-8 px-3 text-[13px]" onClick={openRelease}>
|
<Button className="h-8 px-3 text-[13px]" onClick={openRelease}>
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
릴리즈 등록
|
릴리즈 등록
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button className="h-8 px-3 text-[13px]" onClick={() => setDeployOpen(true)} disabled={!latestBackendBuild?.downloadUrl}>
|
||||||
|
<Play className="size-4" />
|
||||||
|
백엔드 배포
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!loading && !launcher ? <Card className="px-5 py-12 text-center text-sm text-muted-foreground">등록된 런처 버전이 없습니다.</Card> : null}
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
|
<Button variant={tab === 'client' ? 'default' : 'outline'} onClick={() => setTab('client')}>
|
||||||
|
클라이언트 릴리즈
|
||||||
|
</Button>
|
||||||
|
<Button variant={tab === 'backend' ? 'default' : 'outline'} onClick={() => setTab('backend')}>
|
||||||
|
백엔드 배포
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{launcher ? (
|
{tab === 'client' && !loading && !launcher ? <Card className="px-5 py-12 text-center text-sm text-muted-foreground">등록된 런처 버전이 없습니다.</Card> : null}
|
||||||
|
|
||||||
|
{tab === 'client' && launcher ? (
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(360px,0.65fr)]">
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(360px,0.65fr)]">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title="메인 런처 배포 상태" description="현재 배포 기준으로 사용하는 런처 정보입니다." />
|
<CardHeader title="메인 런처 배포 상태" description="현재 배포 기준으로 사용하는 런처 정보입니다." />
|
||||||
@@ -183,6 +233,59 @@ export function LauncherVersionsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{tab === 'backend' ? (
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(360px,0.75fr)]">
|
||||||
|
<Card>
|
||||||
|
<CardHeader title="런처 백엔드 배포 상태" description="현재 k3s에서 실행 중인 런처 백엔드 이미지와 최신 성공 빌드입니다." />
|
||||||
|
<div className="space-y-4 p-5">
|
||||||
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">Namespace</p>
|
||||||
|
<p className="mt-2 font-mono text-sm font-medium">{backendDeployment?.namespace || 'studio-system'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">Deployment</p>
|
||||||
|
<p className="mt-2 font-mono text-sm font-medium">{backendDeployment?.deployment || 'studio-launcher-be'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">상태</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Badge tone={backendDeployment?.available ? 'green' : 'red'}>{backendDeployment?.available ? '조회됨' : '조회 실패'}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">현재 배포 이미지</p>
|
||||||
|
<p className="mt-2 break-all font-mono text-sm font-medium">{backendDeployment?.currentImage || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">최신 성공 빌드 이미지</p>
|
||||||
|
<p className="mt-2 break-all font-mono text-sm font-medium">{latestBackendBuild?.downloadUrl || '-'}</p>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||||
|
<span>버전 <b className="text-foreground">{latestBackendBuild?.version || '-'}</b></span>
|
||||||
|
<span>브랜치 <b className="text-foreground">{latestBackendBuild?.branch || '-'}</b></span>
|
||||||
|
<span>커밋 <b className="font-mono text-foreground">{latestBackendBuild?.commitSha || '-'}</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => setDeployOpen(true)} disabled={!latestBackendBuild?.downloadUrl || deploying}>
|
||||||
|
<Play className="size-4" />
|
||||||
|
백엔드 배포
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title="배포 로그" description="마지막 백엔드 배포 실행 결과입니다." />
|
||||||
|
<div className="p-5">
|
||||||
|
<pre className="min-h-64 overflow-auto rounded-md border border-neutral-800 bg-neutral-950 p-4 font-mono text-xs leading-6 text-neutral-100">
|
||||||
|
{deployResult?.logs || '아직 이 화면에서 실행한 배포 로그가 없습니다.'}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={releaseOpen}
|
open={releaseOpen}
|
||||||
title="릴리즈 등록"
|
title="릴리즈 등록"
|
||||||
@@ -237,6 +340,28 @@ export function LauncherVersionsPage() {
|
|||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={deployOpen}
|
||||||
|
title="런처 백엔드 배포"
|
||||||
|
onClose={() => setDeployOpen(false)}
|
||||||
|
footer={<><Button variant="outline" onClick={() => setDeployOpen(false)}>취소</Button><Button onClick={deployBackend} disabled={deploying || !latestBackendBuild?.downloadUrl}>{deploying ? '배포 중' : '배포'}</Button></>}
|
||||||
|
>
|
||||||
|
<div className="space-y-4 text-sm">
|
||||||
|
<div className="rounded-md border border-border bg-muted/20 px-4 py-3">
|
||||||
|
<p className="font-medium">최신 성공 빌드 이미지로 런처 백엔드를 배포합니다.</p>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">Deployment 이미지가 교체되고 rollout 완료까지 대기합니다.</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">배포 이미지</p>
|
||||||
|
<p className="mt-2 break-all font-mono text-xs font-medium">{latestBackendBuild?.downloadUrl || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-background p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">현재 이미지</p>
|
||||||
|
<p className="mt-2 break-all font-mono text-xs text-muted-foreground">{backendDeployment?.currentImage || '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<LoadingModal open={loadingVisible} />
|
<LoadingModal open={loadingVisible} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,25 @@ export type LauncherBuildJob = {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LauncherBackendDeployment = {
|
||||||
|
namespace: string;
|
||||||
|
deployment: string;
|
||||||
|
container: string;
|
||||||
|
currentImage?: string | null;
|
||||||
|
available: boolean;
|
||||||
|
source?: LauncherSource['backend'];
|
||||||
|
latestBuild?: LauncherBuildJob | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LauncherBackendDeployResult = {
|
||||||
|
deployed: boolean;
|
||||||
|
namespace: string;
|
||||||
|
deployment: string;
|
||||||
|
container: string;
|
||||||
|
image: string;
|
||||||
|
logs: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type LauncherBuildEnvVariable = {
|
export type LauncherBuildEnvVariable = {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -199,6 +218,17 @@ export function listLauncherBuildJobs() {
|
|||||||
return apiRequest<LauncherBuildJob[]>('/launcher/builds');
|
return apiRequest<LauncherBuildJob[]>('/launcher/builds');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLauncherBackendDeployment() {
|
||||||
|
return apiRequest<LauncherBackendDeployment>('/launcher/backend-deployment');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deployLauncherBackend(image: string) {
|
||||||
|
return apiRequest<LauncherBackendDeployResult>('/launcher/backend-deployment', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ image })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function listLauncherBuildEnv() {
|
export function listLauncherBuildEnv() {
|
||||||
return apiRequest<{ variables: LauncherBuildEnvVariable[] }>('/launcher/build-env');
|
return apiRequest<{ variables: LauncherBuildEnvVariable[] }>('/launcher/build-env');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user