'use client'; import { forwardRef, useImperativeHandle, useRef, useState, type ChangeEvent, } from 'react'; export interface AttachmentPickerHandle { getFileIds: () => number[]; clear: () => void; } interface PickedFile { fileId: number; originalName: string; mimeType: string; } /** * チャット/掲示板用の添付ファイルピッカー。 * 選択したファイルを添付用エンドポイントへアップロードし、 * 親フォームは送信時に getFileIds() でファイルIDを取り出す。 * 送信完了後は clear() で状態をリセットする。 */ export const AttachmentPicker = forwardRef< AttachmentPickerHandle, { projectId: number; onLoadingChange?: (loading: boolean) => void } >(function AttachmentPicker({ projectId, onLoadingChange }, ref) { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const inputRef = useRef(null); function reportLoading(next: boolean) { setLoading(next); onLoadingChange?.(next); } useImperativeHandle( ref, () => ({ getFileIds: () => files.map((f) => f.fileId), clear: () => { setFiles([]); setError(null); if (inputRef.current) inputRef.current.value = ''; }, }), [files] ); async function onFile(event: ChangeEvent) { const selected = Array.from(event.target.files ?? []); if (selected.length === 0) return; reportLoading(true); setError(null); try { const uploaded: PickedFile[] = []; for (const f of selected) { const form = new FormData(); form.append('file', f); const res = await fetch(`/api/projects/${projectId}/attachments`, { method: 'POST', body: form, }); if (!res.ok) { const b = (await res.json().catch(() => null)) as { error?: { message?: string }; } | null; throw new Error(b?.error?.message ?? 'アップロードに失敗しました'); } const data = (await res.json()) as { file: { id: number; originalName: string; mimeType: string }; }; uploaded.push({ fileId: data.file.id, originalName: data.file.originalName, mimeType: data.file.mimeType, }); } setFiles((prev) => [...prev, ...uploaded]); } catch (err) { setError( err instanceof Error ? err.message : 'アップロードに失敗しました' ); } finally { reportLoading(false); if (inputRef.current) inputRef.current.value = ''; } } function removeFile(fileId: number) { setFiles((prev) => prev.filter((f) => f.fileId !== fileId)); } return (
{loading && (

アップロード中...

)} {error && (

{error}

)} {files.length > 0 && (
    {files.map((f) => { const isImage = f.mimeType.startsWith('image/'); const url = `/api/files/${f.fileId}/download`; return (
  • {isImage ? ( {f.originalName} ) : ( {f.originalName} )}
  • ); })}
)}
); });