Sprint 002 non-blocking:
- API_URL/POLL_INTERVAL lib/config.ts 공통 추출
- 폴링 간격 NEXT_PUBLIC_POLL_INTERVAL_MS env화
Sprint 003 본문:
- TASK-008: SisterDetailService (config/sessions/subagents/activity 조회)
- TASK-008: GET /api/sisters/:name/{config,sessions,subagents,activity}
- TASK-008: GET /api/org → 조직도 + 파이프라인 데이터
- TASK-009: /sisters 목록 페이지 (카드 클릭 → 상세 이동)
- TASK-009: /sisters/[name] 상세 페이지 (탭: 개요/설정/세션/서브에이전트)
- TASK-009: SisterCard → /sisters/:name 링크 연결
- TASK-010: /org 조직도 페이지 (OrgTree 컴포넌트, 호버 팝업)
- TASK-011: /settings 설정 뷰어 페이지 (자매 선택 드롭다운)
- 테스트 16/16 pass, FE build 성공 (7 routes)
221 lines
6.0 KiB
TypeScript
221 lines
6.0 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useState } from 'react';
|
||
import styled from 'styled-components';
|
||
import SisterCard from '@/components/dashboard/SisterCard';
|
||
import ProjectCard from '@/components/projects/ProjectCard';
|
||
import ActivityFeed from '@/components/dashboard/ActivityFeed';
|
||
import { theme } from '@/styles/theme';
|
||
|
||
type Status = 'online' | 'offline' | 'working' | 'unknown';
|
||
|
||
interface SisterStatus {
|
||
id: number;
|
||
name: string;
|
||
user: string;
|
||
lxcId: number;
|
||
role: string;
|
||
status: Status;
|
||
lastSeen: string | null;
|
||
currentTask: string | null;
|
||
}
|
||
|
||
const MOCK_SISTERS: SisterStatus[] = [
|
||
{ id: 1, name: 'harang', user: 'harang', lxcId: 104, role: 'Orchestrator', status: 'online', lastSeen: new Date().toISOString(), currentTask: 'Sprint 002 기획 중' },
|
||
{ id: 2, name: 'narang', user: 'narang', lxcId: 105, role: 'Generator', status: 'working', lastSeen: new Date().toISOString(), currentTask: 'SPRINT-002 구현 중' },
|
||
{ id: 3, name: 'darang', user: 'darang', lxcId: 106, role: 'Evaluator', status: 'online', lastSeen: new Date().toISOString(), currentTask: null },
|
||
{ id: 4, name: 'erang', user: 'erang', lxcId: 107, role: 'Infra Manager', status: 'online', lastSeen: new Date().toISOString(), currentTask: null },
|
||
];
|
||
|
||
import { API_URL, POLL_INTERVAL_MS } from '@/lib/config';
|
||
|
||
const PageWrapper = styled.div`
|
||
padding: 32px;
|
||
min-height: 100vh;
|
||
background: ${theme.colors.bg};
|
||
`;
|
||
|
||
const PageHeader = styled.div`
|
||
margin-bottom: 32px;
|
||
`;
|
||
|
||
const PageTitle = styled.h1`
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: ${theme.colors.textPrimary};
|
||
margin-bottom: 6px;
|
||
`;
|
||
|
||
const PageSubtitle = styled.p`
|
||
font-size: 14px;
|
||
color: ${theme.colors.textSecondary};
|
||
`;
|
||
|
||
const SectionTitle = styled.h2`
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: ${theme.colors.textSecondary};
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
margin-bottom: 16px;
|
||
`;
|
||
|
||
const SistersGrid = styled.div`
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 40px;
|
||
|
||
@media (max-width: 1024px) {
|
||
flex-wrap: wrap;
|
||
> * { flex: 1 1 calc(50% - 8px); }
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
flex-direction: column;
|
||
}
|
||
`;
|
||
|
||
const TwoColumnRow = styled.div`
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
margin-bottom: 40px;
|
||
|
||
@media (max-width: 900px) {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
`;
|
||
|
||
const ProjectsGrid = styled.div`
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 40px;
|
||
`;
|
||
|
||
const Card = styled.div`
|
||
background: ${theme.colors.cardBg};
|
||
border: 1px solid ${theme.colors.border};
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
`;
|
||
|
||
const ErrorBanner = styled.div`
|
||
padding: 10px 14px;
|
||
background: rgba(255,23,68,0.08);
|
||
border: 1px solid rgba(255,23,68,0.3);
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: ${theme.colors.offline};
|
||
margin-bottom: 20px;
|
||
`;
|
||
|
||
const PlaceholderText = styled.div`
|
||
padding: 32px;
|
||
text-align: center;
|
||
color: ${theme.colors.textSecondary};
|
||
font-size: 13px;
|
||
`;
|
||
|
||
export default function DashboardPage() {
|
||
const [sisters, setSisters] = useState<SisterStatus[]>(MOCK_SISTERS);
|
||
const [projects, setProjects] = useState<any[]>([]);
|
||
const [activityItems, setActivityItems] = useState<any[]>([]);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
const fetchAll = async () => {
|
||
try {
|
||
const [sRes, pRes, aRes] = await Promise.allSettled([
|
||
fetch(`${API_URL}/api/sisters`),
|
||
fetch(`${API_URL}/api/projects`),
|
||
fetch(`${API_URL}/api/activity?limit=20`),
|
||
]);
|
||
|
||
if (sRes.status === 'fulfilled' && sRes.value.ok) {
|
||
setSisters(await sRes.value.json());
|
||
}
|
||
if (pRes.status === 'fulfilled' && pRes.value.ok) {
|
||
setProjects(await pRes.value.json());
|
||
}
|
||
if (aRes.status === 'fulfilled' && aRes.value.ok) {
|
||
const d = await aRes.value.json();
|
||
setActivityItems(d.items ?? []);
|
||
}
|
||
setError(null);
|
||
} catch {
|
||
setError('API 연결 실패 — 일부 데이터는 mock으로 표시 중');
|
||
}
|
||
};
|
||
|
||
fetchAll();
|
||
const interval = setInterval(fetchAll, POLL_INTERVAL_MS);
|
||
return () => clearInterval(interval);
|
||
}, []);
|
||
|
||
return (
|
||
<PageWrapper>
|
||
<PageHeader>
|
||
<PageTitle>하나랑 대시보드</PageTitle>
|
||
<PageSubtitle>4자매 멀티에이전트 파이프라인 관제 현황</PageSubtitle>
|
||
</PageHeader>
|
||
|
||
{error && <ErrorBanner>⚠️ {error}</ErrorBanner>}
|
||
|
||
<SectionTitle>자매 상태</SectionTitle>
|
||
<SistersGrid>
|
||
{sisters.map((sister) => (
|
||
<SisterCard
|
||
key={sister.id}
|
||
name={sister.name}
|
||
role={sister.role}
|
||
status={sister.status}
|
||
lastSeen={sister.lastSeen}
|
||
currentTask={sister.currentTask}
|
||
/>
|
||
))}
|
||
</SistersGrid>
|
||
|
||
<SectionTitle>진행 중 프로젝트</SectionTitle>
|
||
{projects.length > 0 ? (
|
||
<ProjectsGrid>
|
||
{projects.map((p) => (
|
||
<ProjectCard
|
||
key={p.id}
|
||
id={p.id}
|
||
name={p.name}
|
||
description={p.description}
|
||
status={p.status}
|
||
progress={p.progress}
|
||
doneTasks={p.doneTasks}
|
||
totalTasks={p.totalTasks}
|
||
openPRs={p.openPRs}
|
||
currentSprint={p.currentSprint}
|
||
/>
|
||
))}
|
||
</ProjectsGrid>
|
||
) : (
|
||
<TwoColumnRow>
|
||
<Card>
|
||
<PlaceholderText>등록된 프로젝트 없음</PlaceholderText>
|
||
</Card>
|
||
<Card>
|
||
<SectionTitle>최근 활동 피드</SectionTitle>
|
||
<ActivityFeed items={activityItems} />
|
||
</Card>
|
||
</TwoColumnRow>
|
||
)}
|
||
|
||
{projects.length > 0 && (
|
||
<TwoColumnRow>
|
||
<div />
|
||
<Card>
|
||
<SectionTitle>최근 활동 피드</SectionTitle>
|
||
<ActivityFeed items={activityItems} />
|
||
</Card>
|
||
</TwoColumnRow>
|
||
)}
|
||
</PageWrapper>
|
||
);
|
||
}
|