This commit is contained in:
@@ -100,8 +100,7 @@ export function BuildManagementPage() {
|
|||||||
setSelectedComponentId((prev) => {
|
setSelectedComponentId((prev) => {
|
||||||
const valid = filteredSources.some((source) => source.id === prev);
|
const valid = filteredSources.some((source) => source.id === prev);
|
||||||
const next = valid ? prev : filteredSources[0]?.id || '';
|
const next = valid ? prev : filteredSources[0]?.id || '';
|
||||||
const nextSource = filteredSources.find((source) => source.id === next);
|
setBranch('develop');
|
||||||
setBranch(nextSource?.branch || 'develop');
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -137,7 +136,7 @@ export function BuildManagementPage() {
|
|||||||
if (!componentOptions.some((source) => source.id === selectedComponentId)) {
|
if (!componentOptions.some((source) => source.id === selectedComponentId)) {
|
||||||
const nextComponent = componentOptions[0];
|
const nextComponent = componentOptions[0];
|
||||||
setSelectedComponentId(nextComponent?.id || '');
|
setSelectedComponentId(nextComponent?.id || '');
|
||||||
setBranch(nextComponent?.branch || 'develop');
|
setBranch('develop');
|
||||||
}
|
}
|
||||||
}, [componentOptions, selectedComponentId]);
|
}, [componentOptions, selectedComponentId]);
|
||||||
|
|
||||||
@@ -225,9 +224,8 @@ export function BuildManagementPage() {
|
|||||||
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"
|
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={selectedComponent?.id ?? ''}
|
value={selectedComponent?.id ?? ''}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const next = componentOptions.find((source) => source.id === event.target.value);
|
|
||||||
setSelectedComponentId(event.target.value);
|
setSelectedComponentId(event.target.value);
|
||||||
setBranch(next?.branch || 'develop');
|
setBranch('develop');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{componentOptions.length === 0 ? <option value="">구성요소 없음</option> : null}
|
{componentOptions.length === 0 ? <option value="">구성요소 없음</option> : null}
|
||||||
@@ -242,8 +240,6 @@ export function BuildManagementPage() {
|
|||||||
onChange={(event) => setBranch(event.target.value)}
|
onChange={(event) => setBranch(event.target.value)}
|
||||||
>
|
>
|
||||||
<option value="develop">develop</option>
|
<option value="develop">develop</option>
|
||||||
<option value="release">release</option>
|
|
||||||
<option value="main">main</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block md:col-span-2">
|
<label className="block md:col-span-2">
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ function statusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'bl
|
|||||||
type ImageTags = {
|
type ImageTags = {
|
||||||
images?: Record<string, string>;
|
images?: Record<string, string>;
|
||||||
artifacts?: Record<string, string>;
|
artifacts?: Record<string, string>;
|
||||||
|
error?: string;
|
||||||
|
promotion?: {
|
||||||
|
sourceBuildJobIds?: string[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function releaseArtifacts(release: AppRelease) {
|
function releaseArtifacts(release: AppRelease) {
|
||||||
@@ -71,6 +75,17 @@ function usedReleaseValues(releases: AppRelease[]) {
|
|||||||
return new Set(releases.flatMap(releaseArtifacts));
|
return new Set(releases.flatMap(releaseArtifacts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usedReleaseBuildIds(releases: AppRelease[]) {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const release of releases) {
|
||||||
|
const imageTags = release.imageTags as ImageTags | null | undefined;
|
||||||
|
for (const id of imageTags?.promotion?.sourceBuildJobIds ?? []) {
|
||||||
|
ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
function latestBuildIdsByComponent(builds: BuildJob[]) {
|
function latestBuildIdsByComponent(builds: BuildJob[]) {
|
||||||
const map = new Map<string, BuildJob>();
|
const map = new Map<string, BuildJob>();
|
||||||
for (const build of builds) {
|
for (const build of builds) {
|
||||||
@@ -139,12 +154,13 @@ export function ReleaseManagementPage() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const loadingVisible = useMinimumLoading(loading);
|
const loadingVisible = useMinimumLoading(loading);
|
||||||
const usedValues = useMemo(() => usedReleaseValues(releases), [releases]);
|
const usedValues = useMemo(() => usedReleaseValues(releases), [releases]);
|
||||||
|
const usedBuildIds = useMemo(() => usedReleaseBuildIds(releases), [releases]);
|
||||||
const candidateBuilds = useMemo(
|
const candidateBuilds = useMemo(
|
||||||
() => builds.filter((build) => {
|
() => builds.filter((build) => {
|
||||||
const value = buildValue(build);
|
const value = buildValue(build);
|
||||||
return isDevelopBuild(build) && Boolean(buildCommit(build)) && (!value || !usedValues.has(value));
|
return isDevelopBuild(build) && Boolean(buildCommit(build)) && !usedBuildIds.has(build.id) && (!value || !usedValues.has(value));
|
||||||
}),
|
}),
|
||||||
[builds, usedValues]
|
[builds, usedBuildIds, usedValues]
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidateGroups = useMemo(
|
const candidateGroups = useMemo(
|
||||||
@@ -207,7 +223,7 @@ export function ReleaseManagementPage() {
|
|||||||
if (!selectedAppId) return;
|
if (!selectedAppId) return;
|
||||||
try {
|
try {
|
||||||
await createRelease({ appId: selectedAppId, version, branch: 'release', buildJobIds: selectedBuildIds });
|
await createRelease({ appId: selectedAppId, version, branch: 'release', buildJobIds: selectedBuildIds });
|
||||||
toast({ type: 'success', message: '릴리즈가 생성되었습니다.' });
|
toast({ type: 'success', message: '릴리즈 생성을 시작했습니다.' });
|
||||||
setVersion('');
|
setVersion('');
|
||||||
setSelectedBuildIds([]);
|
setSelectedBuildIds([]);
|
||||||
setReleaseModalOpen(false);
|
setReleaseModalOpen(false);
|
||||||
@@ -222,7 +238,7 @@ export function ReleaseManagementPage() {
|
|||||||
<PageTitle
|
<PageTitle
|
||||||
eyebrow="어플리케이션"
|
eyebrow="어플리케이션"
|
||||||
title="릴리즈 관리"
|
title="릴리즈 관리"
|
||||||
description="검증에 성공한 develop 빌드 결과를 release 브랜치로 승격해 배포 가능한 릴리즈를 생성합니다."
|
description="검증에 성공한 develop 빌드를 release로 승격한 뒤 release 기준으로 다시 빌드합니다."
|
||||||
actions={
|
actions={
|
||||||
<Button className="h-8 px-3 text-[13px]" disabled={candidateBuilds.length === 0} onClick={() => openReleaseModal()}>
|
<Button className="h-8 px-3 text-[13px]" disabled={candidateBuilds.length === 0} onClick={() => openReleaseModal()}>
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
@@ -235,7 +251,7 @@ export function ReleaseManagementPage() {
|
|||||||
|
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||||
<Card className="min-w-0 overflow-hidden">
|
<Card className="min-w-0 overflow-hidden">
|
||||||
<CardHeader title="릴리즈 목록" description="성공한 빌드 결과를 버전 단위로 묶은 배포 후보입니다." />
|
<CardHeader title="릴리즈 목록" description="release 브랜치에서 빌드된 산출물 기준의 배포 후보입니다." />
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{filteredReleases.length === 0 ? (
|
{filteredReleases.length === 0 ? (
|
||||||
<div className="flex min-h-44 items-center justify-center text-center text-sm text-muted-foreground">
|
<div className="flex min-h-44 items-center justify-center text-center text-sm text-muted-foreground">
|
||||||
@@ -299,7 +315,7 @@ export function ReleaseManagementPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="min-w-0 overflow-hidden">
|
<Card className="min-w-0 overflow-hidden">
|
||||||
<CardHeader title="릴리즈 후보" description="아직 릴리즈에 포함되지 않은 develop 성공 빌드입니다." />
|
<CardHeader title="릴리즈 후보" description="아직 릴리즈 처리에 사용되지 않은 develop 성공 빌드입니다." />
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{candidateGroups.length === 0 ? (
|
{candidateGroups.length === 0 ? (
|
||||||
<div className="rounded-md border border-border px-3 py-4 text-center text-sm text-muted-foreground">
|
<div className="rounded-md border border-border px-3 py-4 text-center text-sm text-muted-foreground">
|
||||||
@@ -333,7 +349,7 @@ export function ReleaseManagementPage() {
|
|||||||
</select>
|
</select>
|
||||||
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
||||||
<div className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs leading-5 text-muted-foreground">
|
<div className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs leading-5 text-muted-foreground">
|
||||||
develop 성공 빌드만 선택할 수 있으며, 생성 시 각 저장소의 release 브랜치를 선택한 commit으로 fast-forward 합니다.
|
선택한 develop 커밋을 release로 승격한 뒤 release 브랜치에서 다시 빌드합니다.
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
<div className="max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
||||||
{modalBuilds.length === 0 ? (
|
{modalBuilds.length === 0 ? (
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function LauncherBuildPage() {
|
|||||||
setBuildEnv(nextBuildEnv.variables ?? []);
|
setBuildEnv(nextBuildEnv.variables ?? []);
|
||||||
const nextVersion = nextVersions.find((item) => item.isActive) ?? nextVersions[0];
|
const nextVersion = nextVersions.find((item) => item.isActive) ?? nextVersions[0];
|
||||||
setVersion(nextVersion?.version ?? '');
|
setVersion(nextVersion?.version ?? '');
|
||||||
setBranch(target === 'launcher-backend' ? nextSource.source.backend?.branch ?? 'release' : nextSource.source.client?.branch ?? nextVersion?.branch ?? 'release');
|
setBranch('release');
|
||||||
setCommitSha(target === 'launcher-backend' ? nextSource.source.backend?.commitSha ?? '' : nextSource.source.client?.commitSha ?? nextVersion?.commitSha ?? '');
|
setCommitSha(target === 'launcher-backend' ? nextSource.source.backend?.commitSha ?? '' : nextSource.source.client?.commitSha ?? nextVersion?.commitSha ?? '');
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
@@ -133,12 +133,12 @@ export function LauncherBuildPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
if (target === 'launcher-backend') {
|
if (target === 'launcher-backend') {
|
||||||
setBranch(source.backend?.branch ?? 'release');
|
setBranch('release');
|
||||||
setCommitSha(source.backend?.commitSha ?? '');
|
setCommitSha(source.backend?.commitSha ?? '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBranch(source.client?.branch ?? activeVersion?.branch ?? 'release');
|
setBranch('release');
|
||||||
setCommitSha(source.client?.commitSha ?? activeVersion?.commitSha ?? '');
|
setCommitSha(source.client?.commitSha ?? activeVersion?.commitSha ?? '');
|
||||||
}, [activeVersion?.branch, activeVersion?.commitSha, source, target]);
|
}, [activeVersion?.branch, activeVersion?.commitSha, source, target]);
|
||||||
|
|
||||||
|
|||||||
@@ -58,11 +58,17 @@ function toApiDateTime(value: string) {
|
|||||||
return value ? new Date(value).toISOString() : undefined;
|
return value ? new Date(value).toISOString() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exposureText(row: LauncherNotice) {
|
function exposureCell(row: LauncherNotice) {
|
||||||
if (!row.startsAt && !row.endsAt) return '상시';
|
if (!row.startsAt && !row.endsAt) {
|
||||||
if (row.startsAt && row.endsAt) return `${formatNoticeDateTime(row.startsAt)} ~ ${formatNoticeDateTime(row.endsAt)}`;
|
return <span className="text-muted-foreground">상시</span>;
|
||||||
if (row.startsAt) return `${formatNoticeDateTime(row.startsAt)}부터`;
|
}
|
||||||
return `${formatNoticeDateTime(row.endsAt)}까지`;
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-40 text-xs leading-5 text-muted-foreground">
|
||||||
|
<p>{row.startsAt ? formatNoticeDateTime(row.startsAt) : '-'}</p>
|
||||||
|
<p>~ {row.endsAt ? formatNoticeDateTime(row.endsAt) : '-'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toForm(notice: LauncherNotice): NoticeForm {
|
function toForm(notice: LauncherNotice): NoticeForm {
|
||||||
@@ -237,12 +243,7 @@ export function NoticesPage() {
|
|||||||
},
|
},
|
||||||
{ key: 'appKey', header: '대상 앱', cell: (row) => <StatusBadge status={row.appName} tone={isLauncherNoticeKey(row.appKey) ? 'violet' : 'blue'} /> },
|
{ key: 'appKey', header: '대상 앱', cell: (row) => <StatusBadge status={row.appName} tone={isLauncherNoticeKey(row.appKey) ? 'violet' : 'blue'} /> },
|
||||||
{ key: 'status', header: '상태', cell: (row) => <StatusBadge status={row.status ?? (row.isActive ? '게시중' : '비활성')} /> },
|
{ key: 'status', header: '상태', cell: (row) => <StatusBadge status={row.status ?? (row.isActive ? '게시중' : '비활성')} /> },
|
||||||
{ key: 'exposure', header: '노출 일시', cell: (row) => <span className="text-muted-foreground">{exposureText(row)}</span> },
|
{ key: 'exposure', header: '노출 일시', className: 'w-48 px-5 py-3 font-medium', cell: exposureCell },
|
||||||
{
|
|
||||||
key: 'linkUrl',
|
|
||||||
header: '링크주소',
|
|
||||||
cell: (row) => row.linkUrl ? <span className="block max-w-56 truncate text-muted-foreground">{row.linkUrl}</span> : <span className="text-muted-foreground">-</span>
|
|
||||||
},
|
|
||||||
{ key: 'createdAt', header: '생성일시', cell: (row) => <span className="text-muted-foreground">{formatNoticeDateTime(row.createdAt)}</span> },
|
{ key: 'createdAt', header: '생성일시', cell: (row) => <span className="text-muted-foreground">{formatNoticeDateTime(row.createdAt)}</span> },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
|
|||||||
Reference in New Issue
Block a user