Files
Ken Yasue 25d800a529 feat(chat,board): file/image attachments on chat, board threads and comments
チャット・掲示板(スレッド/コメント)へファイル/画像添付を追加。

- lib/db/migrations/002_attachments.sql: attachments テーブル + file_assets.source 列
- repositories/AttachmentRepository + services/AttachmentService: 添付紐付け(プロジェクト/アップロード者チェック)
- FileStorageService.uploadForAttachment: 通知/SSE/アクティビティを行わない添付専用アップロード(source='attachment')
- ChatService/BoardService: fileIds 受理・添付紐付け・履歴/詳細表示・削除クリーンアップ(トランザクション)
- API: POST /attachments + chat/board の fileIds 受理
- UI: AttachmentList(画像サムネイル+Lightbox/ダウンロード) + AttachmentPicker(複数アップロード・送信中は送信不可) をチャット/スレッド/コメントに統合
- 添付ファイルは既存の /api/files/[fileId]/download(権限チェック済)で配信
2026-06-25 10:07:02 +02:00

77 lines
2.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useRef, useState, type FormEvent } from 'react';
import { useRouter } from 'next/navigation';
import { AttachmentPicker } from '@/components/files/AttachmentPicker';
import type { AttachmentPickerHandle } from '@/components/files/AttachmentPicker';
export function CommentForm({
projectId,
threadId,
}: {
projectId: number;
threadId: number;
}) {
const router = useRouter();
const pickerRef = useRef<AttachmentPickerHandle>(null);
const [bodyMd, setBodyMd] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pickerLoading, setPickerLoading] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setLoading(true);
setError(null);
const fileIds = pickerRef.current?.getFileIds() ?? [];
const res = await fetch(
`/api/projects/${projectId}/board/threads/${threadId}/comments`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bodyMd, fileIds }),
}
);
setLoading(false);
if (res.ok) {
setBodyMd('');
pickerRef.current?.clear();
router.refresh();
} else {
const b = (await res.json().catch(() => null)) as {
error?: { message?: string };
} | null;
setError(b?.error?.message ?? '投稿に失敗しました');
}
}
return (
<form onSubmit={onSubmit} className="space-y-2">
<textarea
placeholder="コメントMarkdown"
value={bodyMd}
onChange={(e) => setBodyMd(e.target.value)}
className="min-h-[80px] w-full rounded border px-3 py-2"
required
/>
<AttachmentPicker
ref={pickerRef}
projectId={projectId}
onLoadingChange={setPickerLoading}
/>
{error && (
<p className="text-sm text-red-600" role="alert">
{error}
</p>
)}
<button
type="submit"
disabled={loading || pickerLoading}
className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? '投稿中...' : 'コメント投稿'}
</button>
</form>
);
}