diff --git a/backend-node/src/config/environment.ts b/backend-node/src/config/environment.ts index a4c6c33b..e350642d 100644 --- a/backend-node/src/config/environment.ts +++ b/backend-node/src/config/environment.ts @@ -75,6 +75,8 @@ const getCorsOrigin = (): string[] | boolean => { "http://localhost:9771", // 로컬 개발 환경 "http://192.168.0.70:5555", // 내부 네트워크 접근 "http://39.117.244.52:5555", // 외부 네트워크 접근 + "https://v1.vexplor.com", // 운영 프론트엔드 + "https://api.vexplor.com", // 운영 백엔드 ]; }; diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index f0ddf80c..27adafe3 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -340,7 +340,7 @@ export default function ScreenViewPage() { webType={(() => { // 유틸리티 함수로 파일 컴포넌트 감지 if (isFileComponent(component)) { - console.log(`🎯 page.tsx - 파일 컴포넌트 감지 → webType: "file"`, { + console.log('🎯 page.tsx - 파일 컴포넌트 감지 → webType: "file"', { componentId: component.id, componentType: component.type, originalWebType: component.webType, diff --git a/frontend/components/screen/MenuAssignmentModal.tsx b/frontend/components/screen/MenuAssignmentModal.tsx index e6685301..6e1c16cb 100644 --- a/frontend/components/screen/MenuAssignmentModal.tsx +++ b/frontend/components/screen/MenuAssignmentModal.tsx @@ -202,17 +202,16 @@ export const MenuAssignmentModal: React.FC = ({ setAssignmentSuccess(true); setAssignmentMessage(successMessage); - // 할당 완료 콜백 호출 + // 할당 완료 콜백 호출 (모달은 아직 열린 상태 유지) if (onAssignmentComplete) { onAssignmentComplete(); } - // 3초 후 자동으로 화면 목록으로 이동 + // 3초 후 자동으로 모달 닫고 화면 목록으로 이동 setTimeout(() => { + onClose(); // 모달 닫기 if (onBackToList) { onBackToList(); - } else { - onClose(); } }, 3000); } catch (error: any) { @@ -232,17 +231,16 @@ export const MenuAssignmentModal: React.FC = ({ setAssignmentSuccess(true); setAssignmentMessage(`"${screenInfo.screenName}" 화면이 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다.`); - // 할당 완료 콜백 호출 + // 할당 완료 콜백 호출 (모달은 아직 열린 상태 유지) if (onAssignmentComplete) { onAssignmentComplete(); } - // 3초 후 자동으로 화면 목록으로 이동 + // 3초 후 자동으로 모달 닫고 화면 목록으로 이동 setTimeout(() => { + onClose(); // 모달 닫기 if (onBackToList) { onBackToList(); - } else { - onClose(); } }, 3000); }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 7ddafd80..9bf3d672 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -79,7 +79,7 @@ const panelConfigs: PanelConfig[] = [ id: "tables", title: "테이블 목록", defaultPosition: "left", - defaultWidth: 380, + defaultWidth: 400, defaultHeight: 700, shortcutKey: "t", }, @@ -87,7 +87,7 @@ const panelConfigs: PanelConfig[] = [ id: "components", title: "컴포넌트", defaultPosition: "left", - defaultWidth: 350, + defaultWidth: 400, defaultHeight: 700, shortcutKey: "c", }, @@ -104,7 +104,7 @@ const panelConfigs: PanelConfig[] = [ id: "styles", title: "스타일", defaultPosition: "left", - defaultWidth: 360, + defaultWidth: 400, defaultHeight: 700, shortcutKey: "s", }, @@ -112,7 +112,7 @@ const panelConfigs: PanelConfig[] = [ id: "resolution", title: "해상도", defaultPosition: "left", - defaultWidth: 300, + defaultWidth: 400, defaultHeight: 700, shortcutKey: "e", }, @@ -129,7 +129,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gap: 16, padding: 0, snapToGrid: true, - showGrid: true, + showGrid: false, // 기본값 false로 변경 gridColor: "#d1d5db", gridOpacity: 0.5, }, @@ -955,7 +955,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gap: 16, padding: 0, // padding은 항상 0으로 강제 snapToGrid: true, - showGrid: true, + showGrid: false, // 기본값 false로 변경 gridColor: "#d1d5db", gridOpacity: 0.5, }, @@ -989,7 +989,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }, [selectedScreen?.screenId]); - // 스페이스바 키 이벤트 처리 (Pan 모드) + // 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 입력 필드에서는 스페이스바 무시 @@ -1001,10 +1001,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단 if (!isPanMode) { setIsPanMode(true); - // 커서 변경 - if (canvasContainerRef.current) { - canvasContainerRef.current.style.cursor = "grab"; - } + // body에 커서 스타일 추가 + document.body.style.cursor = "grab"; } } }; @@ -1014,21 +1012,58 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단 setIsPanMode(false); setPanState((prev) => ({ ...prev, isPanning: false })); - // 커서 복원 - if (canvasContainerRef.current) { - canvasContainerRef.current.style.cursor = "default"; - } + // body 커서 스타일 복원 + document.body.style.cursor = "default"; + } + }; + + const handleMouseDown = (e: MouseEvent) => { + if (isPanMode && canvasContainerRef.current) { + e.preventDefault(); + setPanState({ + isPanning: true, + startX: e.pageX, + startY: e.pageY, + scrollLeft: canvasContainerRef.current.scrollLeft, + scrollTop: canvasContainerRef.current.scrollTop, + }); + // 드래그 중 커서 변경 + document.body.style.cursor = "grabbing"; + } + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isPanMode && panState.isPanning && canvasContainerRef.current) { + e.preventDefault(); + const dx = e.pageX - panState.startX; + const dy = e.pageY - panState.startY; + canvasContainerRef.current.scrollLeft = panState.scrollLeft - dx; + canvasContainerRef.current.scrollTop = panState.scrollTop - dy; + } + }; + + const handleMouseUp = () => { + if (isPanMode) { + setPanState((prev) => ({ ...prev, isPanning: false })); + // 드래그 종료 시 커서 복원 + document.body.style.cursor = "grab"; } }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); + window.addEventListener("mousedown", handleMouseDown); + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); return () => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); + window.removeEventListener("mousedown", handleMouseDown); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); }; - }, [isPanMode]); + }, [isPanMode, panState.isPanning, panState.startX, panState.startY, panState.scrollLeft, panState.scrollTop]); // 마우스 휠로 줌 제어 useEffect(() => { @@ -1206,7 +1241,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 저장 const handleSave = useCallback(async () => { - if (!selectedScreen?.screenId) return; + if (!selectedScreen?.screenId) { + console.error("❌ 저장 실패: selectedScreen 또는 screenId가 없습니다.", selectedScreen); + toast.error("화면 정보가 없습니다."); + return; + } try { setIsSaving(true); @@ -1215,23 +1254,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD ...layout, screenResolution: screenResolution, }; - console.log("💾 저장할 레이아웃 데이터:", { + console.log("💾 저장 시작:", { + screenId: selectedScreen.screenId, componentsCount: layoutWithResolution.components.length, gridSettings: layoutWithResolution.gridSettings, screenResolution: layoutWithResolution.screenResolution, }); + await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); + + console.log("✅ 저장 성공! 메뉴 할당 모달 열기"); toast.success("화면이 저장되었습니다."); // 저장 성공 후 메뉴 할당 모달 열기 setShowMenuAssignmentModal(true); } catch (error) { - // console.error("저장 실패:", error); + console.error("❌ 저장 실패:", error); toast.error("저장 중 오류가 발생했습니다."); } finally { setIsSaving(false); } - }, [selectedScreen?.screenId, layout, screenResolution]); + }, [selectedScreen, layout, screenResolution]); // 템플릿 드래그 처리 const handleTemplateDrop = useCallback( @@ -1861,6 +1904,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD webType: component.webType, category: component.category, defaultConfig: component.defaultConfig, + defaultSize: component.defaultSize, }); // 컴포넌트별 gridColumns 설정 및 크기 계산 @@ -1875,15 +1919,62 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD if (isCardDisplay) { gridColumns = 8; } else if (isTableList) { - gridColumns = 1; + gridColumns = 12; // 테이블은 전체 너비 } else { - // 일반 컴포넌트: defaultSize.width를 기준으로 그리드 컬럼 수 계산 - // 그리드가 활성화된 경우에만 - if (layout.gridSettings?.snapToGrid && gridInfo) { - const columnWidth = gridInfo.columnWidth + gridInfo.gap; - const estimatedColumns = Math.round(component.defaultSize.width / columnWidth); - gridColumns = Math.max(1, Math.min(12, estimatedColumns)); // 1-12 범위로 제한 - } + // 웹타입별 적절한 그리드 컬럼 수 설정 + const webType = component.webType; + const componentId = component.id; + + // 웹타입별 기본 컬럼 수 매핑 + const gridColumnsMap: Record = { + // 입력 컴포넌트 (INPUT 카테고리) + "text-input": 4, // 텍스트 입력 (33%) + "number-input": 2, // 숫자 입력 (16.67%) + "email-input": 4, // 이메일 입력 (33%) + "tel-input": 3, // 전화번호 입력 (25%) + "date-input": 3, // 날짜 입력 (25%) + "datetime-input": 4, // 날짜시간 입력 (33%) + "time-input": 2, // 시간 입력 (16.67%) + "textarea-basic": 6, // 텍스트 영역 (50%) + "select-basic": 3, // 셀렉트 (25%) + "checkbox-basic": 2, // 체크박스 (16.67%) + "radio-basic": 3, // 라디오 (25%) + "file-basic": 4, // 파일 (33%) + + // 표시 컴포넌트 (DISPLAY 카테고리) + "label-basic": 2, // 라벨 (16.67%) + "text-display": 3, // 텍스트 표시 (25%) + "card-display": 8, // 카드 (66.67%) + "badge-basic": 1, // 배지 (8.33%) + "alert-basic": 6, // 알림 (50%) + "divider-basic": 12, // 구분선 (100%) + + // 액션 컴포넌트 (ACTION 카테고리) + "button-basic": 1, // 버튼 (8.33%) + "button-primary": 1, // 프라이머리 버튼 (8.33%) + "button-secondary": 1, // 세컨더리 버튼 (8.33%) + "icon-button": 1, // 아이콘 버튼 (8.33%) + + // 레이아웃 컴포넌트 + "container-basic": 6, // 컨테이너 (50%) + "section-basic": 12, // 섹션 (100%) + "panel-basic": 6, // 패널 (50%) + + // 기타 + "image-basic": 4, // 이미지 (33%) + "icon-basic": 1, // 아이콘 (8.33%) + "progress-bar": 4, // 프로그레스 바 (33%) + "chart-basic": 6, // 차트 (50%) + }; + + // componentId 또는 webType으로 매핑, 없으면 기본값 3 + gridColumns = gridColumnsMap[componentId] || gridColumnsMap[webType] || 3; + + console.log("🎯 컴포넌트 타입별 gridColumns 설정:", { + componentId, + webType, + gridColumns, + }); } // 그리드 시스템이 활성화된 경우 gridColumns에 맞춰 너비 재계산 @@ -1914,6 +2005,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); } + console.log("🎨 최종 컴포넌트 크기:", { + componentId: component.id, + componentName: component.name, + defaultSize: component.defaultSize, + finalSize: componentSize, + gridColumns, + }); + const newComponent: ComponentData = { id: generateComponentId(), type: "component", // ✅ 새 컴포넌트 시스템 사용 @@ -2098,6 +2197,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD return defaultColumns; }; + // 웹타입별 기본 높이 계산 + const getDefaultHeight = (widgetType: string): number => { + const heightMap: Record = { + textarea: 120, // 텍스트 영역은 3줄 (40 * 3) + checkbox: 80, // 체크박스 그룹 (40 * 2) + radio: 80, // 라디오 버튼 (40 * 2) + file: 240, // 파일 업로드 (40 * 6) + }; + + return heightMap[widgetType] || 40; // 기본값 40 + }; + // 웹타입별 기본 설정 생성 const getDefaultWebTypeConfig = (widgetType: string) => { switch (widgetType) { @@ -2282,7 +2393,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD parentId: formContainerId, // 폼 컨테이너의 자식으로 설정 componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입 position: { x: relativeX, y: relativeY, z: 1 } as Position, - size: { width: componentWidth, height: 40 }, + size: { width: componentWidth, height: getDefaultHeight(column.widgetType) }, gridColumns: calculatedGridColumns, // 코드 타입인 경우 코드 카테고리 정보 추가 ...(column.widgetType === "code" && @@ -2345,7 +2456,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD readonly: false, componentType: componentId, // DynamicComponentRenderer용 컴포넌트 타입 position: { x, y, z: 1 } as Position, - size: { width: componentWidth, height: 40 }, + size: { width: componentWidth, height: getDefaultHeight(column.widgetType) }, gridColumns: calculatedGridColumns, // 코드 타입인 경우 코드 카테고리 정보 추가 ...(column.widgetType === "code" && @@ -3088,7 +3199,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gap: 16, padding: 0, snapToGrid: true, - showGrid: true, + showGrid: false, gridColor: "#d1d5db", gridOpacity: 0.5, }, @@ -3649,7 +3760,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {/* 열린 패널들 (좌측에서 우측으로 누적) */} {panelStates.tables?.isOpen && ( -
+

테이블 목록

- +
+ {/* 여백 섹션 */} +
+
+ +

여백

+
+ +
+
+
+ + handleStyleChange("margin", e.target.value)} + /> +
+
+ + handleStyleChange("padding", e.target.value)} + />
- - - - - - - 여백 - - - - 테두리 - - - - 배경 - - - - 텍스트 - - - {/* 여백 탭 */} - -
-
- - handleStyleChange("margin", e.target.value)} - /> -
-
- - handleStyleChange("padding", e.target.value)} - /> -
-
+
+
+ + handleStyleChange("gap", e.target.value)} + /> +
+
+
+
-
-
- - handleStyleChange("gap", e.target.value)} - /> -
-
- + {/* 테두리 섹션 */} +
+
+ +

