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:
82
backend/src/routes/details.ts
Normal file
82
backend/src/routes/details.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { resolveTaskActorId } from '../lib/resolveUser';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
async function resolveAuthorId(taskId: string, authorName?: string): Promise<string> {
|
||||
const name = authorName?.toString().trim();
|
||||
if (name) {
|
||||
const user = await prisma.user.findFirst({ where: { name } });
|
||||
if (user) return user.id;
|
||||
}
|
||||
return resolveTaskActorId(taskId);
|
||||
}
|
||||
|
||||
// POST /api/details/:taskId
|
||||
router.post('/:taskId', async (req, res, next) => {
|
||||
try {
|
||||
const taskId = req.params.taskId;
|
||||
const { content, milestoneId, authorName } = req.body as Record<string, string>;
|
||||
|
||||
if (!content?.toString().trim()) throw new AppError(400, '피드백 내용은 필수입니다.');
|
||||
|
||||
const task = await prisma.task.findUnique({ where: { id: taskId }, select: { id: true } });
|
||||
if (!task) throw new AppError(404, '업무를 찾을 수 없습니다.');
|
||||
|
||||
if (milestoneId) {
|
||||
const ms = await prisma.milestone.findFirst({ where: { id: milestoneId, taskId } });
|
||||
if (!ms) throw new AppError(400, '유효하지 않은 업무 단계입니다.');
|
||||
}
|
||||
|
||||
const updatedBy = await resolveAuthorId(taskId, authorName);
|
||||
|
||||
const detail = await prisma.taskDetail.create({
|
||||
data: {
|
||||
taskId,
|
||||
milestoneId: milestoneId || null,
|
||||
content: content.toString().trim(),
|
||||
updatedBy,
|
||||
},
|
||||
include: { author: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
res.status(201).json(detail);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/details/item/:id
|
||||
router.patch('/item/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { content } = req.body as Record<string, string>;
|
||||
if (!content?.toString().trim()) throw new AppError(400, '피드백 내용은 필수입니다.');
|
||||
|
||||
const existing = await prisma.taskDetail.findUnique({ where: { id: req.params.id } });
|
||||
if (!existing) throw new AppError(404, '피드백을 찾을 수 없습니다.');
|
||||
|
||||
const detail = await prisma.taskDetail.update({
|
||||
where: { id: req.params.id },
|
||||
data: { content: content.toString().trim() },
|
||||
include: { author: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
res.json(detail);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/details/item/:id
|
||||
router.delete('/item/:id', async (req, res, next) => {
|
||||
try {
|
||||
await prisma.taskDetail.delete({ where: { id: req.params.id } });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user