This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Copy, ExternalLink, Link2, RotateCcw } from 'lucide-react';
|
||||
import { Copy, ExternalLink, FileText, Link2, RotateCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Badge, Button, Card, CardHeader } from '../../components/ui';
|
||||
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
import { PageTitle } from '../../layout/AppLayout';
|
||||
import { cn } from '../../lib/utils';
|
||||
@@ -8,6 +8,7 @@ import type { GiteaRepo } from '../../types/application';
|
||||
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
|
||||
import {
|
||||
getLauncherSource,
|
||||
getGiteaCommitDiff,
|
||||
listGiteaCommits,
|
||||
listLauncherVersions,
|
||||
listPlatformRepos,
|
||||
@@ -21,6 +22,7 @@ const giteaSshBaseUrl = import.meta.env.VITE_GITEA_SSH_BASE_URL ?? 'ssh://git@lo
|
||||
const giteaAppOrg = import.meta.env.VITE_GITEA_APP_ORG ?? 'platform';
|
||||
|
||||
type SourceTarget = 'launcher-client' | 'launcher-backend';
|
||||
type CommitRange = 'week' | 'month' | 'all';
|
||||
|
||||
type SourceForm = {
|
||||
repository: string;
|
||||
@@ -40,6 +42,14 @@ const targetLabels: Record<SourceTarget, { title: string; description: string; d
|
||||
}
|
||||
};
|
||||
|
||||
const commitRangeLabels: Record<CommitRange, string> = {
|
||||
week: '일주일',
|
||||
month: '한달',
|
||||
all: '전체'
|
||||
};
|
||||
|
||||
const commitPageSize = 5;
|
||||
|
||||
function repoFullName(repo: GiteaRepo) {
|
||||
return (repo as GiteaRepo & { fullName?: string }).fullName ?? repo.full_name ?? normalizeRepository(repo.name);
|
||||
}
|
||||
@@ -113,6 +123,13 @@ function relativeTime(value?: string | null) {
|
||||
return new Intl.DateTimeFormat('ko-KR', { month: '2-digit', day: '2-digit' }).format(new Date(value));
|
||||
}
|
||||
|
||||
function commitRangeSince(range: CommitRange) {
|
||||
if (range === 'all') return undefined;
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - (range === 'week' ? 7 : 30));
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
export function LauncherSourcePage() {
|
||||
const { toast } = useToast();
|
||||
const [activeTarget, setActiveTarget] = useState<SourceTarget>('launcher-client');
|
||||
@@ -124,8 +141,15 @@ export function LauncherSourcePage() {
|
||||
'launcher-backend': emptyForm()
|
||||
});
|
||||
const [commits, setCommits] = useState<GiteaCommit[]>([]);
|
||||
const [commitRange, setCommitRange] = useState<CommitRange>('week');
|
||||
const [commitPage, setCommitPage] = useState(1);
|
||||
const [hasNextCommitPage, setHasNextCommitPage] = useState(false);
|
||||
const [commitsLoading, setCommitsLoading] = useState(false);
|
||||
const [commitsError, setCommitsError] = useState('');
|
||||
const [diffTarget, setDiffTarget] = useState<GiteaCommit | null>(null);
|
||||
const [diffContent, setDiffContent] = useState('');
|
||||
const [diffLoading, setDiffLoading] = useState(false);
|
||||
const [diffError, setDiffError] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
@@ -181,24 +205,37 @@ export function LauncherSourcePage() {
|
||||
setRepos(platformRepos);
|
||||
}, [platformRepos]);
|
||||
|
||||
useEffect(() => {
|
||||
setCommitPage(1);
|
||||
}, [activeForm.repository, activeTarget, commitRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const repository = splitRepository(activeForm.repository, activeTargetMeta.defaultOrg);
|
||||
if (!repository) {
|
||||
setCommits([]);
|
||||
setCommitsError('');
|
||||
setHasNextCommitPage(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
setCommitsLoading(true);
|
||||
setCommitsError('');
|
||||
listGiteaCommits(repository.owner, repository.repo, sourceBranch, 5)
|
||||
listGiteaCommits(repository.owner, repository.repo, sourceBranch, {
|
||||
page: commitPage,
|
||||
limit: commitPageSize + 1,
|
||||
since: commitRangeSince(commitRange)
|
||||
})
|
||||
.then((nextCommits) => {
|
||||
if (!canceled) setCommits(nextCommits);
|
||||
if (!canceled) {
|
||||
setCommits(nextCommits.slice(0, commitPageSize));
|
||||
setHasNextCommitPage(nextCommits.length > commitPageSize);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (!canceled) {
|
||||
setCommits([]);
|
||||
setHasNextCommitPage(false);
|
||||
setCommitsError(error.message);
|
||||
}
|
||||
})
|
||||
@@ -209,7 +246,7 @@ export function LauncherSourcePage() {
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [activeForm.repository, activeTargetMeta.defaultOrg, sourceBranch]);
|
||||
}, [activeForm.repository, activeTargetMeta.defaultOrg, sourceBranch, commitPage, commitRange]);
|
||||
|
||||
const save = () => {
|
||||
if (!activeForm.repository.trim()) return;
|
||||
@@ -237,6 +274,23 @@ export function LauncherSourcePage() {
|
||||
toast({ type: 'success', message: '복사되었습니다.' });
|
||||
};
|
||||
|
||||
const openCommitDiff = (commit: GiteaCommit) => {
|
||||
const repository = splitRepository(activeForm.repository, activeTargetMeta.defaultOrg);
|
||||
if (!repository) {
|
||||
toast({ type: 'error', title: 'Diff 조회 실패', message: '저장소가 연결되어 있지 않습니다.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setDiffTarget(commit);
|
||||
setDiffContent('');
|
||||
setDiffError('');
|
||||
setDiffLoading(true);
|
||||
getGiteaCommitDiff(repository.owner, repository.repo, commit.sha)
|
||||
.then((diff) => setDiffContent(diff))
|
||||
.catch((error: Error) => setDiffError(error.message))
|
||||
.finally(() => setDiffLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -338,7 +392,24 @@ export function LauncherSourcePage() {
|
||||
</div>
|
||||
|
||||
<Card className="mt-4 min-w-0 overflow-hidden">
|
||||
<CardHeader title="최근 커밋" description={`${activeTargetMeta.title} 저장소의 develop 브랜치 기준 최근 5개 커밋입니다.`} />
|
||||
<CardHeader
|
||||
title="커밋 목록"
|
||||
description={`${activeTargetMeta.title} 저장소의 develop 브랜치 기준 커밋 내역입니다.`}
|
||||
action={
|
||||
<div className="flex items-center gap-2">
|
||||
{(Object.keys(commitRangeLabels) as CommitRange[]).map((range) => (
|
||||
<Button
|
||||
key={range}
|
||||
variant={commitRange === range ? 'default' : 'outline'}
|
||||
className="h-8 px-3 text-[13px]"
|
||||
onClick={() => setCommitRange(range)}
|
||||
>
|
||||
{commitRangeLabels[range]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
@@ -347,22 +418,23 @@ export function LauncherSourcePage() {
|
||||
<th className="px-5 py-3 font-medium">SHA1</th>
|
||||
<th className="px-5 py-3 font-medium">메시지</th>
|
||||
<th className="px-5 py-3 font-medium">날짜</th>
|
||||
<th className="w-16 px-5 py-3 text-right font-medium">상세</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commitsLoading ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-5 py-10 text-center text-sm text-muted-foreground">커밋을 불러오는 중입니다.</td>
|
||||
<td colSpan={5} className="px-5 py-10 text-center text-sm text-muted-foreground">커밋을 불러오는 중입니다.</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{!commitsLoading && commitsError ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-5 py-10 text-center text-sm text-muted-foreground">커밋 조회 실패: {commitsError}</td>
|
||||
<td colSpan={5} className="px-5 py-10 text-center text-sm text-muted-foreground">커밋 조회 실패: {commitsError}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{!commitsLoading && !commitsError && commits.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-5 py-10 text-center text-sm text-muted-foreground">표시할 커밋이 없습니다.</td>
|
||||
<td colSpan={5} className="px-5 py-10 text-center text-sm text-muted-foreground">표시할 커밋이 없습니다.</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{!commitsLoading && !commitsError ? commits.map((commit) => (
|
||||
@@ -371,13 +443,76 @@ export function LauncherSourcePage() {
|
||||
<td className="whitespace-nowrap px-5 py-3 font-mono text-xs text-muted-foreground">{commit.sha.slice(0, 10)}</td>
|
||||
<td className="max-w-[360px] truncate px-5 py-3">{commitMessage(commit)}</td>
|
||||
<td className="whitespace-nowrap px-5 py-3 text-muted-foreground">{relativeTime(commitDate(commit))}</td>
|
||||
<td className="whitespace-nowrap px-5 py-3 text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8 px-0"
|
||||
title="내역 상세"
|
||||
aria-label="내역 상세"
|
||||
onClick={() => openCommitDiff(commit)}
|
||||
>
|
||||
<FileText className="size-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 border-t border-border px-5 py-4 text-sm">
|
||||
<span className="text-muted-foreground">페이지 {commitPage}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-3 text-[13px]"
|
||||
disabled={commitPage <= 1 || commitsLoading}
|
||||
onClick={() => setCommitPage((page) => Math.max(1, page - 1))}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-3 text-[13px]"
|
||||
disabled={!hasNextCommitPage || commitsLoading}
|
||||
onClick={() => setCommitPage((page) => page + 1)}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
open={Boolean(diffTarget)}
|
||||
title="커밋 내역 상세"
|
||||
onClose={() => setDiffTarget(null)}
|
||||
className="max-w-5xl"
|
||||
footer={<Button variant="outline" onClick={() => setDiffTarget(null)}>닫기</Button>}
|
||||
>
|
||||
{diffTarget ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span className="font-mono text-xs text-muted-foreground">{diffTarget.sha.slice(0, 10)}</span>
|
||||
<span className="font-medium">{commitMessage(diffTarget)}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{commitAuthor(diffTarget)} · {relativeTime(commitDate(diffTarget))}
|
||||
</p>
|
||||
</div>
|
||||
{diffLoading ? (
|
||||
<div className="flex min-h-40 items-center justify-center text-sm text-muted-foreground">Diff를 불러오는 중입니다.</div>
|
||||
) : diffError ? (
|
||||
<div className="rounded-md border border-rose-500/30 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">Diff 조회 실패: {diffError}</div>
|
||||
) : (
|
||||
<pre className="max-h-[58vh] overflow-auto rounded-md border border-border bg-background p-4 font-mono text-xs leading-5 text-muted-foreground">
|
||||
{diffContent || '표시할 변경 내역이 없습니다.'}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<LoadingModal open={loadingVisible} />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user