224 lines
5.6 KiB
TypeScript
224 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import styled from 'styled-components';
|
|
import SisterActivityLane from './SisterActivityLane';
|
|
import WorkflowTimeline from './WorkflowTimeline';
|
|
import ActiveTaskFocus from './ActiveTaskFocus';
|
|
import { mockLiveWorkboard, rotateEvents } from '@/lib/control/mock-events';
|
|
|
|
const Root = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-lg);
|
|
`;
|
|
|
|
const Hero = styled.header`
|
|
border: 1px solid rgba(255,255,255,0.12);
|
|
border-radius: 28px;
|
|
padding: 24px;
|
|
background:
|
|
radial-gradient(circle at 16% 18%, rgba(255,143,112,0.18), transparent 34%),
|
|
radial-gradient(circle at 82% 12%, rgba(139,211,255,0.16), transparent 28%),
|
|
linear-gradient(135deg, rgba(255,255,255,0.075), rgba(255,255,255,0.02));
|
|
`;
|
|
|
|
const HeroTop = styled.div`
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 16px;
|
|
|
|
@media (max-width: 767px) {
|
|
flex-direction: column;
|
|
}
|
|
`;
|
|
|
|
const TitleBlock = styled.div`
|
|
display: grid;
|
|
gap: 8px;
|
|
`;
|
|
|
|
const Label = styled.div`
|
|
font-family: var(--font-mono);
|
|
color: var(--text-secondary);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
`;
|
|
|
|
const Title = styled.h1`
|
|
margin: 0;
|
|
font-size: clamp(30px, 5vw, 58px);
|
|
line-height: 0.95;
|
|
letter-spacing: -0.055em;
|
|
`;
|
|
|
|
const Summary = styled.p`
|
|
margin: 0;
|
|
max-width: 760px;
|
|
color: var(--text-secondary);
|
|
font-size: 14px;
|
|
`;
|
|
|
|
const SourceBadge = styled.div`
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border: 1px solid rgba(255, 204, 102, 0.35);
|
|
border-radius: 999px;
|
|
padding: 8px 12px;
|
|
color: #ffcc66;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
const Dot = styled.span`
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: #ffcc66;
|
|
box-shadow: 0 0 18px rgba(255, 204, 102, 0.95);
|
|
`;
|
|
|
|
const PhaseGrid = styled.div`
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 8px;
|
|
margin-top: 22px;
|
|
|
|
@media (max-width: 767px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
`;
|
|
|
|
const PhaseCell = styled.div<{ $active: boolean }>`
|
|
border: 1px solid ${({ $active }) => ($active ? 'rgba(255,255,255,0.36)' : 'rgba(255,255,255,0.09)')};
|
|
border-radius: 14px;
|
|
padding: 10px;
|
|
color: ${({ $active }) => ($active ? 'var(--text-primary)' : 'var(--text-secondary)')};
|
|
background: ${({ $active }) => ($active ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.16)')};
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
`;
|
|
|
|
const MainGrid = styled.div`
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 360px;
|
|
gap: var(--space-lg);
|
|
|
|
@media (max-width: 1100px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
`;
|
|
|
|
const LanesGrid = styled.section`
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: var(--space-md);
|
|
|
|
@media (max-width: 900px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
`;
|
|
|
|
const BottomGrid = styled.div`
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
|
|
gap: var(--space-lg);
|
|
|
|
@media (max-width: 1100px) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
`;
|
|
|
|
const NoteCard = styled.section`
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 22px;
|
|
padding: 18px;
|
|
background: rgba(255,255,255,0.025);
|
|
`;
|
|
|
|
const NoteTitle = styled.h2`
|
|
margin: 0 0 8px;
|
|
font-size: 15px;
|
|
`;
|
|
|
|
const NoteText = styled.p`
|
|
margin: 0;
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
`;
|
|
|
|
const phases = [
|
|
['planning', 'PLAN'],
|
|
['building', 'BUILD'],
|
|
['reviewing', 'REVIEW'],
|
|
['waiting_approval', 'APPROVAL'],
|
|
['deploying', 'DEPLOY'],
|
|
] as const;
|
|
|
|
export default function LiveWorkboard() {
|
|
const [tick, setTick] = useState(0);
|
|
const [selectedId, setSelectedId] = useState(mockLiveWorkboard.events[0]?.id ?? '');
|
|
|
|
const events = useMemo(() => rotateEvents(mockLiveWorkboard.events, tick), [tick]);
|
|
const selected = events.find((event) => event.id === selectedId) ?? events[0];
|
|
|
|
useEffect(() => {
|
|
const timer = window.setInterval(() => {
|
|
setTick((value) => value + 1);
|
|
}, 4200);
|
|
return () => window.clearInterval(timer);
|
|
}, []);
|
|
|
|
return (
|
|
<Root>
|
|
<Hero>
|
|
<HeroTop>
|
|
<TitleBlock>
|
|
<Label>4 Sisters Live Workboard</Label>
|
|
<Title>지금 4자매가 어떻게 일하는지</Title>
|
|
<Summary>{mockLiveWorkboard.workflow.summary}</Summary>
|
|
</TitleBlock>
|
|
<SourceBadge aria-label="data source mock">
|
|
<Dot /> source: {mockLiveWorkboard.source} · v0
|
|
</SourceBadge>
|
|
</HeroTop>
|
|
|
|
<PhaseGrid>
|
|
{phases.map(([phase, label]) => (
|
|
<PhaseCell key={phase} $active={mockLiveWorkboard.workflow.phase === phase}>
|
|
{label}
|
|
</PhaseCell>
|
|
))}
|
|
</PhaseGrid>
|
|
</Hero>
|
|
|
|
<MainGrid>
|
|
<LanesGrid>
|
|
{mockLiveWorkboard.sisters.map((lane) => (
|
|
<SisterActivityLane key={lane.id} lane={lane} />
|
|
))}
|
|
</LanesGrid>
|
|
<ActiveTaskFocus selectedEvent={selected} approvals={mockLiveWorkboard.approvals} />
|
|
</MainGrid>
|
|
|
|
<BottomGrid>
|
|
<WorkflowTimeline
|
|
events={events}
|
|
selectedId={selected.id}
|
|
onSelect={(event) => setSelectedId(event.id)}
|
|
/>
|
|
<NoteCard>
|
|
<NoteTitle>v0 범위</NoteTitle>
|
|
<NoteText>
|
|
이 화면은 아직 mock event stream이야. 목적은 먼저 “4자매가 작업하는 과정이 보이는 경험”을 검증하는 것. 다음 단계에서 Socket.IO live event와 `/control/summary`를 붙이면 돼.
|
|
</NoteText>
|
|
</NoteCard>
|
|
</BottomGrid>
|
|
</Root>
|
|
);
|
|
}
|