Files
frontend/apps/sn/src/features/dredging/model/dredging-list-view.ts
T

282 lines
9.9 KiB
TypeScript

import { DREDGING_EXTERNAL_MODE } from "./disclosure";
/** 준설 목록·집계 표의 컬럼·표시 변환. 서버 조회/React 상태와 분리한다. */
import type { DredgingRecord } from "./types";
import { MS_SWAGE_PPL_CODES } from "@/entities/facility/model/facility-code-fallbacks";
export const PAGE_SIZE = 10;
export const ALL_REGION_OPTION = { text: "전체", val: "0" };
export const pipeMaterialNameByCode = new Map(
MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]),
);
export const resolvePipeMaterialName = (
value: string,
names: ReadonlyMap<string, string> = pipeMaterialNameByCode,
) => {
const normalizedValue = value.trim();
if (!normalizedValue) return "확인불가";
const normalizedCode = normalizedValue.toUpperCase();
if (!/^MOP\d+$/.test(normalizedCode)) return normalizedValue;
return names.get(normalizedCode) ?? "확인불가";
};
// 백엔드·프록시 상세 오류 대신 사용자가 다음 행동을 알 수 있는 문구만 표시한다.
export const DREDGING_LOAD_ERROR_MESSAGE =
"준설현황을 불러오지 못했습니다. 잠시 후 다시 시도해주세요.";
export const currentYear = new Date().getFullYear();
export const formatLocalDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
/** 준설일자 기본 검색기간을 오늘까지의 최근 1년으로 생성한다. */
export const createDefaultDredgingDateRange = () => {
const to = new Date();
const from = new Date(to);
from.setFullYear(from.getFullYear() - 1);
return {
fromDate: formatLocalDate(from),
toDate: formatLocalDate(to),
};
};
export const initialRecords: DredgingRecord[] = [
{
id: "mock-dredging-1",
dredgingJobSn: "mock-dredging-job-1",
pipeFid: "MOCK-PIPE-1",
mngInstCd: "4173000",
ctpv: "경기도",
sgg: "양평군",
treatmentZone: "양평처리구역",
fcltMngNo: "PPL-DEMO-001",
pipeDiameterMm: 450,
pipeLengthM: 82.4,
pipeMaterial: "콘크리트",
installationYear: 2004,
dredgingDate: `${currentYear}-05-14`,
photoCount: 3,
registeredDate: `${currentYear}-05-15`,
longitude: 127.4875,
latitude: 37.4916,
snappedLongitude: 127.48751,
snappedLatitude: 37.49159,
registrationMode: "PHOTO",
registrationSource: "EXIF",
matchDistanceMeters: 4.28,
isMock: true,
},
{
id: "mock-dredging-2",
dredgingJobSn: "mock-dredging-job-2",
pipeFid: "MOCK-PIPE-2",
mngInstCd: "1153000",
ctpv: "서울특별시",
sgg: "구로구",
treatmentZone: "서남처리구역",
fcltMngNo: "PPL-DEMO-002",
pipeDiameterMm: 900,
pipeLengthM: 116.7,
pipeMaterial: "PVC",
installationYear: 1992,
dredgingDate: `${currentYear - 1}-11-08`,
photoCount: 0,
registeredDate: `${currentYear - 1}-11-09`,
longitude: 126.8874,
latitude: 37.4954,
snappedLongitude: 126.88741,
snappedLatitude: 37.49539,
registrationMode: "MANUAL",
registrationSource: "MANUAL",
matchDistanceMeters: 8.15,
isMock: true,
},
];
/** 상세·수정 팝업이 동일한 관로·위치 정보를 받도록 쿼리 파라미터를 한곳에서 구성한다. */
export const buildDredgingPopupParams = (record: DredgingRecord) => ({
sourcePath: window.location.pathname,
id: record.id,
dredgingJobSn: record.dredgingJobSn,
targetSn: record.targetSn,
mappingStatus: record.mappingStatus,
pipeFid: record.pipeFid,
mngInstCd: record.mngInstCd,
ctpv: record.ctpv,
sgg: record.sgg,
treatmentZone: record.treatmentZone,
fcltMngNo: record.fcltMngNo,
pipeDiameterMm: record.pipeDiameterMm,
pipeLengthM: record.pipeLengthM,
pipeMaterial: record.pipeMaterial,
installationYear: record.installationYear,
dredgingDate: record.dredgingDate,
photoCount: record.photoCount,
registeredDate: record.registeredDate,
longitude: record.longitude,
latitude: record.latitude,
snappedLongitude: record.snappedLongitude,
snappedLatitude: record.snappedLatitude,
registrationMode:
record.registrationMode ??
(record.photoCount === 0 && record.registrationSource === "MANUAL"
? "MANUAL"
: "PHOTO"),
registrationSource: record.registrationSource,
matchDistanceMeters: record.matchDistanceMeters,
isMock: record.isMock,
});
export type TableRow = Record<string, unknown> & {
recordIds: string[];
};
export type TableHeadDefinition = {
id: string;
title: string;
align?: "left" | "center" | "right";
};
const fullMunicipalityHeadlist: TableHeadDefinition[] = [
{ id: "mappingStatusLabel", title: "관로 지정" },
{ id: "rowNumber", title: "순번" },
{ id: "ctpv", title: "시도" },
{ id: "sgg", title: "시군구" },
{ id: "treatmentZone", title: "처리구역" },
{ id: "fcltMngNo", title: "관로 번호" },
{ id: "pipeDiameterMm", title: "관경(mm)", align: "right" },
{ id: "pipeLengthM", title: "관로연장(m)", align: "right" },
{ id: "pipeMaterial", title: "관재질" },
{ id: "installationYear", title: "매설년도" },
{ id: "elapsedYears", title: "경과년도" },
{ id: "dredgingDate", title: "준설일자" },
{ id: "photoCount", title: "등록사진건수", align: "right" },
{ id: "registeredDate", title: "등록일자" },
];
export const municipalityHeadlist = DREDGING_EXTERNAL_MODE
? fullMunicipalityHeadlist.filter(column => ["rowNumber", "ctpv", "sgg", "dredgingDate", "photoCount", "registeredDate"].includes(column.id))
: fullMunicipalityHeadlist;
export const diameterHeadlist: TableHeadDefinition[] = [
{ id: "ctpv", title: "시도" },
{ id: "totalLengthKm", title: "관로연장(km)", align: "right" },
{ id: "diameter450", title: "450 이하", align: "right" },
{ id: "diameter600", title: "450~600", align: "right" },
{ id: "diameter700", title: "700", align: "right" },
{ id: "diameter900", title: "800~900", align: "right" },
{ id: "diameter1000", title: "1000", align: "right" },
{ id: "diameter1200", title: "1200", align: "right" },
{ id: "diameter1500", title: "1500 이상", align: "right" },
];
export const elapsedYearHeadlist: TableHeadDefinition[] = [
{ id: "ctpv", title: "시도" },
{ id: "totalLengthKm", title: "관로연장(km)", align: "right" },
{ id: "under10", title: "10년 미만", align: "right" },
{ id: "year10To20", title: "10년~20년", align: "right" },
{ id: "year20To30", title: "20년~30년", align: "right" },
{ id: "over30", title: "30년 이상", align: "right" },
{ id: "unknown", title: "확인불가", align: "right" },
];
export const roundKm = (meters: number) => Number((meters / 1000).toFixed(3));
export const parseLocalDate = (value: string) => new Date(`${value}T00:00:00`);
export const formatTableValue = (value: unknown) => {
if (typeof value === "number") return value.toLocaleString("ko-KR");
if (value === null || value === undefined) return "";
return String(value);
};
export const groupByCtpv = (records: DredgingRecord[]) => {
const groups = new Map<string, DredgingRecord[]>();
for (const record of records) {
groups.set(record.ctpv, [...(groups.get(record.ctpv) ?? []), record]);
}
return groups;
};
export const buildDiameterRows = (records: DredgingRecord[]): TableRow[] =>
Array.from(groupByCtpv(records), ([ctpv, items]) => {
const sumLength = (predicate: (record: DredgingRecord) => boolean) =>
roundKm(
items
.filter(predicate)
.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0),
);
return {
ctpv,
totalLengthKm: roundKm(
items.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0),
),
diameter450: sumLength((record) => (record.pipeDiameterMm ?? 0) <= 450),
diameter600: sumLength(
(record) => (record.pipeDiameterMm ?? 0) > 450 && (record.pipeDiameterMm ?? 0) <= 600,
),
diameter700: sumLength(
(record) => (record.pipeDiameterMm ?? 0) > 600 && (record.pipeDiameterMm ?? 0) <= 700,
),
diameter900: sumLength(
(record) => (record.pipeDiameterMm ?? 0) > 700 && (record.pipeDiameterMm ?? 0) <= 900,
),
diameter1000: sumLength(
(record) =>
(record.pipeDiameterMm ?? 0) > 900 && (record.pipeDiameterMm ?? 0) <= 1000,
),
diameter1200: sumLength(
(record) =>
(record.pipeDiameterMm ?? 0) > 1000 && (record.pipeDiameterMm ?? 0) <= 1200,
),
diameter1500: sumLength((record) => (record.pipeDiameterMm ?? 0) >= 1500),
recordIds: items.map((record) => record.id),
};
});
export const buildElapsedYearRows = (records: DredgingRecord[]): TableRow[] =>
Array.from(groupByCtpv(records), ([ctpv, items]) => {
const sumLength = (predicate: (record: DredgingRecord) => boolean) =>
roundKm(
items
.filter(predicate)
.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0),
);
const elapsedYears = (record: DredgingRecord) =>
record.installationYear === null
? null
: currentYear - record.installationYear;
return {
ctpv,
totalLengthKm: roundKm(
items.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0),
),
under10: sumLength((record) => {
const elapsed = elapsedYears(record);
return elapsed !== null && elapsed < 10;
}),
year10To20: sumLength((record) => {
const elapsed = elapsedYears(record);
return elapsed !== null && elapsed >= 10 && elapsed < 20;
}),
year20To30: sumLength((record) => {
const elapsed = elapsedYears(record);
return elapsed !== null && elapsed >= 20 && elapsed < 30;
}),
over30: sumLength((record) => {
const elapsed = elapsedYears(record);
return elapsed !== null && elapsed >= 30;
}),
unknown: sumLength((record) => elapsedYears(record) === null),
recordIds: items.map((record) => record.id),
};
});