import { useEffect, useMemo, useState } from 'react'; import { ChevronLeft, ChevronRight, MoreHorizontal, Plus, RotateCcw } from 'lucide-react'; import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui'; import { useToast } from '../../components/toast'; import { PageTitle } from '../../layout/AppLayout'; import type { AppRelease, DeploymentJob } from '../../types/application'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; import { AppSelector, formatDate, useApps } from './ApplicationControls'; import { createDeployment, listDeployments, listReleases } from './application-api'; function statusLabel(status: string) { return { PENDING: '대기', RUNNING: '실행 중', SUCCEEDED: '성공', FAILED: '실패', CANCELED: '취소' }[status] ?? status; } function statusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'blue' { if (status === 'SUCCEEDED' || status === '성공') return 'green'; if (status === 'RUNNING' || status === '실행 중') return 'blue'; if (status === 'PENDING' || status === '대기') return 'amber'; if (status === 'FAILED' || status === 'CANCELED' || status === '실패' || status === '취소') return 'red'; return 'neutral'; } function environmentLabel(environment: string) { return environment === 'prod' ? '운영' : environment; } type ImageTags = { images?: Record; artifacts?: Record; }; function releaseArtifacts(release?: AppRelease | null) { const imageTags = release?.imageTags as ImageTags | null | undefined; const entries = Object.entries(imageTags?.images ?? {}); if (entries.length) return entries; if (release?.artifact) return [[release.app.name, release.artifact]]; return []; } function firstArtifact(release: AppRelease) { const artifacts = releaseArtifacts(release); return artifacts[0]?.[1] ?? release.artifact ?? '-'; } function deploymentTime(deployment: DeploymentJob) { const value = new Date(deployment.createdAt || deployment.startedAt || deployment.finishedAt || 0).getTime(); return Number.isNaN(value) ? 0 : value; } const deploymentPageSize = 5; export function DeploymentManagementPage() { const { toast } = useToast(); const { apps, selectedAppId, loading: appsLoading, selectApp } = useApps(); const [deployments, setDeployments] = useState([]); const [releases, setReleases] = useState([]); const [releaseId, setReleaseId] = useState(''); const [namespace, setNamespace] = useState(''); const [updateLauncherAppVersion, setUpdateLauncherAppVersion] = useState(true); const [deployModalOpen, setDeployModalOpen] = useState(false); const [selectedDeployment, setSelectedDeployment] = useState(null); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const loadingVisible = useMinimumLoading(loading); const selectedRelease = releases.find((release) => release.id === releaseId); const filteredReleases = useMemo( () => releases.filter((release) => !selectedAppId || release.appId === selectedAppId), [releases, selectedAppId] ); const filteredDeployments = useMemo( () => deployments .filter((deployment) => !selectedAppId || deployment.appId === selectedAppId) .sort((a, b) => deploymentTime(b) - deploymentTime(a)), [deployments, selectedAppId] ); const totalPages = Math.max(1, Math.ceil(filteredDeployments.length / deploymentPageSize)); const currentPage = Math.min(page, totalPages); const pageStart = (currentPage - 1) * deploymentPageSize; const pagedDeployments = filteredDeployments.slice(pageStart, pageStart + deploymentPageSize); const refresh = async () => { setLoading(true); try { const [nextDeployments, nextReleases] = await Promise.all([listDeployments(), listReleases()]); const deployableReleases = nextReleases.filter((release) => release.status === 'SUCCEEDED'); setDeployments(nextDeployments); setReleases(deployableReleases); } finally { setLoading(false); } }; useEffect(() => { void refresh(); }, []); useEffect(() => { setPage(1); }, [selectedAppId]); useEffect(() => { if (selectedRelease?.app?.namespace) setNamespace((prev) => prev || selectedRelease.app.namespace || ''); }, [selectedRelease?.id]); useEffect(() => { setReleaseId((prev) => filteredReleases.some((release) => release.id === prev) ? prev : filteredReleases[0]?.id || ''); setNamespace(filteredReleases[0]?.app?.namespace || ''); }, [filteredReleases]); const deployableCards = useMemo(() => filteredReleases.slice(0, 8), [filteredReleases]); const openDeployModal = (release?: AppRelease) => { if (release) { if (release.appId !== selectedAppId) selectApp(release.appId); setReleaseId(release.id); setNamespace(release.app.namespace || ''); } setUpdateLauncherAppVersion(true); setDeployModalOpen(true); }; const deploy = async () => { try { const result = await createDeployment({ releaseId, environment: 'prod', namespace, updateLauncherAppVersion }); toast({ type: 'success', message: '배포 작업이 실행되었습니다.' }); setSelectedDeployment(result.deploymentJob); setDeployModalOpen(false); await refresh(); } catch (error) { toast({ type: 'error', message: error instanceof Error ? error.message : '배포 실행에 실패했습니다.' }); } }; return ( <> openDeployModal()}> 배포 실행 } />
{filteredDeployments.length === 0 ? (
배포 작업이 없습니다.
) : null} {pagedDeployments.map((deployment, index) => { const artifacts = releaseArtifacts(deployment.release); return (
No. {filteredDeployments.length - pageStart - index} {statusLabel(deployment.status)} {deployment.app.name} {deployment.release?.version || '-'}
환경 {environmentLabel(deployment.environment)} 네임스페이스 {deployment.namespace} 요청자 {deployment.requestedBy || 'admin'} 시작 {formatDate(deployment.startedAt || deployment.createdAt)}

이미지

{artifacts.length ? ( artifacts.map(([name, artifact]) => (

{name}

{artifact}

)) ) : (

-

)}
); })}
{loading ? '불러오는 중' : `총 ${filteredDeployments.length}건 · ${currentPage} / ${totalPages}`}
{deployableCards.length === 0 ? (
배포 가능한 릴리즈가 없습니다.
) : null} {deployableCards.map((release) => ( ))}
setDeployModalOpen(false)} footer={<>}>
setNamespace(event.target.value)} placeholder="stdo-lms" />
setSelectedDeployment(null)} className="log-modal-shell" footer={} > {selectedDeployment?.logs ? (
            {selectedDeployment.logs}
          
) : (
배포 작업을 선택하면 로그가 표시됩니다.
)}
); }