This commit is contained in:
@@ -47,8 +47,22 @@ function jobTime(job: LauncherBuildJob) {
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
const launcherBuildJobPageSize = 3;
|
||||
const launcherBuildJobPageSize = 5;
|
||||
type BuildTarget = 'launcher-client' | 'launcher-backend';
|
||||
type BuildRange = 'week' | 'month' | 'all';
|
||||
|
||||
const buildRangeLabels: Record<BuildRange, string> = {
|
||||
week: '일주일',
|
||||
month: '한달',
|
||||
all: '전체'
|
||||
};
|
||||
|
||||
function buildRangeSince(range: BuildRange) {
|
||||
if (range === 'all') return undefined;
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - (range === 'week' ? 7 : 30));
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
function sanitizeObjectKeyPart(value: string) {
|
||||
return value
|
||||
@@ -89,6 +103,8 @@ export function LauncherBuildPage() {
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [range, setRange] = useState<BuildRange>('week');
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
|
||||
const activeVersion = useMemo(() => versions.find((item) => item.isActive) ?? versions[0], [versions]);
|
||||
@@ -100,19 +116,39 @@ export function LauncherBuildPage() {
|
||||
const backendDockerfile = backendSource?.dockerfile || 'Dockerfile';
|
||||
const uploadPath = launcherArtifactKey(version || activeVersion?.version || '0.0.0', branch, commitSha, artifactName);
|
||||
const downloadUrl = r2PublicBaseUrl ? `${r2PublicBaseUrl.replace(/\/$/, '')}/${encodeObjectKey(uploadPath)}` : '백엔드 R2 설정 기준으로 생성됩니다.';
|
||||
const sortedJobs = useMemo(() => [...jobs].filter((job) => job.target === target).sort((a, b) => jobTime(b) - jobTime(a)), [jobs, target]);
|
||||
const totalPages = Math.max(1, Math.ceil(sortedJobs.length / launcherBuildJobPageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const pageStart = (currentPage - 1) * launcherBuildJobPageSize;
|
||||
const pagedJobs = sortedJobs.slice(pageStart, pageStart + launcherBuildJobPageSize);
|
||||
const currentPage = page;
|
||||
const pagedJobs = jobs;
|
||||
const hasActiveJob = jobs.some((job) => job.status === 'PENDING' || job.status === 'RUNNING');
|
||||
|
||||
const loadBuildJobs = (nextPage = page, nextTarget = target, nextRange = range) => (
|
||||
listLauncherBuildJobs({
|
||||
target: nextTarget,
|
||||
page: nextPage,
|
||||
limit: launcherBuildJobPageSize + 1,
|
||||
since: buildRangeSince(nextRange)
|
||||
}).then((nextJobs) => {
|
||||
setJobs(nextJobs.slice(0, launcherBuildJobPageSize));
|
||||
setHasNextPage(nextJobs.length > launcherBuildJobPageSize);
|
||||
})
|
||||
);
|
||||
|
||||
const refresh = () => {
|
||||
setLoading(true);
|
||||
Promise.all([listLauncherVersions(), listLauncherBuildJobs(), getLauncherSource(), listLauncherBuildEnv()])
|
||||
Promise.all([
|
||||
listLauncherVersions(),
|
||||
listLauncherBuildJobs({
|
||||
target,
|
||||
page,
|
||||
limit: launcherBuildJobPageSize + 1,
|
||||
since: buildRangeSince(range)
|
||||
}),
|
||||
getLauncherSource(),
|
||||
listLauncherBuildEnv()
|
||||
])
|
||||
.then(([nextVersions, nextJobs, nextSource, nextBuildEnv]) => {
|
||||
setVersions(nextVersions);
|
||||
setJobs(nextJobs);
|
||||
setJobs(nextJobs.slice(0, launcherBuildJobPageSize));
|
||||
setHasNextPage(nextJobs.length > launcherBuildJobPageSize);
|
||||
setSource(nextSource.source);
|
||||
setBuildEnv(nextBuildEnv.variables ?? []);
|
||||
const nextVersion = nextVersions.find((item) => item.isActive) ?? nextVersions[0];
|
||||
@@ -130,6 +166,18 @@ export function LauncherBuildPage() {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [target, range]);
|
||||
|
||||
useEffect(() => {
|
||||
loadBuildJobs().catch((error: Error) => {
|
||||
setJobs([]);
|
||||
setHasNextPage(false);
|
||||
toast({ type: 'error', title: '빌드 작업 조회 실패', message: error.message });
|
||||
});
|
||||
}, [target, page, range]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!source) return;
|
||||
if (target === 'launcher-backend') {
|
||||
@@ -145,14 +193,13 @@ export function LauncherBuildPage() {
|
||||
useEffect(() => {
|
||||
if (!hasActiveJob) return;
|
||||
const timer = window.setInterval(() => {
|
||||
listLauncherBuildJobs()
|
||||
.then((nextJobs) => setJobs(nextJobs))
|
||||
loadBuildJobs()
|
||||
.catch((error: Error) => {
|
||||
toast({ type: 'error', title: '빌드 작업 갱신 실패', message: error.message });
|
||||
});
|
||||
}, 2000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [hasActiveJob, toast]);
|
||||
}, [hasActiveJob, toast, target, page, range]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedJob) return;
|
||||
@@ -176,8 +223,10 @@ export function LauncherBuildPage() {
|
||||
downloadUrl: isBackend ? undefined : r2PublicBaseUrl ? downloadUrl : undefined
|
||||
})
|
||||
.then(({ job }) => {
|
||||
setJobs((prev) => [job, ...prev]);
|
||||
setPage(1);
|
||||
setRange('week');
|
||||
setJobs((prev) => [job, ...prev].slice(0, launcherBuildJobPageSize));
|
||||
setHasNextPage(jobs.length >= launcherBuildJobPageSize);
|
||||
setSelectedJob(job);
|
||||
toast({ type: 'success', title: '빌드 작업', message: '런처 빌드 작업이 생성되었습니다.' });
|
||||
})
|
||||
@@ -312,7 +361,24 @@ export function LauncherBuildPage() {
|
||||
</Card>
|
||||
|
||||
<Card className="min-w-0 overflow-hidden">
|
||||
<CardHeader title="빌드 작업 목록" description={isBackend ? '런처 백엔드 이미지 빌드 작업입니다.' : '생성된 런처 클라이언트 빌드 작업과 업로드 계획입니다.'} />
|
||||
<CardHeader
|
||||
title="빌드 작업 목록"
|
||||
description={isBackend ? '런처 백엔드 이미지 빌드 작업입니다.' : '생성된 런처 클라이언트 빌드 작업과 업로드 계획입니다.'}
|
||||
action={
|
||||
<div className="flex items-center gap-2">
|
||||
{(Object.keys(buildRangeLabels) as BuildRange[]).map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
variant={range === item ? 'default' : 'outline'}
|
||||
className="h-8 px-3 text-[13px]"
|
||||
onClick={() => setRange(item)}
|
||||
>
|
||||
{buildRangeLabels[item]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="flex justify-end px-4 pt-4">
|
||||
<Button variant="outline" className="h-8 px-3 text-[13px]" onClick={refresh} disabled={loading}>
|
||||
<RotateCcw className={loading ? 'size-4 animate-spin' : 'size-4'} />
|
||||
@@ -330,7 +396,7 @@ export function LauncherBuildPage() {
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">No. {sortedJobs.length - (pageStart + index)}</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">No. {(currentPage - 1) * launcherBuildJobPageSize + index + 1}</span>
|
||||
<Badge tone={statusTone(job.status)}>{statusText(job.status)}</Badge>
|
||||
<span className="font-medium">{job.target}</span>
|
||||
</div>
|
||||
@@ -363,7 +429,7 @@ export function LauncherBuildPage() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 border-t border-border px-5 py-3 text-sm">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{loading ? '불러오는 중' : `총 ${sortedJobs.length}건 · ${currentPage} / ${totalPages}`}
|
||||
{loading ? '불러오는 중' : `페이지 ${currentPage}`}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -378,8 +444,8 @@ export function LauncherBuildPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-3 text-[13px]"
|
||||
disabled={currentPage >= totalPages}
|
||||
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => setPage((prev) => prev + 1)}
|
||||
>
|
||||
다음
|
||||
<ChevronRight className="size-4" />
|
||||
|
||||
Reference in New Issue
Block a user