feat: detail page attachments, preview, and file metadata

Add file displayName/sortOrder APIs, result preview with Excel/video support,
unified attachment/link editing, feedback modal, and AuthProvider fix.
Run prisma migrate deploy on Render builds.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-05 18:32:56 +09:00
parent c3f620a7ac
commit 9abb58e5c8
22 changed files with 1477 additions and 292 deletions

View File

@@ -8,6 +8,15 @@ import { AppError } from '../middleware/errorHandler';
const router = Router();
/** multer가 latin1로 전달하는 한글 파일명 복원 */
function fixOriginalName(name: string): string {
try {
return Buffer.from(name, 'latin1').toString('utf8') || name;
} catch {
return name;
}
}
// POST /api/files/upload/:taskId — 파일 업로드
router.post('/upload/:taskId', upload.single('file'), async (req, res, next) => {
try {
@@ -29,12 +38,17 @@ router.post('/upload/:taskId', upload.single('file'), async (req, res, next) =>
const uploadedBy = await resolveTaskActorId(taskId);
const displayName = body.displayName?.trim() || null;
const sortOrder = body.sortOrder !== undefined ? Number(body.sortOrder) : 0;
const fileRecord = await prisma.file.create({
data: {
taskId,
milestoneId,
filename: req.file.filename,
originalName: req.file.originalname,
originalName: fixOriginalName(req.file.originalname),
displayName,
sortOrder: Number.isNaN(sortOrder) ? 0 : sortOrder,
mimetype: req.file.mimetype,
size: req.file.size,
path: req.file.path,
@@ -77,6 +91,56 @@ router.get('/:id/download', async (req, res, next) => {
}
});
// POST /api/files/:id/replace — 파일 교체
router.post('/:id/replace', upload.single('file'), async (req, res, next) => {
try {
if (!req.file) throw new AppError(400, '파일이 없습니다.');
const existing = await prisma.file.findUnique({ where: { id: req.params.id } });
if (!existing) throw new AppError(404, '파일을 찾을 수 없습니다.');
if (fs.existsSync(existing.path)) {
fs.unlinkSync(existing.path);
}
const file = await prisma.file.update({
where: { id: req.params.id },
data: {
filename: req.file.filename,
originalName: fixOriginalName(req.file.originalname),
mimetype: req.file.mimetype,
size: req.file.size,
path: req.file.path,
},
});
res.json(file);
} catch (err) {
next(err);
}
});
// PATCH /api/files/:id — 표시명·순서 수정
router.patch('/:id', async (req, res, next) => {
try {
const { displayName, sortOrder } = req.body as { displayName?: string; sortOrder?: number };
const existing = await prisma.file.findUnique({ where: { id: req.params.id } });
if (!existing) throw new AppError(404, '파일을 찾을 수 없습니다.');
const file = await prisma.file.update({
where: { id: req.params.id },
data: {
...(displayName !== undefined && { displayName: displayName?.trim() || null }),
...(sortOrder !== undefined && { sortOrder: Number(sortOrder) }),
},
});
res.json(file);
} catch (err) {
next(err);
}
});
// DELETE /api/files/:id — 파일 삭제
router.delete('/:id', async (req, res, next) => {
try {