diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx index 3626054c7..d7fe9dc1f 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx @@ -3,13 +3,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { Button, Input } from "@common/components"; -import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui"; +import { UiTable } from "@common/business_components/ui"; import { getData } from "@/components/lib/api"; import useStore from "@/store/useStore"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { AGED_PIPE_DEFAULT_FILTERS, - AGED_PIPE_EXCEL_HEADER, AGED_PIPE_LAYER_TYPE, AGED_PIPE_LIST_HEADLIST, AGED_PIPE_PAGE_SIZE, @@ -20,6 +19,7 @@ import { type AgedPipeFilters, type AgedPipeRow, } from "./agedPipeUtils"; +import { downloadAgedPipeCsv } from "./agedPipeExport"; const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; @@ -40,6 +40,7 @@ export default function AgedPipePopupPage() { const [currentPage, setCurrentPage] = useState(1); const [selectedIndex, setSelectedIndex] = useState(-1); const [isLoading, setIsLoading] = useState(false); + const [downloadProgressText, setDownloadProgressText] = useState(""); const sggOptions = useMemo( () => @@ -156,17 +157,28 @@ export default function AgedPipePopupPage() { try { setIsLoading(true); - const response = await fetchRows(1, filters, 10000); - await downloadExcelWithKoreanHeaders({ - data: response.rows as JSON[], - fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`, - sheetNameArray: ["노후관로 현황"], - headerRange: "A1:J1", - headerMapping: AGED_PIPE_EXCEL_HEADER, + setDownloadProgressText("CSV export 작업을 준비하고 있습니다."); + await downloadAgedPipeCsv(filters, (job) => { + if (job.status === "QUEUED") { + setDownloadProgressText("CSV export 작업이 대기 중입니다."); + return; + } + + if (job.status === "RUNNING") { + setDownloadProgressText( + `CSV export 생성 중입니다. (${job.progressPercent ?? 0}%)`, + ); + } }); + setDownloadProgressText(""); } catch (error) { console.error("노후관로 목록 다운로드 중 오류:", error); - alert("노후관로 목록 다운로드 중 오류가 발생했습니다."); + setDownloadProgressText(""); + alert( + error instanceof Error + ? error.message + : "노후관로 CSV 다운로드 중 오류가 발생했습니다.", + ); } finally { setIsLoading(false); } @@ -202,6 +214,12 @@ export default function AgedPipePopupPage() { + {downloadProgressText ? ( +
+ {downloadProgressText} +
+ ) : null} +
) : null} + {agedPipeDownloadProgressText ? ( +
+ {agedPipeDownloadProgressText} +
+ ) : null} => { } }; +const parseDownloadFileName = (contentDisposition: string | null) => { + if (!contentDisposition) return undefined; + + const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1].trim()); + } + + const plainMatch = contentDisposition.match(/filename\s*=\s*"?([^\";]+)"?/i); + return plainMatch?.[1]?.trim(); +}; + +export const postBlobData = async ( + url: string, + data: object, +): Promise<{ blob: Blob; fileName?: string }> => { + const res = await fetch(resolveApiUrl(url), { + method: "POST", + headers: withSiteIdHeaders({ + "Content-Type": "application/json", + }), + body: JSON.stringify(data), + }); + if (res.status === 401) { + handleUnauthorizedResponse(); + } + + if (!res.ok) { + const errorText = await res.text().catch(() => ""); + throw new Error( + `Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`, + ); + } + + return { + blob: await res.blob(), + fileName: parseDownloadFileName(res.headers.get("content-disposition")), + }; +}; + export const postMutiPart = async (url: string, data: FormData): Promise => { const res = await axios.post(`${BASE_URL}/${url}`, data, { headers: DEFAULT_HEADERS,