Files
hanarang-dashboard/frontend/app/page.tsx
narang-ai 4015e5f22f feat(sprint-003): sister detail + org chart + settings viewer
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)
2026-04-04 11:55:14 +09:00

221 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}