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 ? (
+
) : 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,