merge develop into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-07 17:42:21 +09:00
2 changed files with 43 additions and 50 deletions
@@ -164,7 +164,7 @@ export function ReleaseManagementPage() {
const [version, setVersion] = useState('');
const [releaseModalOpen, setReleaseModalOpen] = useState(false);
const [selectedBuildIds, setSelectedBuildIds] = useState<string[]>([]);
const [openLogReleaseId, setOpenLogReleaseId] = useState('');
const [selectedReleaseLogId, setSelectedReleaseLogId] = useState('');
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [range, setRange] = useState<ReleaseRange>('week');
@@ -262,6 +262,8 @@ export function ReleaseManagementPage() {
}
};
const selectedReleaseLog = releases.find((release) => release.id === selectedReleaseLogId) ?? null;
return (
<>
<PageTitle
@@ -306,7 +308,6 @@ export function ReleaseManagementPage() {
) : null}
{pagedReleases.map((release) => {
const artifacts = releaseArtifacts(release);
const isLogOpen = openLogReleaseId === release.id;
return (
<div key={release.id} className="rounded-md border border-border bg-background/70 p-4">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -319,9 +320,9 @@ export function ReleaseManagementPage() {
variant="outline"
className="size-8 px-0"
disabled={!release.logs}
title={isLogOpen ? '로그 닫기' : '로그'}
aria-label={isLogOpen ? '로그 닫기' : '로그'}
onClick={() => setOpenLogReleaseId((prev) => prev === release.id ? '' : release.id)}
title="로그"
aria-label="로그"
onClick={() => setSelectedReleaseLogId(release.id)}
>
<MoreHorizontal className="size-4" />
</Button>
@@ -342,11 +343,6 @@ export function ReleaseManagementPage() {
<p className="mt-1 text-xs text-muted-foreground">-</p>
)}
</div>
{isLogOpen ? (
<pre className="mt-3 max-h-72 overflow-auto rounded-md border border-border bg-black/70 p-3 font-mono text-xs leading-5 text-slate-100">
{release.logs}
</pre>
) : null}
</div>
);
})}
@@ -442,6 +438,23 @@ export function ReleaseManagementPage() {
</div>
</div>
</Modal>
<Modal
open={Boolean(selectedReleaseLog)}
title="릴리즈 로그"
onClose={() => setSelectedReleaseLogId('')}
className="log-modal-shell"
footer={<Button variant="outline" onClick={() => setSelectedReleaseLogId('')}>닫기</Button>}
>
{selectedReleaseLog?.logs ? (
<pre className="whitespace-pre-wrap rounded-md border border-border bg-background p-4 font-mono text-xs leading-5 text-muted-foreground">
{selectedReleaseLog.logs}
</pre>
) : (
<div className="flex min-h-44 items-center justify-center text-center text-sm text-muted-foreground">
로그가 없습니다.
</div>
)}
</Modal>
<LoadingModal open={loadingVisible} />
</>
);
+20 -40
View File
@@ -3,14 +3,12 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useToast } from '../../components/toast';
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
import { PageTitle } from '../../layout/AppLayout';
import type { AppRelease } from '../../types/application';
import type { ApiApp, ApiAppComponent } from '../../types/runtime';
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
import { AppSelector, useApps } from './ApplicationControls';
import {
getGiteaCommitDiff,
listGiteaCommitsPage,
listReleases,
listSources,
type GiteaCommit
} from './application-api';
@@ -68,11 +66,6 @@ function repositoryGitUrl(repository?: string | null) {
return normalized ? `${giteaBaseUrl.replace(/\/$/, '')}/${normalized}.git` : '-';
}
function releaseTime(release: AppRelease) {
const value = new Date(release.createdAt).getTime();
return Number.isNaN(value) ? 0 : value;
}
function commitAuthor(commit: GiteaCommit) {
return commit.author?.login
|| commit.author?.username
@@ -304,7 +297,6 @@ export function SourceManagementPage() {
const { toast } = useToast();
const { apps, selectedAppId, loading: appsLoading, selectApp } = useApps();
const [sources, setSources] = useState<SourceComponent[]>([]);
const [releases, setReleases] = useState<AppRelease[]>([]);
const [sourcesLoading, setSourcesLoading] = useState(true);
const [selectedSourceId, setSelectedSourceId] = useState('');
const [selectedBranch, setSelectedBranch] = useState('release');
@@ -328,33 +320,19 @@ export function SourceManagementPage() {
const selectedSource = filteredSources.find((source) => source.id === selectedSourceId) ?? filteredSources[0] ?? null;
const repository = splitRepository(selectedSource?.repository);
const branch = selectedBranch || 'release';
const latestBranchSha = commits.length > 0 ? commits[0]?.sha ?? '' : '';
const branchOptions = useMemo(
() => [...new Set(['release', 'develop', 'main', selectedSource?.branch].filter((item): item is string => Boolean(item)))],
[selectedSource?.branch]
);
const latestRelease = useMemo(
() => releases
.filter((release) => release.appId === selectedSource?.appId && release.status === 'SUCCEEDED')
.sort((a, b) => releaseTime(b) - releaseTime(a))[0],
[releases, selectedSource?.appId]
);
const totalCommitPages = totalCommits === null ? null : Math.max(1, Math.ceil(totalCommits / commitPageSize));
const selectedRepositoryGitUrl = repositoryGitUrl(selectedSource?.repository);
const refreshSources = () => {
setSourcesLoading(true);
Promise.all([
listSources(),
listReleases().catch(() => [])
])
.then(([sourceItems, releaseItems]) => {
setSources(sourceItems);
setReleases(releaseItems);
})
.catch(() => {
setSources([]);
setReleases([]);
})
listSources()
.then((sourceItems) => setSources(sourceItems))
.catch(() => setSources([]))
.finally(() => setSourcesLoading(false));
};
@@ -464,6 +442,18 @@ export function SourceManagementPage() {
</label>
</div>
<div className="grid gap-3 rounded-md border border-border bg-muted/20 p-3 text-sm md:grid-cols-2">
<div>
<p className="text-xs text-muted-foreground">조회 브랜치</p>
<select
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground outline-none focus:ring-2 focus:ring-ring"
value={branch}
onChange={(event) => setSelectedBranch(event.target.value)}
>
{branchOptions.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
<div className="min-w-0 md:col-span-2">
<p className="text-xs text-muted-foreground">저장소</p>
<div className="mt-2 rounded-md border border-border bg-background px-3 py-2">
@@ -480,23 +470,13 @@ export function SourceManagementPage() {
</div>
</div>
<div>
<p className="text-xs text-muted-foreground">버전</p>
<p className="text-xs text-muted-foreground">SHA1</p>
<div className="mt-2 rounded-md border border-border bg-background px-3 py-2">
<p className="truncate text-sm font-medium">{latestRelease?.version ?? '-'}</p>
<p className="truncate font-mono text-sm font-medium" title={latestBranchSha || '-'}>
{latestBranchSha || '-'}
</p>
</div>
</div>
<div>
<p className="text-xs text-muted-foreground">조회 브랜치</p>
<select
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground outline-none focus:ring-2 focus:ring-ring"
value={branch}
onChange={(event) => setSelectedBranch(event.target.value)}
>
{branchOptions.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
</div>
</div>
</Card>