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
116 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|