From 474ad1ab260007f0c9e8a82252bbd58e0ea05dd1 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Wed, 12 Aug 2026 11:13:49 +0900 Subject: [PATCH] =?UTF-8?q?GIS=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gisManagement/GisManagementPopupPage.tsx | 102 ++++++++++-- .../gisManagement/GisUploadPreviewMap.tsx | 151 ++++++------------ .../windowPopup/lib/openWindowPopup.ts | 7 +- .../windowPopup/model/popupDefinitions.ts | 1 + .../src/features/windowPopup/model/types.ts | 1 + 5 files changed, 140 insertions(+), 122 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx index 5f24e9516..68c4de1e2 100644 --- a/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx @@ -18,9 +18,11 @@ import { getBlobDataWithHeaders, getData, postMutiPart, + postRawJson, } from "@/components/lib/api"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types"; +import { useUserStore } from "@/shared/lib/use-user-store"; import GisUploadPreviewMap from "./GisUploadPreviewMap"; type UploadFacilityType = { @@ -49,6 +51,7 @@ type UploadHistoryItem = { invalidCount?: number | null; registeredAt: string; registeredBy: string; + registeredByName?: string | null; message?: string | null; }; @@ -70,6 +73,16 @@ type ValidationErrorItem = { type ViewMode = "history" | "validationErrors" | "preview"; +type DownloadJob = { + jobId: string; + status: "QUEUED" | "RUNNING" | "READY" | "FAILED" | string; + message?: string | null; + errorCode?: string | null; + downloadId?: string | null; + fileName?: string | null; + retryAfterMillis?: number | null; +}; + const SCREEN_ID = "SN_MN010302"; const FACILITY_TYPE_CODE = "PPL"; const LAYER_ID = "tb_ms_swage_ppl"; @@ -77,6 +90,8 @@ const GIS_UPLOAD_PAGE_SIZE = 10; const SELECT_MNG_INST_VALUE = ""; // A current/history download is an idempotent GET, so one retry is safe after a transient connection drop. const DOWNLOAD_NETWORK_RETRY_DELAY_MS = 500; +const DOWNLOAD_JOB_POLL_INTERVAL_MS = 1_000; +const DOWNLOAD_JOB_MAX_WAIT_MS = 30 * 60 * 1_000; const DEFAULT_FACILITY: UploadFacilityType = { facilityTypeCode: FACILITY_TYPE_CODE, facilityTypeName: "하수관로", @@ -123,6 +138,35 @@ const downloadGeoPackage = async (url: string) => { } }; +// 파일 생성 요청과 상태 조회를 짧은 HTTP 요청으로 분리해 서버의 10초 요청 제한을 넘지 않는다. +const startDownloadJob = async (url: string): Promise => { + const response = await postRawJson(url, {}); + const job = (response?.data ?? response) as DownloadJob | null; + if (!job?.jobId) { + throw new Error("다운로드 작업 접수 정보를 확인하지 못했습니다."); + } + return job; +}; + +const waitForDownloadJob = async (jobId: string): Promise => { + const startedAt = Date.now(); + while (Date.now() - startedAt < DOWNLOAD_JOB_MAX_WAIT_MS) { + const job = (await getData( + `v1/map/gisDownloadJobs/${encodeURIComponent(jobId)}`, + )) as DownloadJob; + if (job?.status === "READY") return job; + if (job?.status === "FAILED") { + throw new Error(job.message || "GeoPackage 다운로드 파일 생성에 실패했습니다."); + } + const retryAfter = Math.max( + Number(job?.retryAfterMillis) || DOWNLOAD_JOB_POLL_INTERVAL_MS, + DOWNLOAD_JOB_POLL_INTERVAL_MS, + ); + await new Promise((resolve) => window.setTimeout(resolve, retryAfter)); + } + throw new Error("GeoPackage 파일 생성 대기 시간이 초과되었습니다. 다시 시도하세요."); +}; + // Keep the object URL alive until Chrome has accepted the synthetic download click. const saveBlob = (blob: Blob, fileName: string) => { const url = window.URL.createObjectURL(blob); @@ -161,6 +205,7 @@ const postRefreshMessageToOpener = (response: GisUploadResponse) => { }; export default function GisManagementPopupPage() { + const userInfo = useUserStore((state) => state.userInfo); // 파일 input은 브라우저 보안 정책상 value 직접 제어가 제한돼 ref로 선택/초기화한다. const fileInputRef = useRef(null); @@ -355,22 +400,30 @@ export default function GisManagementPopupPage() { setIsLoading(true); setNotice(""); try { - const response = await downloadGeoPackage( - `v1/map/gisUploads/swagePpl/download?${params.toString()}`, + const job = await startDownloadJob( + `v1/map/gisUploads/swagePpl/downloadJobs?${params.toString()}`, ); - const downloadId = response.headers.get("X-Gis-Download-Id") ?? ""; + setNotice(`${selectedScopeLabel} GeoPackage 파일을 생성하고 있습니다.`); + const readyJob = await waitForDownloadJob(job.jobId); + const response = await downloadGeoPackage( + `v1/map/gisDownloadJobs/${encodeURIComponent(job.jobId)}/file`, + ); + const downloadId = response.headers.get("X-Gis-Download-Id") ?? readyJob.downloadId ?? ""; if (!downloadId) { setNotice("다운로드 기준 정보를 확인하지 못했습니다. 다시 다운로드한 뒤 업로드하세요."); } else { setDownloadIdsByScope((current) => ({ ...current, [selectedScopeKey]: downloadId })); } - saveBlob(response.blob, response.fileName ?? `swage_ppl_${selectedScopeKey}.gpkg`); + saveBlob( + response.blob, + response.fileName ?? readyJob.fileName ?? `swage_ppl_${selectedScopeKey}.gpkg`, + ); if (downloadId) { setNotice(`${selectedScopeLabel} 기준 GeoPackage를 다운로드했습니다.`); } } catch (error) { console.error("GIS 다운로드 실패:", error); - window.alert("GeoPackage 다운로드 중 오류가 발생했습니다."); + window.alert(getErrorMessage(error, "GeoPackage 다운로드 중 오류가 발생했습니다.")); } finally { setIsLoading(false); } @@ -423,6 +476,7 @@ export default function GisManagementPopupPage() { }; const handleDownloadSelectedHistory = async () => { + if (viewMode !== "history") return; if (!selectedHistory) { window.alert("다운로드할 업로드 현황 행을 선택하세요."); return; @@ -431,14 +485,22 @@ export default function GisManagementPopupPage() { setIsLoading(true); setNotice(""); try { - const response = await downloadGeoPackage( - `v1/map/gisUploads/${encodeURIComponent(selectedHistory.uploadId)}/download`, + const job = await startDownloadJob( + `v1/map/gisUploads/${encodeURIComponent(selectedHistory.uploadId)}/downloadJobs`, + ); + setNotice("선택한 현황의 GeoPackage 파일을 생성하고 있습니다."); + const readyJob = await waitForDownloadJob(job.jobId); + const response = await downloadGeoPackage( + `v1/map/gisDownloadJobs/${encodeURIComponent(job.jobId)}/file`, + ); + saveBlob( + response.blob, + response.fileName ?? readyJob.fileName ?? `${selectedHistory.uploadId}.gpkg`, ); - saveBlob(response.blob, response.fileName ?? `${selectedHistory.uploadId}.gpkg`); setNotice(`${buildManagementLabel(selectedHistory.mngInstCd, selectedHistory.mngInstNm)} 기준 GeoPackage를 다운로드했습니다.`); } catch (error) { console.error("GIS 이력 다운로드 실패:", error); - window.alert("선택한 현황 기준 GeoPackage 다운로드 중 오류가 발생했습니다."); + window.alert(getErrorMessage(error, "선택한 현황 기준 GeoPackage 다운로드 중 오류가 발생했습니다.")); } finally { setIsLoading(false); } @@ -493,8 +555,14 @@ export default function GisManagementPopupPage() { ) : null} -
-
+
+