diff --git a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx index b7686f4b0..902be2916 100644 --- a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx +++ b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx @@ -16,10 +16,11 @@ import { import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions"; type Props = { - onOpenPopup: (popupKey: WindowPopupKey) => void; + onOpenPopup: (popupKey: WindowPopupKey) => Window | null; onOpenInfoSearch: () => void; onToggleGisEdit?: () => void; isGisEditActive?: boolean; + isInfoSearchActive?: boolean; }; const ANALYSIS_POPUP_ITEMS: Array<{ @@ -41,6 +42,7 @@ export default function AnalysisPopupMenu({ onOpenInfoSearch, onToggleGisEdit, isGisEditActive = false, + isInfoSearchActive = false, }: Props) { const [isOpen, setIsOpen] = useState(true); const [activeKey, setActiveKey] = useState(null); @@ -52,6 +54,12 @@ export default function AnalysisPopupMenu({ } }, [activeKey, isGisEditActive]); + useEffect(() => { + if (!isInfoSearchActive && activeKey === "infoSearch") { + setActiveKey(null); + } + }, [activeKey, isInfoSearchActive]); + return (
@@ -94,7 +102,19 @@ export default function AnalysisPopupMenu({ return; } - onOpenPopup(item.key); + const popupWindow = onOpenPopup(item.key); + if (!popupWindow) { + setActiveKey(null); + return; + } + + const closeWatcher = window.setInterval(() => { + if (!popupWindow.closed) return; + window.clearInterval(closeWatcher); + setActiveKey((currentKey) => + currentKey === item.key ? null : currentKey, + ); + }, 300); }} > { + const lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex < 0) return ""; + return fileName.slice(lastDotIndex).toLowerCase(); +}; + +const buildTodayDate = () => new Date().toISOString().slice(0, 10); +const GIS_UPLOAD_PAGE_SIZE = 10; + +export default function GisManagementPopupPage() { + // 브라우저 파일 picker는 제어형 input보다 ref 제어가 안전하다. + const fileInputRef = useRef(null); + + const [selectedFacilityTypeCode, setSelectedFacilityTypeCode] = useState(""); + const [selectedFile, setSelectedFile] = useState(null); + const [uploadHistory, setUploadHistory] = useState( + INITIAL_UPLOAD_HISTORY, + ); + const [selectedUploadId, setSelectedUploadId] = useState( + INITIAL_UPLOAD_HISTORY[0]?.uploadId ?? "", + ); + const [currentPage, setCurrentPage] = useState(1); + + const selectedFacility = useMemo( + () => + UPLOAD_FACILITY_TYPES.find( + (facilityType) => + facilityType.facilityTypeCode === selectedFacilityTypeCode, + ) ?? null, + [selectedFacilityTypeCode], + ); + + const selectedHistory = useMemo( + () => + uploadHistory.find((history) => history.uploadId === selectedUploadId) ?? + null, + [selectedUploadId, uploadHistory], + ); + + const pagedUploadHistory = useMemo(() => { + const startIndex = (currentPage - 1) * GIS_UPLOAD_PAGE_SIZE; + return uploadHistory.slice(startIndex, startIndex + GIS_UPLOAD_PAGE_SIZE); + }, [currentPage, uploadHistory]); + + const resetSelectedFile = () => { + setSelectedFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleFacilityTypeChange = ( + event: ChangeEvent, + ) => { + setSelectedFacilityTypeCode(event.target.value); + resetSelectedFile(); + }; + + const handleOpenFilePicker = () => { + if (!selectedFacility) { + window.alert("시설구분을 먼저 선택하세요."); + return; + } + + fileInputRef.current?.click(); + }; + + const handleFileSelected = (event: ChangeEvent) => { + const nextFile = event.target.files?.[0] ?? null; + + if (!selectedFacility) { + resetSelectedFile(); + window.alert("시설구분을 먼저 선택한 뒤 파일을 선택하세요."); + return; + } + + if (!nextFile) { + resetSelectedFile(); + return; + } + + const fileExtension = getFileExtension(nextFile.name); + const isAllowedExtension = selectedFacility.allowedExtensions.includes( + fileExtension, + ); + + if (!isAllowedExtension) { + resetSelectedFile(); + window.alert( + `업로드 가능한 GIS 파일 형식이 아닙니다. 허용 형식: ${selectedFacility.allowedExtensions.join(", ")}`, + ); + return; + } + + const maxFileSizeInBytes = selectedFacility.maxFileSizeMb * 1024 * 1024; + if (nextFile.size > maxFileSizeInBytes) { + resetSelectedFile(); + window.alert( + `파일 크기가 제한을 초과했습니다. 최대 ${selectedFacility.maxFileSizeMb}MB까지 업로드할 수 있습니다.`, + ); + return; + } + + setSelectedFile(nextFile); + }; + + const handleUpload = () => { + if (!selectedFacility) { + window.alert("시설구분을 선택하세요."); + return; + } + + if (!selectedFile) { + window.alert("업로드할 파일을 선택하세요."); + return; + } + + const isDuplicateFile = uploadHistory.some( + (history) => + history.facilityTypeCode === selectedFacility.facilityTypeCode && + history.fileName.toLowerCase() === selectedFile.name.toLowerCase(), + ); + + if (isDuplicateFile) { + window.alert("동일 시설구분에 같은 파일명이 존재합니다."); + return; + } + + const nextUploadHistory: UploadHistoryItem = { + uploadId: `GUP-MOCK-${Date.now()}`, + facilityTypeCode: selectedFacility.facilityTypeCode, + facilityTypeName: selectedFacility.facilityTypeName, + fileName: selectedFile.name, + registeredAt: buildTodayDate(), + registeredBy: "admin", + }; + + setUploadHistory((currentHistory) => [ + nextUploadHistory, + ...currentHistory, + ]); + setCurrentPage(1); + setSelectedUploadId(nextUploadHistory.uploadId); + window.alert("GIS 업로드가 완료되었습니다."); + resetSelectedFile(); + }; + + const handleDownload = () => { + if (!selectedHistory) { + window.alert("다운로드할 파일을 선택하세요."); + return; + } + + window.alert(`${selectedHistory.fileName} 다운로드를 요청했습니다.`); + }; + + return ( +
+
+
+

+ GIS 관리 +

+
+ +
+
+ GIS 업로드 +
+ +
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ GIS 업로드 현황 +
+ + +
+ +
+ + + + + no + + + 시설구분 + + + 파일명 + + + 등록일자 + + + 등록자 + + + + + + {pagedUploadHistory.map((history, index) => { + const isSelected = history.uploadId === selectedUploadId; + const backgroundColor = isSelected + ? "#eef6ff" + : index % 2 === 0 + ? "#ffffff" + : "#f9fbfd"; + const rowNumber = + (currentPage - 1) * GIS_UPLOAD_PAGE_SIZE + index + 1; + + return ( + setSelectedUploadId(history.uploadId)} + style={{ backgroundColor }} + > + + {rowNumber} + + + {history.facilityTypeName} + + + {history.fileName} + + + {history.registeredAt} + + + {history.registeredBy} + + + ); + })} + +
+
+ + {uploadHistory.length > GIS_UPLOAD_PAGE_SIZE ? ( +
+ +
+ ) : null} +
+
+
+ ); +} diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 2a2b6f0a0..6d6944f20 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -2102,12 +2102,14 @@ const Map = () => { const popup = openWindowPopup(popupKey); if (!popup) { alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요."); - return; + return null; } if (popupKey === "pipeAnalysisPopup") { pipeAnalysisPopupWindowRef.current = popup; } + + return popup; }, []); const openGisEditSessionPopup = useCallback(() => { @@ -2721,6 +2723,7 @@ const Map = () => { onOpenInfoSearch={() => setIsSearchModalOpen(true)} onToggleGisEdit={handleToggleGisEdit} isGisEditActive={gisEdit.isEditMode} + isInfoSearchActive={isSearchModalOpen} /> ( ); -const GisManagementPopupContent = (_props: WindowPopupContentProps) => ( - -); - const FlowRiskPopupContent = (_props: WindowPopupContentProps) => ( ); diff --git a/apps/sn/src/features/windowPopup/ui/GisManagementPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/GisManagementPopupContent.tsx new file mode 100644 index 000000000..9e2dce7d7 --- /dev/null +++ b/apps/sn/src/features/windowPopup/ui/GisManagementPopupContent.tsx @@ -0,0 +1,10 @@ +import { Suspense } from "react"; +import GisManagementPopupPage from "@/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage"; + +export default function GisManagementPopupContent() { + return ( + + + + ); +}