"use client"; import type { SelectDataType } from "@common/business_components"; import type { IUserInfo } from "@/shared/store"; import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES, } from "../common/table"; export const PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE = "SN_PIPE_ANALYSIS_DETAIL_ROW_SELECT"; export type PipeAnalysisType = "YEAR" | "MATERIAL" | "YEAR_MATERIAL"; export type PipeAnalysisSummaryFilters = { analysisType: PipeAnalysisType; ctpvCd: string; sggCd: string; swtrmtZoneNm: string; scopeRegionCode: string; }; export type PipeAnalysisDetailFilters = { ctpvCd: string; sggCd: string; swtrmtZoneNm: string; useCd: string; materialCode: string; diameterFrom: string; diameterTo: string; }; export type PipeAnalysisMaterialColumn = { key: string; materialCode: string; materialName: string; }; export type PipeAnalysisSummaryRow = { rowNo: number; ctpvCd: string; ctpvNm?: string; sggCd: string; sggNm?: string; swtrmtZoneNm: string; totalLengthKm: number; elapsedYearBucket?: string; elapsedYearLabel?: string; metrics: Record; }; export type PipeAnalysisSummaryResponse = { analysisType: PipeAnalysisType; materialColumns: PipeAnalysisMaterialColumn[]; data: PipeAnalysisSummaryRow[]; }; export type PipeAnalysisDetailRow = { rowNo: number; fid: string; lyrTypeNm: string; fcltMngNo: string; instMngNo: string; mngInstCd: string; ctpvCd: string; ctpvNm?: string; sggCd: string; sggNm?: string; swtrmtZoneNm: string; instlYmd: string; elapsedYears?: number; useCd: string; materialCode: string; pdmtVl?: number | string; prlgVl?: number | string; }; export type PipeAnalysisZoneOption = { value: string; label: string; ctpvCd: string; sggCd: string; }; export type PipeAnalysisScope = { regionCode: string; ctpvCd: string; sggCd: string; ctpvLocked: boolean; sggLocked: boolean; }; export type PipeAnalysisCellFilter = { elapsedYearBucket?: string; materialCode?: string; }; export type PipeAnalysisSummaryViewRow = Record & { __isNoZone: boolean; __isSummaryTotal: boolean; __cellFilters: Record; __base: { ctpvCd: string; sggCd: string; swtrmtZoneNm: string; }; }; export type PipeAnalysisYearMaterialGroupRow = { elapsedYearBucket: string; elapsedYearLabel: string; metrics: Record; cellFilters: Record; }; export type PipeAnalysisYearMaterialGroup = { rowNo: number; ctpvNm: string; sggNm: string; swtrmtZoneNm: string; totalLengthKm: number; base: { ctpvCd: string; sggCd: string; swtrmtZoneNm: string; }; rows: PipeAnalysisYearMaterialGroupRow[]; }; const materialMap = new Map(MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text])); const useMap = new Map(MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text])); export const PIPE_ANALYSIS_TYPE_OPTIONS: Array<{ value: PipeAnalysisType; label: string }> = [ { value: "YEAR", label: "경과년도별" }, { value: "MATERIAL", label: "재질별" }, { value: "YEAR_MATERIAL", label: "경과년도&재질" }, ]; export const PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS: PipeAnalysisSummaryFilters = { analysisType: "YEAR", ctpvCd: "0", sggCd: "0", swtrmtZoneNm: "0", scopeRegionCode: "", }; export const PIPE_ANALYSIS_DETAIL_DEFAULT_FILTERS: PipeAnalysisDetailFilters = { ctpvCd: "0", sggCd: "0", swtrmtZoneNm: "0", useCd: "0", materialCode: "0", diameterFrom: "", diameterTo: "", }; export const PIPE_ANALYSIS_FIXED_YEAR_COLUMNS = [ { key: "LT10", title: "10년 미만(km)" }, { key: "Y10_20", title: "10년~20년(km)" }, { key: "Y20_30", title: "20년~30년(km)" }, { key: "GE30", title: "30년 이상(km)" }, { key: "UNKNOWN", title: "확인 불가(km)" }, ]; const elapsedYearBucketLabelMap: Record = { LT10: "10년 미만", Y10_20: "10년~20년", Y20_30: "20년~30년", GE30: "30년 이상", UNKNOWN: "확인 불가", }; const YEAR_MATERIAL_BASE_BUCKETS = ["LT10", "Y10_20", "Y20_30", "GE30"]; export const PIPE_ANALYSIS_DETAIL_HEADLIST = [ { id: "rowNo", title: "NO", widthPercent: 6 }, { id: "ctpvNm", title: "시도", widthPercent: 9 }, { id: "sggNm", title: "시군구", widthPercent: 10 }, { id: "fcltMngNo", title: "시설관리번호", widthPercent: 15 }, { id: "instMngNo", title: "기관관리번호", widthPercent: 15 }, { id: "swtrmtZoneNm", title: "처리구역", widthPercent: 12 }, { id: "instlYmd", title: "설치일자", widthPercent: 10 }, { id: "useNm", title: "용도", widthPercent: 9 }, { id: "pdmtVl", title: "관경(mm)", widthPercent: 8, align: "right" }, { id: "prlgVl", title: "연장(m)", widthPercent: 8, align: "right" }, { id: "materialName", title: "관재질", widthPercent: 12 }, ]; export const buildPipeAnalysisScope = (userInfo: IUserInfo | null): PipeAnalysisScope => { const admin = userInfo?.adminRoleYn === "Y"; if (admin) { return { regionCode: "", ctpvCd: "", sggCd: "", ctpvLocked: false, sggLocked: false, }; } const digits = String(userInfo?.laeSeCd ?? "").replace(/[^0-9]/g, ""); if (digits.length >= 5) { return { regionCode: digits.slice(0, 5), ctpvCd: digits.slice(0, 2), sggCd: digits.slice(0, 5), ctpvLocked: true, sggLocked: true, }; } if (digits.length >= 2) { return { regionCode: digits.slice(0, 2), ctpvCd: digits.slice(0, 2), sggCd: "", ctpvLocked: true, sggLocked: false, }; } return { regionCode: "", ctpvCd: "", sggCd: "", ctpvLocked: false, sggLocked: false, }; }; export const applyScopeToSummaryFilters = ( filters: PipeAnalysisSummaryFilters, scope: PipeAnalysisScope, ): PipeAnalysisSummaryFilters => { const next = { ...filters, scopeRegionCode: scope.regionCode }; if (scope.ctpvCd) { next.ctpvCd = scope.ctpvCd; } if (scope.sggCd) { next.sggCd = scope.sggCd; } else if (scope.ctpvCd && !next.sggCd.startsWith(scope.ctpvCd)) { next.sggCd = "0"; } return next; }; export const applyScopeToDetailFilters = ( filters: PipeAnalysisDetailFilters, scope: PipeAnalysisScope, ): PipeAnalysisDetailFilters => { const next = { ...filters }; if (scope.ctpvCd) { next.ctpvCd = scope.ctpvCd; } if (scope.sggCd) { next.sggCd = scope.sggCd; } else if (scope.ctpvCd && !next.sggCd.startsWith(scope.ctpvCd)) { next.sggCd = "0"; } return next; }; export const buildPipeAnalysisSummaryHeadList = ( analysisType: PipeAnalysisType, materialColumns: PipeAnalysisMaterialColumn[], ) => { const common = [ { id: "rowNo", title: "NO", widthPercent: 6 }, { id: "ctpvNm", title: "시도", widthPercent: 10 }, { id: "sggNm", title: "시군구", widthPercent: 10 }, { id: "swtrmtZoneNm", title: "처리구역", widthPercent: 14 }, { id: "totalLengthKm", title: "합계(km)", widthPercent: 10, align: "right" }, ]; if (analysisType === "YEAR") { return [ ...common, ...PIPE_ANALYSIS_FIXED_YEAR_COLUMNS.map((column) => ({ id: column.key, title: column.title, widthPercent: 12, align: "right" as const, })), ]; } const materialHeads = materialColumns.map((column) => ({ id: column.key, title: resolveMaterialColumnName(column), widthPercent: 12, align: "right" as const, })); if (analysisType === "YEAR_MATERIAL") { return [ ...common, { id: "elapsedYearLabel", title: "경과년도", widthPercent: 12 }, ...materialHeads, ]; } return [...common, ...materialHeads]; }; export const buildPipeAnalysisSummaryViewRows = ( response: PipeAnalysisSummaryResponse, ctpvOptions: SelectDataType[], sggOptions: SelectDataType[], ): PipeAnalysisSummaryViewRow[] => { const materialCodes = response.materialColumns.map((column) => column.key); return response.data.map((row) => { const isNoZone = String(row.swtrmtZoneNm ?? "").trim() === "NO_ZONE"; const isSummaryTotal = !String(row.ctpvCd ?? "").trim() && String(row.swtrmtZoneNm ?? "").trim() === "-"; const viewRow: PipeAnalysisSummaryViewRow = { rowNo: row.rowNo, ctpvNm: isNoZone ? "전체" : row.ctpvNm || resolveRegionName(row.ctpvCd, ctpvOptions, 2), sggNm: isNoZone ? "-" : row.sggNm || resolveRegionName(row.sggCd, sggOptions, 5), swtrmtZoneNm: isNoZone ? "-" : row.swtrmtZoneNm || "-", totalLengthKm: row.totalLengthKm ?? 0, elapsedYearBucket: row.elapsedYearBucket ?? "", elapsedYearLabel: row.elapsedYearLabel ?? "", __isNoZone: isNoZone, __isSummaryTotal: isSummaryTotal, __cellFilters: {}, __base: { ctpvCd: isNoZone ? "" : row.ctpvCd, sggCd: isNoZone ? "" : row.sggCd, swtrmtZoneNm: isNoZone ? "NO_ZONE" : row.swtrmtZoneNm, }, }; if (isSummaryTotal) { viewRow.ctpvNm = row.ctpvNm || viewRow.ctpvNm; viewRow.sggNm = row.sggNm || "-"; viewRow.swtrmtZoneNm = "-"; viewRow.__base.ctpvCd = ""; viewRow.__base.sggCd = ""; viewRow.__base.swtrmtZoneNm = ""; } if (response.analysisType === "YEAR") { if (isNoZone || isSummaryTotal) { PIPE_ANALYSIS_FIXED_YEAR_COLUMNS.forEach((column) => { viewRow[column.key] = Number(row.metrics?.[column.key] ?? 0); }); return viewRow; } PIPE_ANALYSIS_FIXED_YEAR_COLUMNS.forEach((column) => { const value = Number(row.metrics?.[column.key] ?? 0); viewRow[column.key] = value; if (value > 0) { viewRow.__cellFilters[column.key] = { elapsedYearBucket: column.key, }; } }); return viewRow; } const columns = response.analysisType === "YEAR_MATERIAL" ? materialCodes : materialCodes; columns.forEach((columnKey) => { const value = Number(row.metrics?.[columnKey] ?? 0); viewRow[columnKey] = value; if (!isNoZone && !isSummaryTotal && value > 0) { viewRow.__cellFilters[columnKey] = { elapsedYearBucket: response.analysisType === "YEAR_MATERIAL" ? row.elapsedYearBucket : undefined, materialCode: columnKey === "OTHER" ? "" : columnKey, }; } }); return viewRow; }); }; export const buildPipeAnalysisYearMaterialGroups = ( rows: PipeAnalysisSummaryViewRow[], materialColumns: PipeAnalysisMaterialColumn[], ): PipeAnalysisYearMaterialGroup[] => { const materialKeys = materialColumns.map((column) => column.key); const groupMap = new Map(); rows.forEach((row) => { if (row.__isNoZone || row.__isSummaryTotal) { return; } const groupKey = `${row.__base.ctpvCd}|${row.__base.sggCd}|${row.__base.swtrmtZoneNm}`; const elapsedYearBucket = String(row.elapsedYearBucket ?? "").trim(); let group = groupMap.get(groupKey); if (!group) { group = { rowNo: groupMap.size + 1, ctpvNm: String(row.ctpvNm ?? ""), sggNm: String(row.sggNm ?? ""), swtrmtZoneNm: String(row.swtrmtZoneNm ?? ""), totalLengthKm: Number(row.totalLengthKm ?? 0), base: { ctpvCd: row.__base.ctpvCd, sggCd: row.__base.sggCd, swtrmtZoneNm: row.__base.swtrmtZoneNm, }, rows: [], }; groupMap.set(groupKey, group); } const metrics: Record = {}; materialKeys.forEach((materialKey) => { metrics[materialKey] = Number(row[materialKey] ?? 0); }); group.rows.push({ elapsedYearBucket, elapsedYearLabel: String(row.elapsedYearLabel ?? resolveElapsedYearBucketLabel(elapsedYearBucket)), metrics, cellFilters: row.__cellFilters, }); }); return Array.from(groupMap.values()).map((group) => { const rowByBucket = new Map(group.rows.map((row) => [row.elapsedYearBucket, row])); const orderedBuckets = [...YEAR_MATERIAL_BASE_BUCKETS]; if (rowByBucket.has("UNKNOWN")) { orderedBuckets.push("UNKNOWN"); } const normalizedRows = orderedBuckets.map((bucket) => { const existing = rowByBucket.get(bucket); if (existing) { return existing; } const emptyMetrics: Record = {}; materialKeys.forEach((materialKey) => { emptyMetrics[materialKey] = 0; }); return { elapsedYearBucket: bucket, elapsedYearLabel: resolveElapsedYearBucketLabel(bucket), metrics: emptyMetrics, cellFilters: {}, }; }); return { ...group, rows: normalizedRows, }; }); }; export const toPipeAnalysisDetailViewRows = ( rows: PipeAnalysisDetailRow[], ctpvOptions: SelectDataType[], sggOptions: SelectDataType[], ) => rows.map((row) => ({ ...row, ctpvNm: row.ctpvNm || resolveRegionName(row.ctpvCd, ctpvOptions, 2), sggNm: row.sggNm || resolveRegionName(row.sggCd, sggOptions, 5), useNm: resolveUseName(row.useCd), materialName: resolveMaterialName(row.materialCode), })); export const buildPipeAnalysisSummaryDownloadHeaders = ( analysisType: PipeAnalysisType, materialColumns: PipeAnalysisMaterialColumn[], ) => { const headers: Record = { rowNo: "NO", ctpvNm: "시도", sggNm: "시군구", swtrmtZoneNm: "처리구역", totalLengthKm: "합계(km)", }; if (analysisType === "YEAR_MATERIAL") { headers.elapsedYearLabel = "경과년도"; } if (analysisType === "YEAR") { PIPE_ANALYSIS_FIXED_YEAR_COLUMNS.forEach((column) => { headers[column.key] = column.title; }); return headers; } materialColumns.forEach((column) => { headers[column.key] = resolveMaterialColumnName(column); }); return headers; }; export const PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS = { rowNo: "NO", ctpvNm: "시도", sggNm: "시군구", fcltMngNo: "시설관리번호", instMngNo: "기관관리번호", swtrmtZoneNm: "처리구역", instlYmd: "설치일자", useNm: "용도", pdmtVl: "관경(mm)", prlgVl: "연장(m)", materialName: "관재질", }; export const resolveMaterialName = (materialCode?: string) => { const code = String(materialCode ?? "").trim(); if (!code) return "-"; if (code === "OTHER") return "기타"; return materialMap.get(code) ?? code; }; 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; }; export const resolveElapsedYearBucketLabel = (bucket?: string) => { const normalized = String(bucket ?? "").trim(); return elapsedYearBucketLabelMap[normalized] ?? normalized; }; export const resolvePipeAnalysisTypeLabel = (analysisType?: string) => { const normalized = String(analysisType ?? "").trim(); return ( PIPE_ANALYSIS_TYPE_OPTIONS.find((option) => option.value === normalized)?.label ?? normalized ); }; export const resolveUseName = (useCode?: string) => { const code = String(useCode ?? "").trim(); if (!code) return "-"; return useMap.get(code) ?? code; }; export const resolveRegionName = ( code: string, options: SelectDataType[], prefixLength: number, ) => { const normalized = String(code ?? "").trim(); if (!normalized) return "-"; const prefix = normalized.slice(0, prefixLength); return options.find((item) => String(item.val).trim() === prefix)?.text ?? prefix; }; export const normalizeNumberInput = (value: string) => value.replace(/[^0-9]/g, ""); export const buildPipeAnalysisFileDate = () => new Date().toISOString().slice(0, 10); export const fetchPipeAnalysisZoneOptions = async ({ ctpvCd, sggCd, scopeRegionCode, }: { ctpvCd: string; sggCd: string; scopeRegionCode?: string; }): Promise => { const filters: string[] = []; if (sggCd && sggCd !== "0") { filters.push(`sgg_cd='${sggCd}'`); } else if (ctpvCd && ctpvCd !== "0") { filters.push(`ctpv_cd='${ctpvCd}'`); } else if (scopeRegionCode) { if (scopeRegionCode.length >= 5) { filters.push(`sgg_cd='${scopeRegionCode.slice(0, 5)}'`); } else if (scopeRegionCode.length >= 2) { filters.push(`ctpv_cd='${scopeRegionCode.slice(0, 2)}'`); } } const params = new URLSearchParams({ service: "WFS", version: "1.0.0", request: "GetFeature", typeName: "hasudo:tb_ms_swage_prcs_zone", outputFormat: "application/json", propertyName: "ctpv_cd,sgg_cd,swtrmt_zone_nm", }); if (filters.length > 0) { params.set("CQL_FILTER", filters.join(" and ")); } const response = await fetch(`/geoserver/hasudo/ows?${params.toString()}`); if (!response.ok) { throw new Error("처리구역 목록을 불러오지 못했습니다."); } const data = await response.json(); const features = Array.isArray(data?.features) ? data.features : []; const dedupMap = new Map(); features.forEach((feature: any) => { const properties = feature?.properties ?? {}; const zoneName = String(properties.swtrmt_zone_nm ?? "").trim(); if (!zoneName) return; dedupMap.set(zoneName, { value: zoneName, label: zoneName, ctpvCd: String(properties.ctpv_cd ?? "").trim(), sggCd: String(properties.sgg_cd ?? "").trim(), }); }); return Array.from(dedupMap.values()).sort((left, right) => left.label.localeCompare(right.label, "ko"), ); };