diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx index 200405c9a..c958d5caa 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx @@ -18,6 +18,7 @@ import { buildPipeAnalysisScope, normalizeNumberInput, resolveElapsedYearBucketLabel, + resolveMaterialKoreanName, resolveMaterialName, resolvePipeAnalysisTypeLabel, resolveRegionName, @@ -27,6 +28,19 @@ import { } from "./pipeAnalysisUtils"; type DetailViewRow = ReturnType[number]; +const PIPE_ANALYSIS_DETAIL_EXPORT_KEYS = [ + "rowNo", + "ctpvNm", + "sggNm", + "fcltMngNo", + "instMngNo", + "swtrmtZoneNm", + "instlYmd", + "useNm", + "pdmtVl", + "prlgVl", + "materialName", +] as const; export default function PipeAnalysisDetailPopupPage() { const searchParams = useSearchParams(); @@ -151,11 +165,25 @@ export default function PipeAnalysisDetailPopupPage() { try { setIsLoading(true); const response = await fetchRows(1, filters, Math.max(total, 1000)); + const exportRows = response.rows.map((row) => ({ + rowNo: row.rowNo, + ctpvNm: row.ctpvNm, + sggNm: row.sggNm, + fcltMngNo: row.fcltMngNo, + instMngNo: row.instMngNo, + swtrmtZoneNm: row.swtrmtZoneNm, + instlYmd: row.instlYmd, + useNm: row.useNm, + pdmtVl: row.pdmtVl, + prlgVl: row.prlgVl, + materialName: resolveMaterialKoreanName(row.materialCode) || row.materialName, + })); + const detailHeaderRange = `A1:${String.fromCharCode(64 + PIPE_ANALYSIS_DETAIL_EXPORT_KEYS.length)}1`; await downloadExcelWithKoreanHeaders({ - data: response.rows as unknown as JSON[], + data: exportRows as unknown as JSON[], fileName: `관로분석_관로현황_${buildPipeAnalysisFileDate()}`, sheetNameArray: ["관로현황"], - headerRange: "A1:K1", + headerRange: detailHeaderRange, headerMapping: PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS, }); } catch (error) { diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts index 7eeaa934b..fddb1717c 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipeAnalysisUtils.ts @@ -519,17 +519,69 @@ export const resolveMaterialName = (materialCode?: string) => { return materialMap.get(code) ?? code; }; +/** + * 관재질 라벨에서 영문 약어/코드를 걷어내고 엑셀 헤더용 한글명을 우선 추출한다. + * - `염화비닐관(PVC)` -> `염화비닐관` + * - `PCF관(강판보강 PE 나선형관)` -> `강판보강 나선형관` + */ +const toKoreanMaterialLabel = (value?: string) => { + const normalized = String(value ?? "").trim(); + if (!normalized) return ""; + + const withoutParenthesis = normalized + .replace(/\s*\([^)]*\)\s*/g, " ") + .replace(/\s+/g, " ") + .trim(); + const withoutAsciiToken = withoutParenthesis + .replace(/[A-Za-z0-9-]+/g, " ") + .replace(/\s+/g, " ") + .trim(); + + if (/[가-힣]/.test(withoutAsciiToken)) { + return withoutAsciiToken; + } + + if (/[가-힣]/.test(withoutParenthesis)) { + return withoutParenthesis; + } + + const parenthesisMatches = Array.from(normalized.matchAll(/\(([^)]*)\)/g)) + .map((match) => String(match[1] ?? "").trim()) + .filter(Boolean); + const koreanParenthesisText = parenthesisMatches.find((text) => /[가-힣]/.test(text)); + + if (koreanParenthesisText) { + const simplifiedParenthesisText = koreanParenthesisText + .replace(/[A-Za-z0-9-]+/g, " ") + .replace(/\s+/g, " ") + .trim(); + return simplifiedParenthesisText || koreanParenthesisText; + } + + return normalized; +}; + export const resolveMaterialColumnName = (column: PipeAnalysisMaterialColumn) => { const rawName = String(column.materialName ?? "").trim(); const rawCode = String(column.materialCode ?? "").trim(); const rawKey = String(column.key ?? "").trim(); - if (!rawName) { - return resolveMaterialName(rawCode || rawKey); - } - if (rawName === rawCode || rawName === rawKey) { - return resolveMaterialName(rawCode || rawKey); - } - return rawName; + const mappedName = resolveMaterialName(rawCode || rawKey); + const candidates = [ + toKoreanMaterialLabel(mappedName), + toKoreanMaterialLabel(rawName), + mappedName, + rawName, + rawCode, + rawKey, + ].filter(Boolean); + + return candidates[0] ?? "-"; +}; + +export const resolveMaterialKoreanName = (materialCode?: string) => { + const resolved = resolveMaterialName(materialCode); + const koreanLabel = toKoreanMaterialLabel(resolved); + return koreanLabel || resolved; }; export const resolveElapsedYearBucketLabel = (bucket?: string) => {