From fa8ed76e22d0a77d73c426984f5f14fb27565f17 Mon Sep 17 00:00:00 2001 From: EENE Dashboard Date: Fri, 5 Jun 2026 22:44:52 +0900 Subject: [PATCH] fix: file preview URLs and milestone web link saving Co-authored-by: Cursor --- backend/src/routes/files.ts | 13 ++++++++++++- frontend/src/components/detail/ExcelPreview.tsx | 5 +++-- frontend/src/components/detail/ResultPreview.tsx | 3 ++- frontend/src/components/detail/StageModal.tsx | 11 ++++++++++- frontend/src/lib/apiClient.ts | 8 ++++++++ frontend/src/pages/DetailPage.tsx | 2 +- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/files.ts b/backend/src/routes/files.ts index 01e6e17..cc6463c 100644 --- a/backend/src/routes/files.ts +++ b/backend/src/routes/files.ts @@ -1,4 +1,4 @@ -import { Router } from 'express'; +import { Router, type Response } from 'express'; import path from 'path'; import fs from 'fs'; import { prisma } from '../lib/prisma'; @@ -8,6 +8,16 @@ import { AppError } from '../middleware/errorHandler'; const router = Router(); +/** Vercel 상세 창에서 PDF 등 iframe 미리보기 허용 */ +function allowCrossOriginPreview(res: Response) { + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); + res.setHeader( + 'Content-Security-Policy', + "frame-ancestors 'self' https://eene-dashboard.vercel.app https://*.vercel.app http://localhost:3000", + ); + res.removeHeader('X-Frame-Options'); +} + /** multer가 latin1로 전달하는 한글 파일명 복원 */ function fixOriginalName(name: string): string { try { @@ -70,6 +80,7 @@ router.get('/:id/view', async (req, res, next) => { if (!file) throw new AppError(404, '파일을 찾을 수 없습니다.'); if (!fs.existsSync(file.path)) throw new AppError(404, '파일이 서버에 없습니다.'); + allowCrossOriginPreview(res); res.setHeader('Content-Type', file.mimetype); res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(file.originalName)}"`); fs.createReadStream(file.path).pipe(res); diff --git a/frontend/src/components/detail/ExcelPreview.tsx b/frontend/src/components/detail/ExcelPreview.tsx index 7841175..32b5fa1 100644 --- a/frontend/src/components/detail/ExcelPreview.tsx +++ b/frontend/src/components/detail/ExcelPreview.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import * as XLSX from 'xlsx'; +import { fileDownloadUrl, fileViewUrl } from '../../lib/apiClient'; interface ExcelPreviewProps { fileId: string; @@ -21,7 +22,7 @@ export function ExcelPreview({ fileId, fileName }: ExcelPreviewProps) { (async () => { try { - const res = await fetch(`/api/files/${fileId}/view`); + const res = await fetch(fileViewUrl(fileId)); if (!res.ok) throw new Error('파일을 불러올 수 없습니다.'); const buffer = await res.arrayBuffer(); const wb = XLSX.read(buffer, { type: 'array' }); @@ -48,7 +49,7 @@ export function ExcelPreview({ fileId, fileName }: ExcelPreviewProps) {

{error}

{fileName} 다운로드 diff --git a/frontend/src/components/detail/ResultPreview.tsx b/frontend/src/components/detail/ResultPreview.tsx index b3385c4..50dfd7a 100644 --- a/frontend/src/components/detail/ResultPreview.tsx +++ b/frontend/src/components/detail/ResultPreview.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { ExcelPreview } from './ExcelPreview'; +import { fileViewUrl } from '../../lib/apiClient'; import { openLinkOnRightMonitor } from '../../lib/dualMonitor'; import { fileDisplayName, isExcelFile, isVideoFile } from '../../lib/fileDisplay'; import type { FileRecord, MilestoneLink } from '../../types'; @@ -53,7 +54,7 @@ export function ResultPreview({ files, links, hasSelectedStage }: ResultPreviewP if (isExcel) { return ; } - const src = `/api/files/${activeFile.id}/view`; + const src = fileViewUrl(activeFile.id); if (isImage) { return ( a.sortOrder - b.sortOrder); const uploads: PendingFileUpload[] = []; const existingEdits: ExistingFileEdit[] = []; @@ -312,7 +321,7 @@ export function StageModal({ } }); - await onSave(form, { + await onSave(saveForm, { uploads, existingEdits, deletedFileIds, diff --git a/frontend/src/lib/apiClient.ts b/frontend/src/lib/apiClient.ts index 572c8f8..00f4c94 100644 --- a/frontend/src/lib/apiClient.ts +++ b/frontend/src/lib/apiClient.ts @@ -16,6 +16,14 @@ export const apiClient = axios.create({ }, }); +export function fileViewUrl(fileId: string): string { + return `${baseURL}/files/${fileId}/view`; +} + +export function fileDownloadUrl(fileId: string): string { + return `${baseURL}/files/${fileId}/download`; +} + apiClient.interceptors.request.use((config) => { if (config.data instanceof FormData) { delete config.headers['Content-Type']; diff --git a/frontend/src/pages/DetailPage.tsx b/frontend/src/pages/DetailPage.tsx index c8c6cce..90bbd5b 100644 --- a/frontend/src/pages/DetailPage.tsx +++ b/frontend/src/pages/DetailPage.tsx @@ -286,7 +286,7 @@ function DetailView({ task }: { task: TaskWithRelations }) { startDate: data.startDate || undefined, dueDate: data.dueDate || undefined, progress: data.progress, - links: data.links.length > 0 ? JSON.stringify(data.links) : undefined, + links: data.links, }; let milestoneId: string;