Files
hanarang-dashboard/frontend/components/projects/ProjectCard.tsx
narang-ai 52732e88b9 feat(sprint-002): projects + task ledger + activity feed
Sprint 001 non-blocking 해결:
- N6: npm audit high 패키지 업데이트 (@nestjs/config, @nestjs/cli)
- S1: CORS origin 화이트리스트 (CORS_ORIGINS env)
- S3: DATABASE_URL 미설정 시 즉시 throw
- N2: SSH_KEY_PATH 미설정 시 즉시 throw (하드코딩 fallback 제거)
- N3: API 응답에서 내부 IP 제거
- N7: SshService 에러 메시지 마스킹 (IP 노출 차단)
- N4: theme.ts 색상 SisterCard/StatusBadge에 실제 적용
- N5: Sidebar-MainContent margin 연동 (SidebarContext)
- N9: LayoutShell Client 컴포넌트로 분리

Sprint 002 본문:
- TASK-004: GiteaService (org repo 목록, PR 조회)
- TASK-004: GET /api/projects → Gitea + DB 병합
- TASK-004: GET /api/projects/:id → 상세 + open PRs
- TASK-005: GET /api/projects/:id/tasks → Sprint/Task Ledger
- TASK-005: POST /api/projects/:id/sprints → Sprint 생성
- TASK-005: PATCH /api/tasks/:id → Task 상태 업데이트 + ActivityLog
- TASK-006: GET /api/activity, /api/projects/:id/activity
- TASK-007: /projects/[id] 상세 페이지 (SprintAccordion + TaskTable + 활동탭)
- TASK-007: 메인 대시보드에 프로젝트 섹션 + 활동 피드 추가
- 테스트 11/11 pass
2026-04-04 11:34:04 +09:00

116 lines
2.6 KiB
TypeScript

'use client';
import React from 'react';
import styled from 'styled-components';
import Link from 'next/link';
import ProgressBar from '../common/ProgressBar';
import { theme } from '@/styles/theme';
interface ProjectCardProps {
id: number;
name: string;
description: string | null;
status: string;
progress: number;
doneTasks: number;
totalTasks: number;
openPRs: number;
currentSprint: string | null;
}
const Card = styled(Link)`
display: block;
background: ${theme.colors.cardBg};
backdrop-filter: blur(12px);
border: 1px solid ${theme.colors.border};
border-radius: 12px;
padding: 20px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
text-decoration: none;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border-color: rgba(88,166,255,0.3);
}
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
gap: 8px;
`;
const Name = styled.span`
font-size: 15px;
font-weight: 600;
color: ${theme.colors.textPrimary};
`;
const Meta = styled.div`
font-size: 12px;
color: ${theme.colors.textSecondary};
margin-bottom: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Stats = styled.div`
display: flex;
gap: 16px;
margin-top: 12px;
`;
const Stat = styled.div`
font-size: 12px;
color: ${theme.colors.textSecondary};
display: flex;
align-items: center;
gap: 4px;
`;
const ProgressLabel = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: 6px;
`;
const ProgressText = styled.span`
font-size: 11px;
color: ${theme.colors.textSecondary};
`;
const PRBadge = styled.span<{ $count: number }>`
font-size: 11px;
padding: 2px 7px;
border-radius: 4px;
background: ${({ $count }) => $count > 0 ? 'rgba(88,166,255,0.12)' : 'rgba(139,148,158,0.08)'};
color: ${({ $count }) => $count > 0 ? theme.colors.accent : theme.colors.textSecondary};
font-weight: 600;
`;
export default function ProjectCard({
id, name, description, progress, doneTasks, totalTasks, openPRs, currentSprint,
}: ProjectCardProps) {
return (
<Card href={`/projects/${id}`}>
<Header>
<Name>{name}</Name>
<PRBadge $count={openPRs}>PR {openPRs}</PRBadge>
</Header>
{description && <Meta>{description}</Meta>}
{currentSprint && (
<Meta>📌 {currentSprint}</Meta>
)}
<ProgressLabel>
<ProgressText></ProgressText>
<ProgressText>{progress}% ({doneTasks}/{totalTasks})</ProgressText>
</ProgressLabel>
<ProgressBar value={progress} size="sm" />
</Card>
);
}