File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { useEffect, useState } from 'react'
import { getFiles, fileDownloadUrl, type ChannelKindKey, type FileView, type Org } from '../api/client'
const dateFormatter = new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
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 formatDate(createAt: number): string {
// Intl 기본 포맷은 "2026. 07. 22. 14:05" 형태라 화면 요구사항(yyyy-MM-dd HH:mm)에 맞게 다듬는다.
const parts = dateFormatter.formatToParts(new Date(createAt))
const get = (type: string) => parts.find((p) => p.type === type)?.value ?? ''
return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}`
}
/** 기관관리 상세의 자료 탭. 선택한 채널에 올라온 첨부파일 전체를 목록으로 보여준다(폴링 없음). */
export default function ChannelFiles({ org }: { org: Org }) {
const bothMissing = !org.channelIdMj && !org.channelIdLaw
const [channel, setChannel] = useState<ChannelKindKey>(org.channelIdMj ? 'mj' : 'law')
const [files, setFiles] = useState<FileView[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [reloadKey, setReloadKey] = useState(0)
useEffect(() => {
if (bothMissing) {
return
}
let cancelled = false
setLoading(true)
setError(null)
getFiles(org.id, channel)
.then((data) => {
if (!cancelled) {
setFiles(data)
}
})
.catch((e: Error) => {
if (!cancelled) {
setError(e.message)
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => {
cancelled = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channel, org.id, reloadKey])
return (
<div className="p-4">
<div className="flex items-center justify-between gap-2">
<div className="flex gap-2">
<button
type="button"
disabled={!org.channelIdMj}
title={org.channelIdMj ? undefined : '채널 미생성'}
onClick={() => setChannel('mj')}
className={`rounded-full border px-3 py-1 text-xs ${
channel === 'mj'
? 'border-blue-600 bg-blue-50 font-semibold text-blue-700'
: 'border-gray-200 text-gray-500'
} disabled:cursor-not-allowed disabled:opacity-40`}
>
문정원
</button>
<button
type="button"
disabled={!org.channelIdLaw}
title={org.channelIdLaw ? undefined : '채널 미생성'}
onClick={() => setChannel('law')}
className={`rounded-full border px-3 py-1 text-xs ${
channel === 'law'
? 'border-blue-600 bg-blue-50 font-semibold text-blue-700'
: 'border-gray-200 text-gray-500'
} disabled:cursor-not-allowed disabled:opacity-40`}
>
법률검토
</button>
</div>
{!bothMissing && (
<button
type="button"
onClick={() => setReloadKey((k) => k + 1)}
className="rounded-md border border-gray-300 px-3 py-1 text-xs text-gray-600 hover:bg-gray-50"
>
새로고침
</button>
)}
</div>
{bothMissing ? (
<p className="mt-6 text-center text-sm text-gray-400">채널이 아직 생성되지 않았습니다.</p>
) : (
<div className="mt-3">
{loading && <p className="py-6 text-center text-sm text-gray-400">불러오는 중…</p>}
{!loading && error && <p className="py-4 text-sm text-red-600">{error}</p>}
{!loading && !error && files.length === 0 && (
<p className="py-6 text-center text-sm text-gray-300">등록된 자료가 없습니다.</p>
)}
{!loading && !error && files.length > 0 && (
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-200 text-xs text-gray-400">
<th className="py-2 font-medium">파일명</th>
<th className="py-2 font-medium">크기</th>
<th className="py-2 font-medium">올린 사람</th>
<th className="py-2 font-medium">올린 시각</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr key={file.id} className="border-b border-gray-100">
<td className="py-2">
<a href={fileDownloadUrl(file.id)} className="text-blue-600 hover:underline">
{file.name}
</a>
</td>
<td className="py-2 text-gray-600">{humanizeSize(file.size)}</td>
<td className="py-2 text-gray-600">{file.uploader}</td>
<td className="py-2 text-gray-600">{formatDate(file.createAt)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
)
}