Files
hanarang-dashboard/frontend/components/projects/SprintAccordion.tsx
narang-ai 230d3847ef feat(redesign-v2): 미니멀 터미널 UI 전체 리디자인
- 디자인 시스템: #151515 배경, 1px 보더 카드, CSS vars, mono 포인트
- GlobalStyle/theme.ts v2 (DESIGN-SYSTEM.md 기반)
- 공통 컴포넌트 (ui/base.tsx): LabelMeta, BracketValue, Card, TechBar, Timeline 등
- Sidebar: [대시] 브라켓 네비 + 모바일 하단 탭바 (767px)
- LayoutShell: SidebarContext 제거, 단순화
- 대시보드 (/): SYS 상태 카드 4개 + ONGOING PROJECTS + ACTIVITY FEED
- 활동 로그 (/activities): 로그 테이블 + 필터 + 검색 + 페이지네이션 (신규)
- 설정 (/settings): 4섹션 Toggle/Bracket Input 그리드
- 자매 노드 관리 (/sisters): 3열 수평 레이아웃 (INFO/GRAPH/SYNC)
- 자매 상세 (/sisters/[name]): 간소화 + 터미널 스타일
- 조직도 (/org): 터미널 카드 트리 구조 + 레벨 색상
- 프로젝트 목록 (/projects): 새 라우트
- 프로젝트 상세 (/projects/[id]): Phase Timeline + Audit Log + Checklist
- FE 14 routes build 성공
2026-04-04 13:10:57 +09:00

202 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState } from 'react';
import styled from 'styled-components';
import TaskBadge from '../common/TaskBadge';
import ProgressBar from '../common/ProgressBar';
import { theme } from '@/styles/theme';
interface Task {
id: number;
taskId: string;
title: string;
assignee: string;
status: string;
iteration: number;
}
interface Sprint {
id: number;
number: number;
name: string;
status: string;
progress: number;
tasks: Task[];
}
const sprintStatusIcon: Record<string, string> = {
pending: '⏳',
in_progress: '🔄',
review: '🔍',
done: '✅',
failed: '❌',
};
const sprintStatusColor: Record<string, string> = {
pending: 'var(--text-secondary)',
in_progress: '#2979FF',
review: '#FF9800',
done: '#00FF00',
failed: '#FF1744',
};
const assigneeEmojis: Record<string, string> = {
harang: '🦊', narang: '🦊', darang: '🐱', erang: '🐺',
};
const Wrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const AccordionItem = styled.div<{ $open: boolean }>`
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 10px;
overflow: hidden;
transition: border-color 0.2s;
${({ $open }) => $open && `border-color: rgba(88,166,255,0.3);`}
`;
const Header = styled.button<{ $statusColor: string }>`
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
background: none;
border: none;
cursor: pointer;
text-align: left;
color: var(--text-primary);
transition: background 0.15s;
&:hover {
background: rgba(240, 246, 252, 0.04);
}
`;
const SprintNum = styled.span<{ $color: string }>`
font-size: 13px;
font-weight: 700;
color: ${({ $color }) => $color};
min-width: 60px;
`;
const SprintName = styled.span`
font-size: 14px;
font-weight: 600;
flex: 1;
`;
const ProgressWrap = styled.div`
width: 80px;
`;
const Chevron = styled.span<{ $open: boolean }>`
font-size: 12px;
color: var(--text-secondary);
transform: ${({ $open }) => $open ? 'rotate(90deg)' : 'none'};
transition: transform 0.2s;
`;
const Body = styled.div<{ $open: boolean }>`
display: ${({ $open }) => $open ? 'block' : 'none'};
border-top: 1px solid var(--border-color);
`;
const TaskRow = styled.div`
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid var(--border-color);
font-size: 13px;
&:last-child { border-bottom: none; }
&:hover { background: rgba(240,246,252,0.03); }
`;
const TaskId = styled.code`
font-size: 11px;
color: #58A6FF;
min-width: 70px;
font-family: monospace;
`;
const TaskTitle = styled.span`
flex: 1;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Assignee = styled.span`
font-size: 12px;
color: var(--text-secondary);
min-width: 60px;
`;
const Iteration = styled.span<{ $count: number }>`
font-size: 11px;
color: ${({ $count }) => $count > 0 ? '#FF9800' : 'var(--text-secondary)'};
min-width: 28px;
text-align: right;
`;
export default function SprintAccordion({ sprints }: { sprints: Sprint[] }) {
const [openIds, setOpenIds] = useState<Set<number>>(
() => new Set(sprints.filter((s) => s.status === 'in_progress').map((s) => s.id))
);
const toggle = (id: number) => {
setOpenIds((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
return (
<Wrapper>
{sprints.map((sprint) => {
const open = openIds.has(sprint.id);
const color = sprintStatusColor[sprint.status] ?? 'var(--text-secondary)';
return (
<AccordionItem key={sprint.id} $open={open}>
<Header $statusColor={color} onClick={() => toggle(sprint.id)}>
<SprintNum $color={color}>
{sprintStatusIcon[sprint.status] ?? '⏳'} #{sprint.number}
</SprintNum>
<SprintName>{sprint.name}</SprintName>
<ProgressWrap>
<ProgressBar value={sprint.progress} size="sm" />
</ProgressWrap>
<Chevron $open={open}></Chevron>
</Header>
<Body $open={open}>
{sprint.tasks.length === 0 ? (
<TaskRow><TaskTitle style={{ color: 'var(--text-secondary)' }}> </TaskTitle></TaskRow>
) : (
sprint.tasks.map((task) => (
<TaskRow key={task.id}>
<TaskId>{task.taskId}</TaskId>
<TaskTitle>{task.title}</TaskTitle>
<Assignee>{assigneeEmojis[task.assignee] ?? '🤖'} {task.assignee}</Assignee>
<TaskBadge status={task.status as any} />
<Iteration $count={task.iteration}>
{task.iteration > 0 ? `×${task.iteration}` : ''}
</Iteration>
</TaskRow>
))
)}
</Body>
</AccordionItem>
);
})}
</Wrapper>
);
}