/* 회의록 브레인 — 화면 ③ 액션 보드 + 간트 */ function ScreenBoard({ actions, onCycle, onOpenMeeting }) { const DS = window.CalComDesignSystem_436c9d; const { Avatar } = DS; const D = window.MB_DATA; const parse = (s) => new Date(s + "T00:00:00"); // 스크롤 영역 실측 → 달 수가 적으면 가용 폭을 채우고, 많으면 최소폭 유지(스크롤) const scrollRef = React.useRef(null); const [metrics, setMetrics] = React.useState(() => { const w = (typeof window !== "undefined") ? window.innerWidth : 1200; return { labelW: w < 860 ? 120 : 168, minMonthW: w < 640 ? 64 : 92, availW: 0 }; }); React.useLayoutEffect(() => { const measure = () => { const w = window.innerWidth; const labelW = w < 860 ? 120 : 168; const el = scrollRef.current; // 레인 가용 폭 = 스크롤영역 clientWidth − 좌우패딩(44) − 라벨 폭 (−2 안전여백) const avail = el ? el.clientWidth - 44 - labelW - 2 : 0; setMetrics({ labelW, minMonthW: w < 640 ? 64 : 92, availW: Math.max(0, avail) }); }; measure(); let ro; if (typeof ResizeObserver !== "undefined" && scrollRef.current) { ro = new ResizeObserver(measure); ro.observe(scrollRef.current); } window.addEventListener("resize", measure); return () => { window.removeEventListener("resize", measure); if (ro) ro.disconnect(); }; }, []); const { labelW, minMonthW, availW } = metrics; const today = React.useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }, []); const todayStr = React.useMemo(() => { const z = (n) => String(n).padStart(2, "0"); return `${today.getFullYear()}-${z(today.getMonth() + 1)}-${z(today.getDate())}`; }, [today]); // 담당자별 그룹 (시드 PEOPLE 순서 유지) — 진행률 + 간트 행 공용 const owners = React.useMemo(() => { const order = D.PEOPLE.map((p) => p.name); const set = [...new Set(actions.map((a) => a.owner))]; set.sort((x, y) => { const ix = order.indexOf(x), iy = order.indexOf(y); return (ix === -1 ? 99 : ix) - (iy === -1 ? 99 : iy); }); return set; }, [actions]); // 전체 액션을 달 경계로 스냅한 범위 (최소 start月 ~ 최대 end月, 최소 3개월) const span = React.useMemo(() => { if (!actions.length) return null; let lo = Infinity, hi = -Infinity; actions.forEach((a) => { lo = Math.min(lo, +parse(a.start)); hi = Math.max(hi, +parse(a.end)); }); const s = new Date(lo), e = new Date(hi); const startY = s.getFullYear(), startM = s.getMonth(); let count = (e.getFullYear() - startY) * 12 + (e.getMonth() - startM) + 1; if (count < 3) count = 3; // 한두 개 액션일 때 너무 좁지 않게 return { startY, startM, count }; }, [actions]); // 달 한 칸 너비: 달 수가 적으면 가용 폭을 꽉 채워 오른쪽 빈공간 제거, 많으면 최소폭 → 스크롤 const monthW = React.useMemo( () => (span ? Math.max(minMonthW, availW / span.count) : minMonthW), [span, availW, minMonthW], ); // 날짜 → 픽셀 (달 시작 = idx*monthW, 달 내부는 일수 비례 → 눈금과 바가 정확히 정렬) const xOf = React.useCallback((dateStr) => { if (!span) return 0; const d = parse(dateStr); const idx = (d.getFullYear() - span.startY) * 12 + (d.getMonth() - span.startM); const dim = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); return idx * monthW + ((d.getDate() - 1) / dim) * monthW; }, [span, monthW]); const totalW = span ? span.count * monthW : 0; // 월 눈금 — 달마다 1칸, 1월/첫칸은 연도 포함 const ticks = React.useMemo(() => { if (!span) return []; const out = []; for (let i = 0; i < span.count; i++) { const m = (span.startM + i) % 12; const y = span.startY + Math.floor((span.startM + i) / 12); const label = (m === 0 || i === 0) ? `${y}.${m + 1}` : `${m + 1}월`; out.push({ x: i * monthW, label }); } return out; }, [span, monthW]); // 주 눈금 — 각 달을 1·8·15·22일로 세분 (isMonth = 달 경계선) const weekTicks = React.useMemo(() => { if (!span) return []; const out = []; for (let i = 0; i < span.count; i++) { const m = (span.startM + i) % 12; const y = span.startY + Math.floor((span.startM + i) / 12); const dim = new Date(y, m + 1, 0).getDate(); [1, 8, 15, 22].forEach((day) => { out.push({ x: i * monthW + ((day - 1) / dim) * monthW, label: day, isMonth: day === 1 }); }); } return out; }, [span, monthW]); // 오늘선 — 범위 안일 때만 표시 const todayInRange = React.useMemo(() => { if (!span) return false; const startMs = +new Date(span.startY, span.startM, 1); const endMs = +new Date(span.startY, span.startM + span.count, 0); return +today >= startMs && +today <= endMs; }, [span, today]); const todayX = todayInRange ? xOf(todayStr) : null; // 가로 스크롤 제어 (한 달씩 이동 / 오늘로 점프) const scrollByMonths = (n) => { if (scrollRef.current) scrollRef.current.scrollBy({ left: n * monthW, behavior: "smooth" }); }; const scrollToToday = React.useCallback(() => { const el = scrollRef.current; if (!el || !span) return; let tx; if (todayInRange) tx = xOf(todayStr); else tx = (+today < +new Date(span.startY, span.startM, 1)) ? 0 : totalW; el.scrollTo({ left: Math.max(0, tx - (el.clientWidth - labelW) / 2), behavior: "smooth" }); }, [span, todayInRange, xOf, todayStr, today, totalW, labelW]); // 첫 렌더 / 레이아웃 변경 시 시작 위치를 '오늘' 근처로 React.useEffect(() => { scrollToToday(); }, [scrollToToday]); // 진행률 const overall = React.useMemo(() => { if (!actions.length) return { done: 0, progress: 0, planned: 0, rate: 0 }; const done = actions.filter((a) => a.status === "done").length; const progress = actions.filter((a) => a.status === "progress").length; const planned = actions.filter((a) => a.status === "planned").length; return { done, progress, planned, rate: done / actions.length }; }, [actions]); const ownerRate = (name) => { const list = actions.filter((a) => a.owner === name); if (!list.length) return 0; return list.filter((a) => a.status === "done").length / list.length; }; if (!actions.length) { return (
전체 액션 · {actions.length}개
액션 보드