'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([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [syncResult, setSyncResult] = useState(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 ( <>
프로젝트
{syncResult && {syncResult}} VOL: {String(projects.length).padStart(2, '0')} runSync('/api/admin/gitea/sync', 'gitea sync')} disabled={syncing}>{syncing ? 'SYNCING...' : '↻ GITEA SYNC'} runSync('/api/projects/sync-all-sprints', 'sprint sync')} disabled={syncing}>{syncing ? '...' : '↻ SPRINT SYNC'}
{loading ? LOADING... : projects.length === 0 ? NO PROJECTS REGISTERED : ( {projects.map((project) => ( P-{String(project.id).padStart(3, '0')} {project.name} {project.description ?? '설명 없음'}
{project.phase} {project.currentSprint ? `SPRINT ${project.currentSprint}` : 'PLANNING BACKLOG'} {project.latestHotfix && HOTFIX {project.latestHotfix}}
{project.progress ?? 0}%
{qaLabel(project)} {project.deployStatus} {new Date(project.updatedAt).toLocaleDateString('ko-KR')} UPDATED
))}
)} ); }