114 lines
5.8 KiB
TypeScript
114 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import styled from 'styled-components';
|
|
import Link from 'next/link';
|
|
import { PageTitle, LabelMeta, TechBar, TechBarFill, Btn } from '@/components/ui/base';
|
|
import { API_URL } from '@/lib/config';
|
|
import { adminFetch } from '@/lib/adminFetch';
|
|
|
|
interface ProjectListItem {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
progress: number;
|
|
phase: string;
|
|
currentSprint: string | null;
|
|
latestHotfix: string | null;
|
|
latestQaLabel: string | null;
|
|
latestQaStatus: 'passed' | 'failed' | 'unknown';
|
|
blockerCount: number;
|
|
deployStatus: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
const ProjectList = styled.div`display:flex;flex-direction:column;gap:var(--space-md);`;
|
|
const ProjectRow = styled(Link)`display:grid;grid-template-columns:1.2fr 1fr 220px;gap:var(--space-lg);padding:var(--space-lg);border:1px solid var(--border-color);align-items:center;text-decoration:none;transition:border-color .15s, background .15s;&:hover{border-color:var(--border-hover);background:#111}@media (max-width:1199px){grid-template-columns:1fr;}`;
|
|
const Left = styled.div`display:flex;flex-direction:column;gap:6px;min-width:0;`;
|
|
const ProjectName = styled.div`font-size:15px;font-weight:600;color:var(--text-primary);`;
|
|
const ProjectDesc = styled.div`font-size:12px;color:var(--text-secondary);line-height:1.5;`;
|
|
const Middle = styled.div`display:flex;flex-direction:column;gap:10px;`;
|
|
const Right = styled.div`display:flex;flex-direction:column;gap:8px;align-items:flex-end;@media (max-width:1199px){align-items:flex-start;}`;
|
|
const Meta = styled.div`font-size:11px;color:var(--text-secondary);font-family:var(--font-mono);`;
|
|
const PhaseBadge = styled.div<{ $phase:string }>`font-size:11px;font-family:var(--font-mono);padding:4px 8px;border:1px solid ${({$phase})=>$phase==='DEPLOYED'?'#5fafff44':$phase==='READY FOR DEPLOY'?'#5fff8a44':$phase==='QA'?'#ffcc6644':'var(--border-color)'};color:${({$phase})=>$phase==='DEPLOYED'?'#5fafff':$phase==='READY FOR DEPLOY'?'#5fff8a':$phase==='QA'?'#ffcc66':'var(--text-primary)'};white-space:nowrap;`;
|
|
const EmptyState = styled.div`padding:var(--space-xl) 0;color:var(--text-secondary);font-size:13px;font-family:var(--font-mono);`;
|
|
|
|
function qaLabel(project: ProjectListItem): string {
|
|
if (project.latestQaStatus === 'passed') return 'QA PASSED';
|
|
if (project.latestQaStatus === 'failed') return `QA FAILED${project.blockerCount > 0 ? ` · ${project.blockerCount} BLOCKERS` : ''}`;
|
|
return 'QA PENDING';
|
|
}
|
|
|
|
export default function ProjectsPage() {
|
|
const [projects, setProjects] = useState<ProjectListItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [syncing, setSyncing] = useState(false);
|
|
const [syncResult, setSyncResult] = useState<string | null>(null);
|
|
|
|
const loadProjects = () => {
|
|
setLoading(true);
|
|
fetch(`${API_URL}/api/projects`).then((r) => r.json()).then(setProjects).catch(() => {}).finally(() => setLoading(false));
|
|
};
|
|
|
|
const runSync = async (path: string, label: string) => {
|
|
setSyncing(true);
|
|
setSyncResult(null);
|
|
try {
|
|
const res = await adminFetch(path, { method: 'POST' });
|
|
const data = await res.json();
|
|
setSyncResult(`${label}: ${JSON.stringify(data)}`);
|
|
loadProjects();
|
|
} catch {
|
|
setSyncResult(`${label}: failed`);
|
|
} finally {
|
|
setSyncing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { loadProjects(); }, []);
|
|
|
|
return (
|
|
<>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 'var(--space-md)', flexWrap: 'wrap', marginBottom: 'var(--space-lg)' }}>
|
|
<PageTitle>프로젝트</PageTitle>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-md)', flexWrap: 'wrap' }}>
|
|
{syncResult && <span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--text-secondary)' }}>{syncResult}</span>}
|
|
<LabelMeta>VOL: {String(projects.length).padStart(2, '0')}</LabelMeta>
|
|
<Btn onClick={() => runSync('/api/admin/gitea/sync', 'gitea sync')} disabled={syncing}>{syncing ? 'SYNCING...' : '↻ GITEA SYNC'}</Btn>
|
|
<Btn onClick={() => runSync('/api/projects/sync-all-sprints', 'sprint sync')} disabled={syncing}>{syncing ? '...' : '↻ SPRINT SYNC'}</Btn>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? <EmptyState>LOADING...</EmptyState> : projects.length === 0 ? <EmptyState>NO PROJECTS REGISTERED</EmptyState> : (
|
|
<ProjectList>
|
|
{projects.map((project) => (
|
|
<ProjectRow key={project.id} href={`/projects/${project.id}`}>
|
|
<Left>
|
|
<Meta>P-{String(project.id).padStart(3, '0')}</Meta>
|
|
<ProjectName>{project.name}</ProjectName>
|
|
<ProjectDesc>{project.description ?? '설명 없음'}</ProjectDesc>
|
|
</Left>
|
|
<Middle>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)', flexWrap: 'wrap' }}>
|
|
<PhaseBadge $phase={project.phase}>{project.phase}</PhaseBadge>
|
|
<Meta>{project.currentSprint ? `SPRINT ${project.currentSprint}` : 'PLANNING BACKLOG'}</Meta>
|
|
{project.latestHotfix && <Meta>HOTFIX {project.latestHotfix}</Meta>}
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
|
|
<TechBar style={{ flex: 1 }}><TechBarFill $width={project.progress ?? 0} /></TechBar>
|
|
<LabelMeta style={{ minWidth: 44, textAlign: 'right' }}>{project.progress ?? 0}%</LabelMeta>
|
|
</div>
|
|
</Middle>
|
|
<Right>
|
|
<Meta>{qaLabel(project)}</Meta>
|
|
<Meta>{project.deployStatus}</Meta>
|
|
<Meta>{new Date(project.updatedAt).toLocaleDateString('ko-KR')} UPDATED</Meta>
|
|
</Right>
|
|
</ProjectRow>
|
|
))}
|
|
</ProjectList>
|
|
)}
|
|
</>
|
|
);
|
|
}
|