diff --git a/src/pages/launcher/LauncherBuildPage.tsx b/src/pages/launcher/LauncherBuildPage.tsx index 30b2be8..0f05336 100644 --- a/src/pages/launcher/LauncherBuildPage.tsx +++ b/src/pages/launcher/LauncherBuildPage.tsx @@ -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 = { + 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('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() { - + + {(Object.keys(buildRangeLabels) as BuildRange[]).map((item) => ( + + ))} + + } + />
+ ))} +
+ } + />
@@ -347,22 +418,23 @@ export function LauncherSourcePage() { + {commitsLoading ? ( - + ) : null} {!commitsLoading && commitsError ? ( - + ) : null} {!commitsLoading && !commitsError && commits.length === 0 ? ( - + ) : null} {!commitsLoading && !commitsError ? commits.map((commit) => ( @@ -371,13 +443,76 @@ export function LauncherSourcePage() { + )) : null}
SHA1 메시지 날짜상세
커밋을 불러오는 중입니다.커밋을 불러오는 중입니다.
커밋 조회 실패: {commitsError}커밋 조회 실패: {commitsError}
표시할 커밋이 없습니다.표시할 커밋이 없습니다.
{commit.sha.slice(0, 10)} {commitMessage(commit)} {relativeTime(commitDate(commit))} + +
+
+ 페이지 {commitPage} +
+ + +
+
+ setDiffTarget(null)} + className="max-w-5xl" + footer={} + > + {diffTarget ? ( +
+
+
+ {diffTarget.sha.slice(0, 10)} + {commitMessage(diffTarget)} +
+

+ {commitAuthor(diffTarget)} · {relativeTime(commitDate(diffTarget))} +

+
+ {diffLoading ? ( +
Diff를 불러오는 중입니다.
+ ) : diffError ? ( +
Diff 조회 실패: {diffError}
+ ) : ( +
+                {diffContent || '표시할 변경 내역이 없습니다.'}
+              
+ )} +
+ ) : null} +
+ ); diff --git a/src/pages/launcher/launcher-api.ts b/src/pages/launcher/launcher-api.ts index 24b01e5..0b0f754 100644 --- a/src/pages/launcher/launcher-api.ts +++ b/src/pages/launcher/launcher-api.ts @@ -204,18 +204,42 @@ export function listStudioRepos(limit = 50) { .then((result) => result.repos); } -export function listGiteaCommits(owner: string, repo: string, branch = 'develop', limit = 5) { +export function listGiteaCommits( + owner: string, + repo: string, + branch = 'develop', + options?: { page?: number; limit?: number; since?: string } +) { const params = new URLSearchParams({ sha: branch, - limit: String(limit) + page: String(options?.page ?? 1), + limit: String(options?.limit ?? 5) }); + if (options?.since) params.set('since', options.since); return apiRequest<{ commits: GiteaCommit[] }>( `/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?${params.toString()}` ).then((result) => result.commits); } -export function listLauncherBuildJobs() { - return apiRequest('/launcher/builds'); +export function getGiteaCommitDiff(owner: string, repo: string, sha: string) { + return apiRequest<{ diff: string }>( + `/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(sha)}/diff` + ).then((result) => result.diff); +} + +export function listLauncherBuildJobs(options?: { + target?: string; + page?: number; + limit?: number; + since?: string; +}) { + const params = new URLSearchParams(); + if (options?.target) params.set('target', options.target); + if (options?.page) params.set('page', String(options.page)); + if (options?.limit) params.set('limit', String(options.limit)); + if (options?.since) params.set('since', options.since); + const query = params.toString() ? `?${params.toString()}` : ''; + return apiRequest(`/launcher/builds${query}`); } export function getLauncherBackendDeployment() {