- 백엔드 SDK 이전: FastAPI OpenAPI → orval 자동생성 (React Query 훅 포함) - HTTP 어댑터: ky 기반 customFetcher, fetch httpClient 시그니처 - 아키텍처: features 모듈 / shared 레이어 컨벤션 정의 - 디자인 시스템: Tailwind v4 CSS-first 토큰 (브랜드 색 / status / shadcn) - 커스텀 CSS: utilities / animations 를 custom.css 로 분리 - Docker: 개발(Dockerfile.dev + compose) / 프로덕션(nginx) 셋업 - README: 기술 스펙 / 프로젝트 구조 / 디자인 토큰 / 폰트 / SDK 사용법 정리 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
import { motion } from 'motion/react';
|
|
import { CheckCircle2, Circle } from 'lucide-react';
|
|
import { SectionWrapper } from './ui/SectionWrapper';
|
|
import type { RoadmapMonth } from '@/features/report/types/report';
|
|
|
|
interface RoadmapTimelineProps {
|
|
months: RoadmapMonth[];
|
|
}
|
|
|
|
const monthMeta: Record<number, { badge: string; accent: string }> = {
|
|
1: { badge: 'from-[#6C5CE7] to-[#4F1DA1]', accent: 'border-[#6C5CE7]/20' },
|
|
2: { badge: 'from-[#4F1DA1] to-[#021341]', accent: 'border-[#4F1DA1]/20' },
|
|
3: { badge: 'from-[#021341] to-[#0A1128]', accent: 'border-[#021341]/20' },
|
|
};
|
|
|
|
export default function RoadmapTimeline({ months }: RoadmapTimelineProps) {
|
|
return (
|
|
<SectionWrapper id="roadmap" title="90-Day Roadmap" subtitle="실행 로드맵">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{months.map((month, i) => {
|
|
const meta = monthMeta[month.month] || monthMeta[1];
|
|
return (
|
|
<motion.div
|
|
key={month.month}
|
|
className={`rounded-2xl bg-white border ${meta.accent} shadow-sm overflow-hidden`}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: i * 0.15 }}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center gap-4 p-6 pb-4">
|
|
<div
|
|
className={`w-11 h-11 rounded-full bg-gradient-to-br ${meta.badge} text-white flex items-center justify-center font-bold text-sm shrink-0`}
|
|
>
|
|
{month.month}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-serif font-bold text-xl text-[#0A1128] leading-tight">
|
|
{month.title}
|
|
</h3>
|
|
<p className="text-sm text-slate-500">{month.subtitle}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className="mx-6 border-t border-slate-100" />
|
|
|
|
{/* Task checklist */}
|
|
<ul className="p-6 pt-4 space-y-3">
|
|
{month.tasks.map((task, j) => (
|
|
<motion.li
|
|
key={j}
|
|
className="flex items-start gap-3"
|
|
initial={{ opacity: 0, x: -10 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.3, delay: i * 0.15 + j * 0.05 }}
|
|
>
|
|
{task.completed ? (
|
|
<CheckCircle2 size={18} className="text-[#6C5CE7] shrink-0 mt-0.5" />
|
|
) : (
|
|
<Circle size={18} className="text-slate-300 shrink-0 mt-0.5" />
|
|
)}
|
|
<span
|
|
className={`text-sm leading-relaxed ${
|
|
task.completed ? 'text-slate-400 line-through' : 'text-slate-700'
|
|
}`}
|
|
>
|
|
{task.task}
|
|
</span>
|
|
</motion.li>
|
|
))}
|
|
</ul>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</SectionWrapper>
|
|
);
|
|
}
|