import { useEffect, useState } from 'react' import { getTimeline, type Org, type TimelineEvent } from '../api/client' import { stageLabel } from '../stages' const dateFormatter = new Intl.DateTimeFormat('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short', }) const timeFormatter = new Intl.DateTimeFormat('ko-KR', { hour: '2-digit', minute: '2-digit', hour12: false, }) function dateParts(at: number): { year: string; month: string; day: string; weekday: string } { const parts = dateFormatter.formatToParts(new Date(at)) const get = (type: string) => parts.find((p) => p.type === type)?.value ?? '' return { year: get('year'), month: get('month'), day: get('day'), weekday: get('weekday') } } function dateKey(at: number): string { const { year, month, day } = dateParts(at) return `${year}-${month}-${day}` } function dateHeader(at: number): string { const { year, month, day, weekday } = dateParts(at) return `${year}-${month}-${day} (${weekday})` } function formatTime(at: number): string { return timeFormatter.format(new Date(at)) } interface EventGroup { key: string header: string items: TimelineEvent[] } function groupByDate(events: TimelineEvent[]): EventGroup[] { const groups: EventGroup[] = [] for (const event of events) { const key = dateKey(event.at) const current = groups[groups.length - 1] if (current && current.key === key) { current.items.push(event) } else { groups.push({ key, header: dateHeader(event.at), items: [event] }) } } return groups } const EVENT_META: Record = { STAGE: { dot: 'bg-blue-600', icon: '๐Ÿšฉ', badge: '๋‹จ๊ณ„ ๋ณ€๊ฒฝ', badgeClass: 'bg-blue-50 text-blue-700' }, FILE: { dot: 'bg-amber-500', icon: '๐Ÿ“Ž', badge: '์ž๋ฃŒ ๋“ฑ๋ก', badgeClass: 'bg-amber-50 text-amber-700' }, MEMO: { dot: 'bg-emerald-600', icon: 'โœ๏ธ', badge: '์—…๋ฌด๋ฉ”๋ชจ', badgeClass: 'bg-emerald-50 text-emerald-700' }, } /** STAGE ์ด๋ฒคํŠธ์˜ title(๋‹จ๊ณ„ ๋ฒˆํ˜ธ ๋ฌธ์ž์—ด)์„ "5. ๋ฒ•๋ฅ ๊ฒ€ํ† (๊ถŒ๋ฆฌํ™•์ธ) ์ง„ํ–‰" ํ˜•ํƒœ๋กœ ๋ฐ”๊พผ๋‹ค. */ function stageEventText(title: string): string { const n = Number(title) if (!Number.isInteger(n)) { return `${title} ๋‹จ๊ณ„๋กœ ์ง„ํ–‰` } return `${n}. ${stageLabel(n)} ๋‹จ๊ณ„๋กœ ์ง„ํ–‰` } function EventBody({ event }: { event: TimelineEvent }) { if (event.type === 'STAGE') { return

{stageEventText(event.title)}

} if (event.type === 'FILE') { return (

{event.title} {event.detail && {event.detail}}

) } return (

{event.title} {event.detail && {event.detail}}

) } /** ๊ธฐ๊ด€๊ด€๋ฆฌ ์ƒ์„ธ์˜ ํƒ€์ž„๋ผ์ธ ํƒญ. ๋‹จ๊ณ„ ๋ณ€๊ฒฝ ์ด๋ ฅยท์—…๋ฌด๋ฉ”๋ชจยท์ฑ„๋„ ์ฒจ๋ถ€ํŒŒ์ผ ๋“ฑ๋ก์„ ํ•˜๋‚˜์˜ ์‹œ๊ฐ„์ˆœ * ๋ชฉ๋ก์œผ๋กœ ํ•ฉ์ณ ๋ณด์—ฌ์ค€๋‹ค(์กฐํšŒ๋Š” GET /api/orgs/{id}/timeline ํ•œ ๋ฒˆ๋ฟ์ด๊ณ  ํด๋ง์€ ์—†๋‹ค). */ export default function Timeline({ org }: { org: Org }) { const [events, setEvents] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [reloadKey, setReloadKey] = useState(0) useEffect(() => { let cancelled = false setLoading(true) setError(null) getTimeline(org.id) .then((data) => { if (!cancelled) { setEvents(data) } }) .catch((e: Error) => { if (!cancelled) { setError(e.message) } }) .finally(() => { if (!cancelled) { setLoading(false) } }) return () => { cancelled = true } }, [org.id, reloadKey]) const groups = groupByDate(events) return (
{loading &&

๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ

} {!loading && error &&

{error}

} {!loading && !error && events.length === 0 && (

์•„์ง ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค. ์ฑ„๋„์„ ๋งŒ๋“ค๊ณ  ์—…๋ฌด๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด ์—ฌ๊ธฐ์— ์Œ“์ž…๋‹ˆ๋‹ค.

)} {!loading && !error && events.length > 0 && (
{groups.map((group) => (

{group.header}

    {group.items.map((event, i) => { const meta = EVENT_META[event.type] return (
  1. {formatTime(event.at)} {meta.badge}
  2. ) })}
))}
)}
) }