improve launcher management screens
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-03 17:18:47 +09:00
parent c6e99b1efc
commit 461c81a142
4 changed files with 455 additions and 67 deletions
+60 -10
View File
@@ -81,6 +81,7 @@ export function LauncherBuildPage() {
const [commitSha, setCommitSha] = useState('');
const [jobs, setJobs] = useState<LauncherBuildJob[]>([]);
const [selectedJob, setSelectedJob] = useState<LauncherBuildJob | null>(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const loadingVisible = useMinimumLoading(loading);
@@ -90,7 +91,6 @@ export function LauncherBuildPage() {
const backendSource = source?.backend;
const clientSource = source?.client;
const artifactName = `autonomy-studio-launcher-${version || activeVersion?.version || '0.0.0'}.deb`;
const clientArtifactPreview = 'package.json 버전과 실제 .deb 파일명 기준으로 빌드 후 자동 확정';
const backendImageName = backendSource?.image || 'localhost:5000/studio-launcher-be';
const backendImageTag = `${backendImageName.replace(/\/$/, '')}:${version || '0.1.0'}`;
const backendDockerfile = backendSource?.dockerfile || 'Dockerfile';
@@ -158,6 +158,7 @@ export function LauncherBuildPage() {
}, [jobs, selectedJob]);
const createJob = () => {
setConfirmOpen(false);
setLoading(true);
createLauncherBuildJob({
target,
@@ -188,7 +189,7 @@ export function LauncherBuildPage() {
title="빌드 관리"
description="런처 클라이언트 설치 파일과 백엔드 컨테이너 이미지를 구성요소별로 빌드합니다."
actions={
<Button className="h-8 px-3 text-[13px]" onClick={createJob} disabled={isBackend && !version.trim()}>
<Button className="h-8 px-3 text-[13px]" onClick={() => setConfirmOpen(true)} disabled={isBackend && !version.trim()}>
<Play className="size-4" />
실행
</Button>
@@ -231,14 +232,14 @@ export function LauncherBuildPage() {
<span className="text-xs font-medium text-muted-foreground">Commit SHA</span>
<input className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring" value={commitSha} onChange={(event) => setCommitSha(event.target.value)} placeholder="비워두면 브랜치 최신 커밋" />
</label>
<div className="rounded-md border border-border bg-background p-3">
<p className="text-xs text-muted-foreground">{isBackend ? '이미지' : '산출물'}</p>
<p className="mt-2 break-all font-mono text-xs text-foreground">{isBackend ? backendImageTag : clientArtifactPreview}</p>
<p className="mt-2 text-xs text-muted-foreground">{isBackend ? 'Dockerfile' : 'R2 Object Key'}</p>
<p className="mt-1 break-all font-mono text-xs text-foreground">{isBackend ? backendDockerfile : clientArtifactPreview}</p>
<p className="mt-2 text-xs text-muted-foreground">{isBackend ? 'Repository' : '다운로드 URL'}</p>
<p className="mt-1 break-all font-mono text-xs text-foreground">{isBackend ? backendSource?.repository || '-' : clientArtifactPreview}</p>
</div>
{isBackend ? (
<div className="rounded-md border border-border bg-background p-3">
<p className="text-xs text-muted-foreground">이미지</p>
<p className="mt-2 break-all font-mono text-xs text-foreground">{backendImageTag}</p>
<p className="mt-2 text-xs text-muted-foreground">Repository</p>
<p className="mt-1 break-all font-mono text-xs text-foreground">{backendSource?.repository || '-'}</p>
</div>
) : null}
</div>
</Card>
@@ -331,6 +332,55 @@ export function LauncherBuildPage() {
{selectedJob?.logs || '작업을 선택하면 로그가 표시됩니다.'}
</pre>
</Modal>
<Modal
open={confirmOpen}
title="빌드 실행 확인"
onClose={() => setConfirmOpen(false)}
footer={
<>
<Button variant="outline" onClick={() => setConfirmOpen(false)}>취소</Button>
<Button onClick={createJob} disabled={loading || (isBackend && !version.trim())}>
<Play className="size-4" />
실행
</Button>
</>
}
>
<div className="space-y-3 text-sm">
<div className="rounded-md border border-border bg-muted/20 px-4 py-3">
<p className="font-medium">{isBackend ? '런처 백엔드 이미지 빌드' : '런처 클라이언트 설치 파일 빌드'}를 실행합니다.</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
실행 후 빌드 작업이 생성되고, 작업 목록에서 진행 상태와 로그를 확인할 수 있습니다.
</p>
</div>
<div className="grid gap-2 rounded-md border border-border bg-background p-4">
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">대상</span>
<span className="font-medium">{isBackend ? '백엔드' : '클라이언트'}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">저장소</span>
<span className="truncate font-medium">
{isBackend ? backendSource?.repository || '-' : clientSource?.repository || activeVersion?.repository || '-'}
</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">브랜치</span>
<span className="font-medium">{branch}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">커밋</span>
<span className="font-mono text-xs font-medium">{commitSha.trim() || '브랜치 최신'}</span>
</div>
{isBackend ? (
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">이미지</span>
<span className="truncate font-mono text-xs font-medium">{backendImageTag}</span>
</div>
) : null}
</div>
</div>
</Modal>
<LoadingModal open={loadingVisible} />
</>
);
+16 -56
View File
@@ -25,10 +25,7 @@ type SourceTarget = 'launcher-client' | 'launcher-backend';
type SourceForm = {
repository: string;
branch: string;
commitSha: string;
image: string;
dockerfile: string;
};
const targetLabels: Record<SourceTarget, { title: string; description: string; defaultOrg: string }> = {
@@ -74,10 +71,7 @@ function sshCloneUrl(repository?: string | null, fallbackOrg?: string) {
function emptyForm(): SourceForm {
return {
repository: '',
branch: 'release',
commitSha: '',
image: 'localhost:5000/studio-launcher-be',
dockerfile: 'Dockerfile'
image: 'localhost:5000/studio-launcher-be'
};
}
@@ -135,6 +129,7 @@ export function LauncherSourcePage() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const loadingVisible = useMinimumLoading(loading);
const sourceBranch = 'develop';
const activeVersion = useMemo(() => versions.find((version) => version.isActive) ?? versions[0], [versions]);
const activeForm = forms[activeTarget];
@@ -164,17 +159,11 @@ export function LauncherSourcePage() {
setForms({
'launcher-client': {
repository: clientRepository,
branch: sourceResult.source.client?.branch ?? sourceResult.source.branch ?? nextVersion?.branch ?? 'release',
commitSha: sourceResult.source.client?.commitSha ?? sourceResult.source.commitSha ?? nextVersion?.commitSha ?? '',
image: '',
dockerfile: ''
image: ''
},
'launcher-backend': {
repository: backendRepository,
branch: sourceResult.source.backend?.branch ?? 'release',
commitSha: sourceResult.source.backend?.commitSha ?? '',
image: sourceResult.source.backend?.image ?? 'localhost:5000/studio-launcher-be',
dockerfile: sourceResult.source.backend?.dockerfile ?? 'Dockerfile'
image: sourceResult.source.backend?.image ?? 'localhost:5000/studio-launcher-be'
}
});
})
@@ -199,7 +188,7 @@ export function LauncherSourcePage() {
let canceled = false;
setCommitsLoading(true);
setCommitsError('');
listGiteaCommits(repository.owner, repository.repo, activeForm.branch || 'develop', 5)
listGiteaCommits(repository.owner, repository.repo, sourceBranch, 5)
.then((nextCommits) => {
if (!canceled) setCommits(nextCommits);
})
@@ -216,7 +205,7 @@ export function LauncherSourcePage() {
return () => {
canceled = true;
};
}, [activeForm.repository, activeForm.branch, activeTargetMeta.defaultOrg]);
}, [activeForm.repository, activeTargetMeta.defaultOrg, sourceBranch]);
const save = () => {
if (!activeForm.repository.trim()) return;
@@ -224,10 +213,10 @@ export function LauncherSourcePage() {
updateLauncherSource({
target: activeTarget,
repository: activeForm.repository.trim(),
branch: activeForm.branch.trim() || undefined,
commitSha: activeForm.commitSha.trim() || undefined,
branch: sourceBranch,
commitSha: '',
image: activeTarget === 'launcher-backend' ? activeForm.image.trim() || undefined : undefined,
dockerfile: activeTarget === 'launcher-backend' ? activeForm.dockerfile.trim() || undefined : undefined
dockerfile: activeTarget === 'launcher-backend' ? 'Dockerfile' : undefined
})
.then(() => {
toast({ type: 'success', title: '소스 연결', message: `${activeTargetMeta.title} 소스 연결을 저장했습니다.` });
@@ -287,30 +276,13 @@ export function LauncherSourcePage() {
{repos.map((repo) => <option key={repoFullName(repo)} value={repoFullName(repo)} />)}
</datalist>
</label>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">브랜치</span>
<select
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring"
value={activeForm.branch}
onChange={(event) => updateForm({ branch: event.target.value })}
>
<option value="develop">develop</option>
<option value="release">release</option>
<option value="main">main</option>
</select>
</label>
<label className="block md:col-span-3">
<span className="text-xs font-medium text-muted-foreground">Commit SHA</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
value={activeForm.commitSha}
onChange={(event) => updateForm({ commitSha: event.target.value })}
placeholder="비워두면 브랜치 최신 커밋 기준"
/>
</label>
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
<p className="text-xs font-medium text-muted-foreground">빌드 기준</p>
<p className="mt-1 text-sm font-medium text-foreground">develop 최신 성공 빌드</p>
</div>
{activeTarget === 'launcher-backend' ? (
<>
<label className="block md:col-span-2">
<label className="block md:col-span-3">
<span className="text-xs font-medium text-muted-foreground">이미지 이름</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
@@ -319,21 +291,12 @@ export function LauncherSourcePage() {
placeholder="localhost:5000/studio-launcher-be"
/>
</label>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">Dockerfile</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
value={activeForm.dockerfile}
onChange={(event) => updateForm({ dockerfile: event.target.value })}
placeholder="Dockerfile"
/>
</label>
</>
) : null}
<div className="flex justify-end md:col-span-3">
<Button onClick={save} disabled={!activeForm.repository.trim() || saving}>
<Link2 className="size-4" />
저장
저장소 연결 저장
</Button>
</div>
</div>
@@ -377,10 +340,7 @@ export function LauncherSourcePage() {
</div>
<Card className="mt-4 min-w-0 overflow-hidden">
<CardHeader
title="최근 커밋"
description={`${activeTargetMeta.title} 저장소의 ${activeForm.branch || 'develop'} 브랜치 기준 최근 5개 커밋입니다.`}
/>
<CardHeader title="최근 커밋" description={`${activeTargetMeta.title} 저장소의 develop 브랜치 기준 최근 5개 커밋입니다.`} />
<div className="overflow-x-auto">
<table className="min-w-[760px] w-full text-left text-sm">
<thead className="border-y border-border bg-muted/30 text-xs text-muted-foreground">
+5 -1
View File
@@ -174,7 +174,11 @@ export function LauncherVersionsPage() {
</Card>
<Card>
<CardHeader title="릴리즈 노트" description="최근 런처 배포 메모입니다." />
<div className="p-5 text-sm leading-6 text-muted-foreground">{launcher.releaseNotes || '-'}</div>
<div className="p-5">
<div className="min-h-32 rounded-md border border-neutral-800 bg-neutral-950 px-4 py-3 text-sm leading-6 text-neutral-100">
<p className="whitespace-pre-wrap">{launcher.releaseNotes || '-'}</p>
</div>
</div>
</Card>
</div>
) : null}