import { useEffect, useRef, useState } from 'react' import { getPosts, fileDownloadUrl, type ChannelKindKey, type FileRef, type Org, type PostView } from '../api/client' const POLL_INTERVAL_MS = 3000 const timeFormatter = new Intl.DateTimeFormat('ko-KR', { hour: '2-digit', minute: '2-digit' }) function humanizeSize(bytes: number): string { if (bytes < 1024) { return `${bytes} B` } const kb = bytes / 1024 if (kb < 1024) { return `${kb.toFixed(1)} KB` } return `${(kb / 1024).toFixed(1)} MB` } function AttachmentChip({ file }: { file: FileRef }) { return ( {file.name} ({humanizeSize(file.size)}) ) } // 이름에서 결정되는 아바타 색 — 같은 사람은 항상 같은 색이 나온다 (Mattermost 이니셜 아바타 방식) const AVATAR_COLORS = [ 'bg-rose-500', 'bg-orange-500', 'bg-amber-500', 'bg-emerald-500', 'bg-teal-500', 'bg-sky-500', 'bg-indigo-500', 'bg-violet-500', 'bg-fuchsia-500', ] function avatarColor(name: string): string { let hash = 0 for (const ch of name) { hash = (hash + ch.codePointAt(0)!) % AVATAR_COLORS.length } return AVATAR_COLORS[hash] } function Avatar({ name }: { name: string }) { return ( ) } /** 같은 사람이 5분 안에 연달아 쓴 메시지는 아바타·이름 없이 이어 붙인다 (Mattermost와 동일) */ const GROUP_WINDOW_MS = 5 * 60 * 1000 function PostRow({ post, prev }: { post: PostView; prev: PostView | null }) { if (post.system) { return

{post.message}

} const continued = prev !== null && !prev.system && prev.user === post.user && post.createAt - prev.createAt < GROUP_WINDOW_MS return (
{continued ? : }
{!continued && (
{post.user} {timeFormatter.format(new Date(post.createAt))}
)} {post.message && (

{post.message}

)} {post.files.length > 0 && (
{post.files.map((f) => ( ))}
)}
) } /** 기관관리 상세의 게시물 탭. 선택한 채널의 최근 게시글을 3초 간격으로 폴링해서 보여준다. */ export default function ChannelPosts({ org }: { org: Org }) { const bothMissing = !org.channelIdMj && !org.channelIdLaw const [channel, setChannel] = useState(org.channelIdMj ? 'mj' : 'law') const [posts, setPosts] = useState([]) const [error, setError] = useState(null) const containerRef = useRef(null) const prevCountRef = useRef(0) useEffect(() => { if (bothMissing) { return } let cancelled = false async function load() { try { const data = await getPosts(org.id, channel) if (!cancelled) { setPosts(data) setError(null) } } catch { if (!cancelled) { setError('연결이 원활하지 않습니다. 재시도 중…') } } } void load() const timer = setInterval(() => void load(), POLL_INTERVAL_MS) return () => { cancelled = true clearInterval(timer) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [channel, org.id]) useEffect(() => { if (posts.length > prevCountRef.current && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight } prevCountRef.current = posts.length }, [posts.length]) return (
{bothMissing ? (

채널이 아직 생성되지 않았습니다.

) : ( <> {error && (

{error}

)}
{posts.length === 0 ? (

아직 게시글이 없습니다. Mattermost에서 메시지를 보내면 여기 나타납니다.

) : ( posts.map((post, i) => ( 0 ? posts[i - 1] : null} /> )) )}
)}
) }