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(権限チェック済)で配信
This commit is contained in:
Ken Yasue
2026-06-25 10:07:02 +02:00
parent c747978f3d
commit 25d800a529
31 changed files with 1640 additions and 102 deletions

View File

@ -157,4 +157,49 @@ describe('FileStorageService', () => {
service.delete(authorId, file.id); // authorId is admin
expect(() => service.getFileInfo(memberId, file.id)).toThrow();
});
it('uploadForAttachment is silent: no notification/SSE/activity, source=attachment', () => {
const file = service.uploadForAttachment(authorId, projectId, {
originalName: 'pic.png',
mimeType: 'image/png',
data: Buffer.from('img'),
});
expect(file.source).toBe('attachment');
expect(fs.existsSync(file.path)).toBe(true);
// メンバーへの file_shared 通知は飛ばない
expect(new NotificationRepository(db).countUnreadByUser(memberId)).toBe(0);
// file_uploaded アクティビティは記録されない
expect(
new ActivityLogRepository(db)
.findByProject(projectId)
.items.some((l) => l.action === 'file_uploaded')
).toBe(false);
});
it('uploadForAttachment result is excluded from the library list', () => {
service.uploadForAttachment(authorId, projectId, {
originalName: 'pic.png',
mimeType: 'image/png',
data: Buffer.from('img'),
});
const list = service.listFiles(authorId, projectId);
expect(list.total).toBe(0);
});
it('uploadForAttachment still enforces MIME and membership', () => {
expect(() =>
service.uploadForAttachment(authorId, projectId, {
originalName: 'evil.exe',
mimeType: 'application/x-msdownload',
data: Buffer.from('x'),
})
).toThrow(ValidationError);
expect(() =>
service.uploadForAttachment(outsiderId, projectId, {
originalName: 'x.png',
mimeType: 'image/png',
data: Buffer.from('x'),
})
).toThrow(ForbiddenError);
});
});