테두리

+
+ +
+
+
+ + handleStyleChange("borderWidth", e.target.value)} + /> +
+
+ + +
+
- {/* 테두리 탭 */} - -
-
- - handleStyleChange("borderWidth", e.target.value)} - /> -
-
- - -
-
+
+
+ + handleStyleChange("borderColor", e.target.value)} + /> +
+
+ + handleStyleChange("borderRadius", e.target.value)} + /> +
+
+
+
-
-
- - handleStyleChange("borderColor", e.target.value)} - /> -
-
- - handleStyleChange("borderRadius", e.target.value)} - /> -
-
- + {/* 배경 섹션 */} +
+
+ +

배경

+
+ +
+
+ + handleStyleChange("backgroundColor", e.target.value)} + /> +
- {/* 배경 탭 */} - -
- - handleStyleChange("backgroundColor", e.target.value)} - /> -
+
+ + handleStyleChange("backgroundImage", e.target.value)} + /> +
+
+
-
- - handleStyleChange("backgroundImage", e.target.value)} - /> -
- + {/* 텍스트 섹션 */} +
+
+ +

텍스트

+
+ +
+
+
+ + handleStyleChange("color", e.target.value)} + /> +
+
+ + handleStyleChange("fontSize", e.target.value)} + /> +
+
- {/* 텍스트 탭 */} - -
-
- - handleStyleChange("color", e.target.value)} - /> -
-
- - handleStyleChange("fontSize", e.target.value)} - /> -
-
- -
-
- - -
-
- - -
-
-
- - - +
+
+ + +
+
+ + +
+
+
+
); } diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index e486a4bc..ef188fe1 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -227,23 +227,21 @@ export const UnifiedPropertiesPanel: React.FC = ({ )} {/* 크기 */} -
-
- - handleUpdate("width", parseInt(e.target.value) || 0)} - /> -
-
- - handleUpdate("height", parseInt(e.target.value) || 0)} - /> -
+
+ + { + const value = parseInt(e.target.value) || 0; + // 40 단위로 반올림 + const roundedValue = Math.max(40, Math.round(value / 40) * 40); + handleUpdate("size.height", roundedValue); + }} + step={40} + placeholder="40 단위로 입력" + /> +

40 단위로 자동 조정됩니다

{/* 컬럼 스팬 */} diff --git a/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx b/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx index bb6e6a7b..91407caf 100644 --- a/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx +++ b/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Database, Layout, Cog, Settings, Palette, Monitor, Square } from "lucide-react"; +import { Database, Layout, Cog, Settings, Palette, Monitor } from "lucide-react"; import { cn } from "@/lib/utils"; export interface ToolbarButton { @@ -110,14 +110,6 @@ export const defaultToolbarButtons: ToolbarButton[] = [ group: "editor", panelWidth: 300, }, - { - id: "zone", - label: "구역", - icon: , - shortcut: "Z", - group: "editor", - panelWidth: 0, // 토글만 (패널 없음) - }, ]; export default LeftUnifiedToolbar; diff --git a/frontend/hooks/usePanelState.ts b/frontend/hooks/usePanelState.ts index 407a6953..ae550baa 100644 --- a/frontend/hooks/usePanelState.ts +++ b/frontend/hooks/usePanelState.ts @@ -58,27 +58,56 @@ export const usePanelState = (panels: PanelConfig[]) => { }); }, [panels]); - // 패널 토글 + // 패널 토글 (다른 패널 자동 닫기) const togglePanel = useCallback((panelId: string) => { - setPanelStates((prev) => ({ - ...prev, - [panelId]: { - ...prev[panelId], - isOpen: !prev[panelId]?.isOpen, - }, - })); + setPanelStates((prev) => { + const isCurrentlyOpen = prev[panelId]?.isOpen; + const newStates = { ...prev }; + + // 다른 모든 패널 닫기 + Object.keys(newStates).forEach((id) => { + if (id !== panelId) { + newStates[id] = { + ...newStates[id], + isOpen: false, + }; + } + }); + + // 현재 패널 토글 + newStates[panelId] = { + ...newStates[panelId], + isOpen: !isCurrentlyOpen, + }; + + return newStates; + }); }, []); - // 패널 열기 + // 패널 열기 (다른 패널 자동 닫기) const openPanel = useCallback((panelId: string) => { // console.log("📂 패널 열기:", panelId); - setPanelStates((prev) => ({ - ...prev, - [panelId]: { - ...prev[panelId], + setPanelStates((prev) => { + const newStates = { ...prev }; + + // 다른 모든 패널 닫기 + Object.keys(newStates).forEach((id) => { + if (id !== panelId) { + newStates[id] = { + ...newStates[id], + isOpen: false, + }; + } + }); + + // 현재 패널 열기 + newStates[panelId] = { + ...newStates[panelId], isOpen: true, - }, - })); + }; + + return newStates; + }); }, []); // 패널 닫기 diff --git a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx index 076fa8de..cd725849 100644 --- a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx +++ b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx @@ -135,7 +135,7 @@ export const CheckboxBasicComponent: React.FC = ({ checked={checkedValues.includes(option.value)} onChange={(e) => handleGroupChange(option.value, e.target.checked)} disabled={componentConfig.disabled || isDesignMode} - className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" + className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-0 focus:outline-none" /> {option.label} @@ -146,13 +146,7 @@ export const CheckboxBasicComponent: React.FC = ({ // checkbox (기본 체크박스) return ( -
`; - + setRenderedContent(htmlContent); return true; } else if (fileExt === "doc") { @@ -130,7 +124,7 @@ export const FileViewerModal: React.FC = ({

(.docx 파일만 미리보기 지원)

`; - + setRenderedContent(htmlContent); return true; } else if (["ppt", "pptx"].includes(fileExt)) { @@ -142,22 +136,22 @@ export const FileViewerModal: React.FC = ({

파일을 다운로드하여 확인해주세요.

`; - + setRenderedContent(htmlContent); return true; } - + return false; // 지원하지 않는 형식 } catch (error) { console.error("Office 문서 렌더링 오류:", error); - + const htmlContent = `
Office 문서를 읽을 수 없습니다.
파일이 손상되었거나 지원하지 않는 형식일 수 있습니다.
`; - + setRenderedContent(htmlContent); return true; // 오류 메시지라도 표시 } finally { @@ -182,7 +176,7 @@ export const FileViewerModal: React.FC = ({ const url = URL.createObjectURL(file._file); setPreviewUrl(url); setIsLoading(false); - + return () => URL.revokeObjectURL(url); } @@ -192,20 +186,35 @@ export const FileViewerModal: React.FC = ({ const generatePreviewUrl = async () => { try { const fileExt = file.fileExt.toLowerCase(); - + // 미리보기 지원 파일 타입 정의 const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg"]; - const documentExtensions = ["pdf","doc", "docx", "xls", "xlsx", "ppt", "pptx", "rtf", "odt", "ods", "odp", "hwp", "hwpx", "hwpml", "hcdt", "hpt", "pages", "numbers", "keynote"]; + const documentExtensions = [ + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "rtf", + "odt", + "ods", + "odp", + "hwp", + "hwpx", + "hwpml", + "hcdt", + "hpt", + "pages", + "numbers", + "keynote", + ]; const textExtensions = ["txt", "md", "json", "xml", "csv"]; const mediaExtensions = ["mp4", "webm", "ogg", "mp3", "wav"]; - - const supportedExtensions = [ - ...imageExtensions, - ...documentExtensions, - ...textExtensions, - ...mediaExtensions - ]; - + + const supportedExtensions = [...imageExtensions, ...documentExtensions, ...textExtensions, ...mediaExtensions]; + if (supportedExtensions.includes(fileExt)) { // 이미지나 PDF는 인증된 요청으로 Blob 생성 if (imageExtensions.includes(fileExt) || fileExt === "pdf") { @@ -213,15 +222,15 @@ export const FileViewerModal: React.FC = ({ // 인증된 요청으로 파일 데이터 가져오기 const response = await fetch(`${API_BASE_URL}/files/preview/${file.objid}`, { headers: { - "Authorization": `Bearer ${localStorage.getItem("authToken")}`, + Authorization: `Bearer ${localStorage.getItem("authToken")}`, }, }); - + if (response.ok) { const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); setPreviewUrl(blobUrl); - + // 컴포넌트 언마운트 시 URL 정리를 위해 cleanup 함수 저장 cleanup = () => URL.revokeObjectURL(blobUrl); } else { @@ -236,20 +245,20 @@ export const FileViewerModal: React.FC = ({ try { const response = await fetch(`${API_BASE_URL}/files/preview/${file.objid}`, { headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}`, + Authorization: `Bearer ${localStorage.getItem("authToken")}`, }, }); - + if (response.ok) { const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); - + // Office 문서를 위한 특별한 처리 - CDN 라이브러리 사용 if (["doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(fileExt)) { // CDN 라이브러리로 클라이언트 사이드 렌더링 시도 try { const renderSuccess = await renderOfficeDocument(blob, fileExt, file.realFileName); - + if (!renderSuccess) { // 렌더링 실패 시 Blob URL 사용 setPreviewUrl(blobUrl); @@ -263,7 +272,7 @@ export const FileViewerModal: React.FC = ({ // 기타 문서는 직접 Blob URL 사용 setPreviewUrl(blobUrl); } - + return () => URL.revokeObjectURL(blobUrl); // Cleanup function } else { throw new Error(`HTTP ${response.status}`); @@ -291,7 +300,7 @@ export const FileViewerModal: React.FC = ({ }; generatePreviewUrl(); - + // cleanup 함수 반환 return () => { if (cleanup) { @@ -306,24 +315,20 @@ export const FileViewerModal: React.FC = ({ const renderPreview = () => { if (isLoading) { return ( -
-
+
+
); } if (previewError) { return ( -
- -

미리보기 불가

-

{previewError}

-
@@ -335,11 +340,11 @@ export const FileViewerModal: React.FC = ({ // 이미지 파일 if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(fileExt)) { return ( -
+
{file.realFileName} { console.error("이미지 로드 오류:", previewUrl, e); setPreviewError("이미지를 불러올 수 없습니다. 파일이 손상되었거나 서버에서 접근할 수 없습니다."); @@ -358,100 +363,83 @@ export const FileViewerModal: React.FC = ({
+
); } - // Office 문서 (CDN 라이브러리 렌더링 또는 iframe) + // Office 문서 - 모든 Office 문서는 다운로드 권장 if ( - ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "hwp", "hwpx", "hwpml", "hcdt", "hpt", "pages", "numbers", "keynote"].includes(fileExt) + [ + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "hwp", + "hwpx", + "hwpml", + "hcdt", + "hpt", + "pages", + "numbers", + "keynote", + ].includes(fileExt) ) { - // CDN 라이브러리로 렌더링된 콘텐츠가 있는 경우 - if (renderedContent) { - return ( -
-
-
- ); - } - - // iframe 방식 (fallback) + // Office 문서 안내 메시지 표시 return ( -
-